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() 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('') expect(palletsSource).not.toContain('') expect(palletsSource).not.toContain('') expect(palletsSource).toContain('@media (max-width: 720px)') }) it('uses the prototype-sized centered editor without an extra detail drawer', () => { expect(palletsSource).not.toContain(' { 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 vm.openEdit(pallet) void vm.openEdit(secondPallet) resolveSecond(secondPallet) await flushPromises() resolveFirst(pallet) await flushPromises() expect(vm.form).toMatchObject({ palletCode: 'TP-002', color: '蓝色' }) }) })