import ElementPlus from 'element-plus' 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 { listPlatformTenants as listPlatformCustomers, type PlatformTenantResource as PlatformCustomerResource } from '@/modules/iam' import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses' import DataTable from '@/shared/components/DataTable.vue' import QueryToolbar from '@/shared/components/QueryToolbar.vue' import { createPlatformPallet, getPlatformPallet, listPlatformPallets, type PlatformPalletResource, } from '../api/platformPallets' import PalletsView from './PalletsView.vue' vi.mock('@/modules/iam', async (importOriginal) => { const actual = await importOriginal() const listPlatformTenants = vi.fn() return { ...actual, listPlatformTenants } }) vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: vi.fn() })) vi.mock('../api/platformPallets', () => ({ listPlatformPallets: 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 customer = (id: number | string, tenantName: string): PlatformCustomerResource => ({ id, tenantCode: String(id), tenantName, status: 'ACTIVE', legalName: tenantName, legalRepresentative: null, unifiedSocialCreditCode: null, contactName: null, contactPhone: null, contactEmail: null, servicePackage: null, contactWechat: null, salesOwnerName: null, paymentTermDays: null, enterpriseType: null, establishedDate: null, registeredCapital: null, registeredCapitalCurrency: null, registeredAddress: null, internalRemark: null, createdAt: null, approvedAt: null, }) const warehouse: PlatformWarehouseResource = { id: '19001', tenantId: 10001, warehouseCode: 'CN-YW-01', warehouseName: '义乌收货仓', warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null, } const pallet: PlatformPalletResource = { id: '20001', tenantId: 10001, warehouseId: '19001', palletCode: 'TP-001', palletType: '木托盘', capacityCbm: '1.200000', 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 setFilterContext(wrapper: VueWrapper, customerId = '10001', warehouseId = '') { wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', { customerId, warehouseId: '', palletCode: '', palletType: '', }) await flushPromises() if (!warehouseId) return wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', { customerId, warehouseId, palletCode: '', palletType: '', }) await flushPromises() } async function loadRows(wrapper: VueWrapper, warehouseId = '') { await setFilterContext(wrapper, '10001', warehouseId) wrapper.findComponent(QueryToolbar).vm.$emit('search', { customerId: '10001', warehouseId, palletCode: '', palletType: '', }) await flushPromises() } enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('PalletsView', () => { beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) document.body.innerHTML = '' vi.mocked(listPlatformCustomers).mockReset().mockResolvedValue({ records: [customer(10001, '默认企业'), customer('9223372036854775806', '雪花企业')], page: 1, size: 200, total: 2, pages: 1, }) vi.mocked(listPlatformWarehouses).mockReset().mockResolvedValue({ records: [warehouse], page: 1, size: 200, total: 1, pages: 1 }) vi.mocked(listPlatformPallets).mockReset().mockResolvedValue({ records: [pallet], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(getPlatformPallet).mockReset().mockResolvedValue(pallet) vi.mocked(createPlatformPallet).mockReset().mockResolvedValue('20002') }) it('starts with all customers and uses compact semantic filter widths', async () => { const wrapper = await mountView() const toolbar = wrapper.findComponent(QueryToolbar) const fields = toolbar.props('fields') expect(toolbar.props('modelValue')).toMatchObject({ customerId: '', warehouseId: '' }) expect(fields.map((field) => [field.key, field.width])).toEqual([ ['customerId', 260], ['warehouseId', 240], ['palletCode', 240], ['palletType', 180], ]) expect(wrapper.find('.pallet-filter-toolbar').exists()).toBe(true) expect(fields.find((field) => field.key === 'customerId')?.placeholder).toBe('全部客户') expect(listPlatformWarehouses).not.toHaveBeenCalled() expect(listPlatformPallets).toHaveBeenCalledWith({ page: 1, size: 20, }) }) it('loads warehouses after customer selection and omits an empty warehouse id when searching', async () => { const wrapper = await mountView() await loadRows(wrapper) expect(listPlatformWarehouses).toHaveBeenCalledWith(10001, 1, 200) expect(listPlatformPallets).toHaveBeenCalledWith({ page: 1, size: 20, tenantId: 10001, }) expect(wrapper.text()).toContain('TP-001') expect(wrapper.text()).toContain('木托盘') expect(wrapper.text()).toContain('1.2 m³') expect(wrapper.text()).toContain('义乌收货仓') expect(wrapper.text()).toContain('托盘列表') expect(wrapper.text()).not.toContain('启停托盘') expect(wrapper.text()).not.toContain('全部状态') expect(wrapper.text()).toContain('全部托盘类型') expect(wrapper.find('input[aria-label="所属企业"]').exists()).toBe(true) 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('.table-actions.pallet-row-actions').exists()).toBe(true) expect(wrapper.findAll('thead th').map((item) => item.text().trim())[0]).toBe('操作') }) 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', { customerId: '', warehouseId: '', palletCode: '', palletType: '' }) await flushPromises() expect(listPlatformPallets).toHaveBeenLastCalledWith({ page: 1, size: 20, }) }) it('resets customer and dependent warehouse filters without sending an empty id', async () => { const wrapper = await mountView() await setFilterContext(wrapper, '10001', '19001') const toolbar = wrapper.findComponent(QueryToolbar) toolbar.vm.reset() await flushPromises() expect(toolbar.props('modelValue')).toMatchObject({ customerId: '', warehouseId: '' }) expect(listPlatformPallets).toHaveBeenCalledTimes(2) expect(listPlatformPallets).toHaveBeenLastCalledWith({ page: 1, size: 20, }) expect(toolbar.props('fields').find((field) => field.key === 'customerId')?.placeholder).toBe('全部客户') }) it('preserves a snowflake customer id when switching the enterprise filter', async () => { const wrapper = await mountView() const snowflakeId = '9223372036854775806' wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', { customerId: snowflakeId, warehouseId: '', palletCode: '', palletType: '', }) await flushPromises() expect(listPlatformWarehouses).toHaveBeenLastCalledWith(snowflakeId, 1, 200) }) it('applies pallet filters only after search', async () => { const wrapper = await mountView() await setFilterContext(wrapper, '10001', '19001') await wrapper.find('input[placeholder="托盘编码(模糊查询)"]').setValue('TP-00') expect(listPlatformPallets).toHaveBeenCalledTimes(1) wrapper.findComponent(QueryToolbar).vm.$emit('search', { customerId: '10001', warehouseId: '19001', palletCode: 'TP-00', palletType: '木托盘', }) await flushPromises() expect(listPlatformPallets).toHaveBeenLastCalledWith({ page: 1, size: 20, tenantId: 10001, 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 setFilterContext(wrapper, '10001', '19001') const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增托盘') await createButton?.trigger('click') await flushPromises() await wrapper.find('input[placeholder="例如 TP-001"]').setValue('TP-002') const palletTypeItem = wrapper.findAll('.el-form-item').find((item) => item.text().includes('托盘类型')) palletTypeItem?.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '塑料托盘') const capacity = wrapper.findComponent({ name: 'ElInputNumber' }) capacity.vm.$emit('update:modelValue', 1.5) await wrapper.find('textarea[placeholder="可填写规格、材质或现场识别说明"]').setValue('蓝色标准托盘') const saveButton = wrapper.findAll('button').find((button) => button.text().trim() === '保存托盘') await saveButton?.trigger('click') await flushPromises() expect(createPlatformPallet).toHaveBeenCalledWith(10001, { warehouseId: 19001, palletCode: 'TP-002', palletType: '塑料托盘', capacityCbm: 1.5, remark: '蓝色标准托盘', }) }) 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).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()).toContain('Gateway unavailable') expect(wrapper.text()).toContain('重新加载') expect(wrapper.text()).not.toContain('TP-001') }) it('stops dependent requests when the enterprise filter cannot load', async () => { vi.mocked(listPlatformCustomers).mockRejectedValueOnce(new Error('Gateway unavailable')) const wrapper = await mountView() expect(wrapper.text()).toContain('所属企业列表暂时无法加载') expect(wrapper.text()).toContain('重新加载') expect(listPlatformWarehouses).not.toHaveBeenCalled() expect(listPlatformPallets).toHaveBeenCalledWith({ page: 1, size: 20, }) }) it('shows only the pallet fields exposed by the backend contract', async () => { const crossCustomerPallet = { ...pallet, tenantId: '9223372036854775806' } vi.mocked(listPlatformPallets).mockResolvedValue({ records: [crossCustomerPallet], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(getPlatformPallet).mockResolvedValue(crossCustomerPallet) const wrapper = await mountView() await loadRows(wrapper) expect(wrapper.findComponent(DataTable).props('data')).toEqual([crossCustomerPallet]) const tableBody = wrapper.find('.el-table__body-wrapper') const viewButton = tableBody.findAll('.pallet-row-actions button').find((button) => button.text().trim() === '查看') await viewButton?.trigger('click') await flushPromises() expect(getPlatformPallet).toHaveBeenCalledWith('9223372036854775806', 19001, '20001') expect(document.body.textContent).toContain('所属企业') expect(document.body.textContent).toContain('雪花企业(9223372036854775806)') expect(document.body.textContent).toContain('托盘编码') expect(document.body.textContent).toContain('托盘类型') expect(document.body.textContent).toContain('托盘容积') expect(document.body.textContent).toContain('归属信息') expect(document.body.textContent).toContain('托盘规格') expect(document.body.textContent).toContain('记录信息') expect(document.body.textContent).not.toContain('企业 ID') expect(document.body.textContent).not.toContain('租户') expect(document.body.textContent).not.toContain('托盘状态') expect(document.body.textContent).not.toContain('最大承重') }) })