384 lines
17 KiB
TypeScript
384 lines
17 KiB
TypeScript
import ElementPlus from 'element-plus'
|
||
import { readFileSync } from 'node:fs'
|
||
import { resolve } from 'node:path'
|
||
import { createPinia, setActivePinia } from 'pinia'
|
||
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { useAuthStore } from '@/core/auth/auth'
|
||
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
|
||
import DataTable from '@/shared/components/DataTable.vue'
|
||
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||
import {
|
||
createPlatformPallet,
|
||
getPlatformPallet,
|
||
getPlatformPalletSummary,
|
||
listPlatformPallets,
|
||
type PlatformPalletResource,
|
||
} from '../api/platformPallets'
|
||
import PalletsView from './PalletsView.vue'
|
||
|
||
const palletsSource = readFileSync(resolve(process.cwd(), 'src/modules/warehouse-resources/pallets/pages/PalletsView.vue'), 'utf8')
|
||
|
||
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
|
||
vi.mock('../api/platformPallets', () => ({
|
||
listPlatformPallets: vi.fn(),
|
||
getPlatformPalletSummary: vi.fn(),
|
||
getPlatformPallet: vi.fn(),
|
||
createPlatformPallet: vi.fn(),
|
||
updatePlatformPallet: vi.fn(),
|
||
deletePlatformPallet: vi.fn(),
|
||
}))
|
||
|
||
class ResizeObserverStub implements ResizeObserver {
|
||
observe() {}
|
||
unobserve() {}
|
||
disconnect() {}
|
||
}
|
||
|
||
function createMemoryStorage(): Storage {
|
||
const values = new Map<string, string>()
|
||
return {
|
||
get length() { return values.size },
|
||
clear: () => values.clear(),
|
||
getItem: (key) => values.get(key) ?? null,
|
||
key: (index) => [...values.keys()][index] ?? null,
|
||
removeItem: (key) => values.delete(key),
|
||
setItem: (key, value) => values.set(key, value),
|
||
}
|
||
}
|
||
|
||
const warehouse: PlatformWarehouseResource = {
|
||
id: '19001', tenantId: 0, warehouseCode: 'CN-YW-01', internalCode: null, warehouseName: '义乌收货仓',
|
||
warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null,
|
||
contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null,
|
||
}
|
||
|
||
const fbaWarehouse: PlatformWarehouseResource = {
|
||
...warehouse,
|
||
id: '19002',
|
||
warehouseCode: 'FBA-ONT8',
|
||
internalCode: '154',
|
||
warehouseName: 'ONT8 FBA 仓',
|
||
warehouseType: 'FBA',
|
||
}
|
||
|
||
const overseasWarehouse: PlatformWarehouseResource = {
|
||
...warehouse,
|
||
id: '19003',
|
||
warehouseCode: 'US-LAX-01',
|
||
internalCode: null,
|
||
warehouseName: '洛杉矶海外仓',
|
||
warehouseType: 'OVERSEAS',
|
||
}
|
||
|
||
const pallet: PlatformPalletResource = {
|
||
id: '20001', tenantId: 0, warehouseId: '19001', palletCode: 'TP-001', palletType: '木托盘',
|
||
capacityCbm: '1.200000', weightKg: '8.000', color: '黄色', remark: '标准川字托盘', createdAt: '2026-08-04T09:00:00', updatedAt: '2026-08-04T10:00:00',
|
||
}
|
||
|
||
async function mountView(permissions = [
|
||
'platform:pallet:list', 'platform:pallet:create', 'platform:pallet:edit', 'platform:pallet:delete',
|
||
]) {
|
||
vi.stubGlobal('localStorage', createMemoryStorage())
|
||
vi.stubGlobal('sessionStorage', createMemoryStorage())
|
||
const pinia = createPinia()
|
||
setActivePinia(pinia)
|
||
const auth = useAuthStore()
|
||
auth.token = 'token'
|
||
auth.permissions = permissions
|
||
useAccessPreviewStore().initializeAfterLogin()
|
||
const wrapper = mount(PalletsView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||
await flushPromises()
|
||
return wrapper
|
||
}
|
||
|
||
async function setWarehouseFilter(wrapper: VueWrapper, warehouseId = '') {
|
||
wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', {
|
||
warehouseId,
|
||
palletCode: '',
|
||
palletType: '',
|
||
})
|
||
await flushPromises()
|
||
}
|
||
|
||
async function loadRows(wrapper: VueWrapper, warehouseId = '') {
|
||
await setWarehouseFilter(wrapper, warehouseId)
|
||
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
|
||
warehouseId,
|
||
palletCode: '',
|
||
palletType: '',
|
||
})
|
||
await flushPromises()
|
||
}
|
||
|
||
enableAutoUnmount(afterEach)
|
||
afterAll(() => vi.unstubAllGlobals())
|
||
|
||
const densitySource = readFileSync(resolve(process.cwd(), 'src/styles/adminDensity.css'), 'utf8')
|
||
|
||
describe('PalletsView', () => {
|
||
it('uses bordered striped tables and explicit semantic row actions', () => {
|
||
expect(palletsSource).toContain('border')
|
||
expect(palletsSource).toContain('stripe')
|
||
expect(palletsSource).toContain('class="density-action" size="small" title="编辑托盘"')
|
||
expect(palletsSource).toContain('class="density-action" data-tone="danger"')
|
||
expect(palletsSource).not.toContain('<Eye :size="15" />')
|
||
expect(palletsSource).not.toContain('<Pencil :size="15" />')
|
||
expect(palletsSource).not.toContain('<Trash2 :size="15" />')
|
||
expect(palletsSource).toContain('@media (max-width: 720px)')
|
||
})
|
||
it('uses the prototype-sized centered editor without an extra detail drawer', () => {
|
||
expect(palletsSource).not.toContain('<AppDrawer')
|
||
expect(palletsSource).toContain('<AppDialog v-model="editorOpen" :title="editorTitle" width="min(560px, calc(100vw - 28px))"')
|
||
expect(palletsSource).not.toContain('<el-drawer v-model="editorOpen"')
|
||
expect(densitySource).toContain('.admin-density-dialog.el-dialog .el-dialog__body { min-height: 0; overflow-y: auto;')
|
||
expect(palletsSource).toContain('.editor-field-grid { grid-template-columns: minmax(0, 1fr); }')
|
||
})
|
||
beforeEach(() => {
|
||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||
document.body.innerHTML = ''
|
||
vi.mocked(listAllPlatformWarehouses).mockReset().mockResolvedValue([warehouse, fbaWarehouse, overseasWarehouse])
|
||
vi.mocked(listPlatformPallets).mockReset().mockResolvedValue({ records: [pallet], page: 1, size: 20, total: 1, pages: 1 })
|
||
vi.mocked(getPlatformPalletSummary).mockReset().mockResolvedValue({ totalCount: 1, idleCount: 1, occupiedCount: 0 })
|
||
vi.mocked(getPlatformPallet).mockReset().mockResolvedValue(pallet)
|
||
vi.mocked(createPlatformPallet).mockReset().mockResolvedValue('20002')
|
||
})
|
||
|
||
it('starts with enabled domestic warehouse candidates for querying using compact filters', async () => {
|
||
const wrapper = await mountView()
|
||
const toolbar = wrapper.findComponent(QueryToolbar)
|
||
const fields = toolbar.props('fields')
|
||
|
||
expect(toolbar.props('modelValue')).toMatchObject({ warehouseId: '' })
|
||
expect(fields.map((field) => [field.key, field.width])).toEqual([
|
||
['warehouseId', 300],
|
||
['palletCode', 240],
|
||
['palletType', 180],
|
||
])
|
||
expect(wrapper.find('.pallet-filter-toolbar').exists()).toBe(true)
|
||
expect(fields.find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
|
||
expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([
|
||
{ label: '义乌收货仓(CN-YW-01)', value: '19001' },
|
||
])
|
||
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
|
||
expect(listPlatformPallets).toHaveBeenCalledWith({
|
||
page: 1, size: 20,
|
||
})
|
||
})
|
||
|
||
it('loads all warehouses independently and omits an empty warehouse id when searching', async () => {
|
||
const wrapper = await mountView()
|
||
await loadRows(wrapper)
|
||
|
||
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
|
||
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
|
||
expect(listPlatformPallets).toHaveBeenCalledWith({
|
||
page: 1, size: 20,
|
||
})
|
||
expect(wrapper.text()).toContain('TP-001')
|
||
expect(wrapper.text()).toContain('义乌收货仓')
|
||
expect(wrapper.text()).toContain('CN-YW-01')
|
||
expect(wrapper.text()).toContain('木托盘')
|
||
expect(wrapper.text()).toContain('1.2 m³')
|
||
expect(wrapper.text()).toContain('8 kg')
|
||
expect(wrapper.find('.data-table').text()).not.toContain('黄色')
|
||
expect(wrapper.find('.data-table').text()).not.toContain('标准川字托盘')
|
||
expect(wrapper.text()).not.toContain('更新时间')
|
||
const tableToolbar = wrapper.find('.data-table--integrated .data-table__toolbar')
|
||
expect(tableToolbar.exists()).toBe(true)
|
||
expect(tableToolbar.text()).toContain('新增托盘')
|
||
expect(tableToolbar.text()).not.toContain('托盘列表')
|
||
expect(wrapper.find('.data-table__pagination-left').text()).toContain('共 1 个托盘')
|
||
expect(wrapper.text()).not.toContain('启停托盘')
|
||
expect(wrapper.text()).not.toContain('全部状态')
|
||
expect(wrapper.text()).toContain('全部托盘类型')
|
||
expect(wrapper.find('input[aria-label="客户"]').exists()).toBe(false)
|
||
expect(wrapper.find('input[aria-label="所属仓库"]').exists()).toBe(true)
|
||
expect(wrapper.find('.platform-tenant-selector').exists()).toBe(false)
|
||
expect(wrapper.text()).not.toContain('租户')
|
||
expect(wrapper.find('.data-table').exists()).toBe(true)
|
||
expect(wrapper.find('.density-actions.pallet-row-actions').exists()).toBe(true)
|
||
expect(wrapper.findAll('thead th').map((item) => item.text().trim())).toEqual([
|
||
'操作', '托盘编码', '所属仓库', '托盘类型', '托盘容积(m³)', '托盘重量(kg)',
|
||
])
|
||
})
|
||
|
||
it('queries all customers explicitly without adding a customer id', async () => {
|
||
vi.mocked(listPlatformPallets).mockResolvedValue({ records: [pallet], page: 1, size: 20, total: 41, pages: 3 })
|
||
const wrapper = await mountView()
|
||
const toolbar = wrapper.findComponent(QueryToolbar)
|
||
|
||
toolbar.vm.$emit('search', { warehouseId: '', palletCode: '', palletType: '' })
|
||
await flushPromises()
|
||
expect(listPlatformPallets).toHaveBeenLastCalledWith({
|
||
page: 1, size: 20,
|
||
})
|
||
|
||
})
|
||
|
||
it('resets the warehouse filter without sending an empty id', async () => {
|
||
const wrapper = await mountView()
|
||
await setWarehouseFilter(wrapper, '19001')
|
||
const toolbar = wrapper.findComponent(QueryToolbar)
|
||
|
||
toolbar.vm.reset()
|
||
await flushPromises()
|
||
|
||
expect(toolbar.props('modelValue')).toMatchObject({ warehouseId: '' })
|
||
expect(listPlatformPallets).toHaveBeenCalledTimes(2)
|
||
expect(listPlatformPallets).toHaveBeenLastCalledWith({
|
||
page: 1, size: 20,
|
||
})
|
||
expect(toolbar.props('fields').find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
|
||
})
|
||
|
||
it('does not expose customer ownership in warehouse choices', async () => {
|
||
const wrapper = await mountView()
|
||
const warehouseField = wrapper.findComponent(QueryToolbar).props('fields').find((field) => field.key === 'warehouseId')
|
||
|
||
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
|
||
expect(warehouseField?.options).toEqual([
|
||
{ label: '义乌收货仓(CN-YW-01)', value: '19001' },
|
||
])
|
||
})
|
||
|
||
it('does not offer editing for a pallet outside a domestic warehouse', async () => {
|
||
vi.mocked(listPlatformPallets).mockResolvedValue({
|
||
records: [{ ...pallet, id: '20002', warehouseId: fbaWarehouse.id }, pallet], page: 1, size: 20, total: 2, pages: 1,
|
||
})
|
||
const wrapper = await mountView()
|
||
|
||
const bodyRows = wrapper.findAll('.el-table__body-wrapper tbody tr')
|
||
expect(bodyRows[0]?.text()).not.toContain('编辑')
|
||
expect(bodyRows[1]?.text()).toContain('编辑')
|
||
})
|
||
|
||
it('applies pallet filters only after search', async () => {
|
||
const wrapper = await mountView()
|
||
await setWarehouseFilter(wrapper, '19001')
|
||
await wrapper.find('input[placeholder="托盘编码(模糊查询)"]').setValue('TP-00')
|
||
expect(listPlatformPallets).toHaveBeenCalledTimes(1)
|
||
|
||
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
|
||
warehouseId: '19001', palletCode: 'TP-00', palletType: '木托盘',
|
||
})
|
||
await flushPromises()
|
||
|
||
expect(listPlatformPallets).toHaveBeenLastCalledWith({
|
||
page: 1, size: 20, warehouseId: 19001, palletCode: 'TP-00', palletType: '木托盘',
|
||
})
|
||
})
|
||
|
||
it('creates a pallet in the selected warehouse with the real request fields', async () => {
|
||
const wrapper = await mountView()
|
||
await setWarehouseFilter(wrapper, '19001')
|
||
const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增托盘')
|
||
await createButton?.trigger('click')
|
||
await flushPromises()
|
||
|
||
const palletTypeItem = wrapper.findAll('.el-form-item').find((item) => item.text().includes('托盘类型'))
|
||
palletTypeItem?.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '塑料托盘')
|
||
const inputNumbers = wrapper.findAllComponents({ name: 'ElInputNumber' })
|
||
inputNumbers[0]?.vm.$emit('update:modelValue', 1.5)
|
||
inputNumbers[1]?.vm.$emit('update:modelValue', 10)
|
||
await wrapper.find('input[placeholder="例如 黄色"]').setValue('黑色')
|
||
await wrapper.find('input[placeholder="选填"]').setValue('标准托盘')
|
||
const saveButton = wrapper.findAll('button').find((button) => button.text().trim() === '保存托盘')
|
||
await saveButton?.trigger('click')
|
||
await flushPromises()
|
||
|
||
expect(createPlatformPallet).toHaveBeenCalledWith({
|
||
warehouseId: 19001, palletType: '塑料托盘', capacityCbm: 1.5, weightKg: 10, color: '黑色', remark: '标准托盘',
|
||
})
|
||
})
|
||
|
||
it('uses a compact three-row two-column editor', async () => {
|
||
const wrapper = await mountView()
|
||
const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增托盘')
|
||
await createButton?.trigger('click')
|
||
await flushPromises()
|
||
|
||
const editorDialog = document.body.querySelector('.pallet-editor-dialog')
|
||
expect(editorDialog?.textContent).toContain('新增托盘')
|
||
expect(editorDialog?.querySelector('.el-dialog__title')?.textContent).toBe('新增托盘')
|
||
expect(document.body.querySelectorAll('.editor-field-grid')).toHaveLength(3)
|
||
expect(document.body.querySelector('.editor-section')).toBeNull()
|
||
expect(document.body.querySelector('.editor-context')).toBeNull()
|
||
expect(document.body.querySelector('.pallet-editor-footer')?.textContent).toContain('为必填项')
|
||
})
|
||
|
||
it('hides all write actions without write permissions', async () => {
|
||
const wrapper = await mountView(['platform:pallet:list'])
|
||
|
||
expect(wrapper.find('.panel-heading button').exists()).toBe(false)
|
||
const actions = wrapper.find('.pallet-row-actions').text()
|
||
expect(actions).not.toContain('编辑')
|
||
expect(actions).not.toContain('删除')
|
||
expect(actions).not.toContain('查看')
|
||
})
|
||
|
||
it('shows a recoverable list failure instead of demo data', async () => {
|
||
vi.mocked(listPlatformPallets).mockRejectedValueOnce(new Error('Gateway unavailable'))
|
||
const wrapper = await mountView()
|
||
|
||
expect(wrapper.text()).toContain('托盘列表暂时无法加载')
|
||
expect(wrapper.text()).not.toContain('Gateway unavailable')
|
||
expect(wrapper.text()).toContain('重新加载')
|
||
expect(wrapper.text()).not.toContain('TP-001')
|
||
})
|
||
|
||
it('keeps platform warehouse and pallet queries independent from customer names', async () => {
|
||
const wrapper = await mountView()
|
||
|
||
expect(wrapper.text()).not.toContain('客户列表暂时无法加载')
|
||
expect(wrapper.text()).not.toContain('所属企业')
|
||
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
|
||
expect(listPlatformPallets).toHaveBeenCalledWith({
|
||
page: 1, size: 20,
|
||
})
|
||
})
|
||
|
||
it('keeps the list limited to fields exposed by the backend contract', async () => {
|
||
const publicPallet = { ...pallet, tenantId: '0' }
|
||
vi.mocked(listPlatformPallets).mockResolvedValue({ records: [publicPallet], page: 1, size: 20, total: 1, pages: 1 })
|
||
vi.mocked(getPlatformPallet).mockResolvedValue(publicPallet)
|
||
const wrapper = await mountView()
|
||
await loadRows(wrapper)
|
||
expect(wrapper.findComponent(DataTable).props('data')).toEqual([publicPallet])
|
||
const tableText = wrapper.find('.data-table').text()
|
||
expect(tableText).toContain('托盘编码')
|
||
expect(tableText).toContain('托盘类型')
|
||
expect(tableText).toContain('托盘容积')
|
||
expect(tableText).not.toContain('所属企业')
|
||
expect(tableText).not.toContain('企业 ID')
|
||
expect(tableText).not.toContain('租户')
|
||
expect(tableText).not.toContain('托盘状态')
|
||
expect(tableText).not.toContain('最大承重')
|
||
})
|
||
|
||
it('keeps the latest pallet selected for editing', async () => {
|
||
const secondPallet = { ...pallet, id: '20002', palletCode: 'TP-002', color: '蓝色' }
|
||
let resolveFirst!: (value: PlatformPalletResource) => void
|
||
let resolveSecond!: (value: PlatformPalletResource) => void
|
||
vi.mocked(getPlatformPallet).mockImplementation((_warehouseId, palletId) => new Promise((resolve) => {
|
||
if (String(palletId) === String(pallet.id)) resolveFirst = resolve
|
||
else resolveSecond = resolve
|
||
}))
|
||
const wrapper = await mountView()
|
||
const vm = wrapper.vm as unknown as {
|
||
form: { palletCode: string; color: string }
|
||
openEdit(row: PlatformPalletResource): Promise<void>
|
||
}
|
||
|
||
void vm.openEdit(pallet)
|
||
void vm.openEdit(secondPallet)
|
||
resolveSecond(secondPallet)
|
||
await flushPromises()
|
||
resolveFirst(pallet)
|
||
await flushPromises()
|
||
|
||
expect(vm.form).toMatchObject({ palletCode: 'TP-002', color: '蓝色' })
|
||
})
|
||
})
|