import ElementPlus from 'element-plus' import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import AppDialog from '@/shared/components/AppDialog.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import { overseasWarehousePermissions as permission } from '../permissions' import type { PalletDto } from '../api/pallets' import PalletsView from './PalletsView.vue' const api = vi.hoisted(() => ({ warehouses: vi.fn(), fba: vi.fn(), list: vi.fn(), summary: vi.fn(), detail: vi.fn(), create: vi.fn(), update: vi.fn(), print: vi.fn() })) const can = vi.hoisted(() => vi.fn((_code: string) => true)) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: api.fba })) vi.mock('../api/pallets', () => ({ listReceivingOverseasWarehouses: api.warehouses, listPallets: api.list, getPalletSummary: api.summary, getPallet: api.detail, createPallet: api.create, updatePallet: api.update, getPalletLabelPdf: api.print })) enableAutoUnmount(afterEach) afterEach(() => vi.unstubAllGlobals()) function pallet(): PalletDto { return { id: '9007199254740993', customerId: '0', warehouseId: '9007199254740994', palletCode: 'PLT-LAX-001', palletType: 'WOOD', fbaWarehouseCode: 'ABE3', capacityCbm: 2, unitPrice: 10, status: 'IDLE', remark: '原备注', version: 0 } } beforeEach(() => { vi.resetAllMocks(); can.mockReturnValue(true) vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) api.warehouses.mockResolvedValue({ records: [{ id: '9007199254740994', warehouseName: '洛杉矶仓', warehouseCode: 'LAX' }] }) api.fba.mockResolvedValue([{ id: 'F1', warehouseName: 'ABE3仓', warehouseCode: 'ABE3' }]) api.list.mockResolvedValue({ records: [pallet()], total: 1 }) api.summary.mockResolvedValue({ totalCount: 1, idleCount: 1, occupiedCount: 0, scrappedCount: 0 }) api.detail.mockImplementation(async () => pallet()); api.create.mockResolvedValue('9007199254740995'); api.update.mockResolvedValue(null) }) function field(dialog: Pick, label: string) { return dialog.findAllComponents({ name: 'ElFormItem' }).find(item => item.props('label') === label)! } async function page() { const wrapper = mount(PalletsView, { global: { plugins: [ElementPlus] } }); await flushPromises(); return wrapper } async function open(action = '新增托盘') { const wrapper = await page(); await wrapper.findAll('button').filter(button => button.text() === action).at(-1)!.trigger('click'); await flushPromises(); return { wrapper, dialog: wrapper.findComponent(AppDialog) } } async function fill(dialog: VueWrapper) { await field(dialog, '托盘编码').get('input').setValue(' PLT-NEW ') for (const [label, value] of [['托盘类型', 'PLASTIC'], ['仓库', '9007199254740994'], ['所属 FBA 仓', 'ABE3'], ['状态', 'OCCUPIED']]) field(dialog, label!).getComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', value) field(dialog, '容积(m³)').getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2.3456) field(dialog, '采购价格(USD)').getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 12.34) await field(dialog, '备注').get('textarea').setValue(' 新备注 '); await flushPromises() } async function confirm(dialog: VueWrapper) { await dialog.get('.app-dialog__footer .el-button--primary').trigger('click'); await flushPromises() } describe('native overseas pallet dialogs', () => { it('retains every field, precision and exact creation payload through fullscreen', async () => { const { dialog } = await open(); await fill(dialog) expect(dialog.findAllComponents({ name: 'ElFormItem' })).toHaveLength(8) expect(field(dialog, '容积(m³)').getComponent({ name: 'ElInputNumber' }).props()).toMatchObject({ min: 0.0001, precision: 4 }) expect(field(dialog, '采购价格(USD)').getComponent({ name: 'ElInputNumber' }).props()).toMatchObject({ min: 0, precision: 2 }) expect(field(dialog, '托盘类型').findAllComponents({ name: 'ElOption' }).map(option => option.props('value'))).toEqual(['WOOD', 'PLASTIC', 'METAL', 'PAPER', 'FUMIGATION_FREE']) expect(field(dialog, '状态').findAllComponents({ name: 'ElOption' }).map(option => option.props('value'))).toEqual(['IDLE', 'OCCUPIED', 'SCRAPPED']) await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true) expect((field(dialog, '托盘编码').get('input').element as HTMLInputElement).value).toBe(' PLT-NEW ') await confirm(dialog) expect(api.create).toHaveBeenCalledExactlyOnceWith('9007199254740994', { palletCode: 'PLT-NEW', palletType: 'PLASTIC', fbaWarehouseCode: 'ABE3', capacityCbm: 2.3456, unitPrice: 12.34, status: 'OCCUPIED', locationId: null, remark: '新备注' }) expect(api.update).not.toHaveBeenCalled(); expect(dialog.props('modelValue')).toBe(false) }) it('keeps required fields and positive capacity validation before creating', async () => { const { dialog } = await open(); await confirm(dialog); expect(api.create).not.toHaveBeenCalled() await fill(dialog) field(dialog, '容积(m³)').getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 0); await confirm(dialog) expect(api.create).not.toHaveBeenCalled() field(dialog, '容积(m³)').getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2) field(dialog, '所属 FBA 仓').getComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', ''); await confirm(dialog) expect(api.create).not.toHaveBeenCalled(); expect(dialog.props('modelValue')).toBe(true) }) it('locks duplicate creation and closing, then retains the original fields after failure', async () => { let reject!: (error: Error) => void api.create.mockReturnValueOnce(new Promise((_resolve, fail) => { reject = fail })) const { dialog } = await open(); await fill(dialog) await confirm(dialog); dialog.vm.$emit('confirm'); await flushPromises() expect(api.create).toHaveBeenCalledOnce(); expect(field(dialog, '托盘编码').get('input').attributes('disabled')).toBeDefined() expect(dialog.findComponent({ name: 'ElDialog' }).props('showClose')).toBe(false) expect(dialog.get('.app-dialog__footer .el-button').attributes('disabled')).toBeDefined() reject(new ApiResponseError('托盘保存未确认', 'A0503', 503, null, 'pallet-create-request')); await flushPromises() expect(dialog.text()).toContain('pallet-create-request'); expect(dialog.findComponent(LoadErrorState).props('showRetry')).toBe(false) expect((field(dialog, '托盘编码').get('input').element as HTMLInputElement).value).toBe(' PLT-NEW ') const originalPayload = api.create.mock.calls[0]![1] await confirm(dialog); expect(api.create.mock.calls[1]![1]).toEqual(originalPayload) }) it('shows the edit business code while reading and retries the same warehouse and long id', async () => { let reject!: (error: Error) => void api.detail.mockReturnValueOnce(new Promise((_resolve, fail) => { reject = fail })) const { wrapper, dialog } = await open('编辑') expect(dialog.props('title')).toBe('编辑托盘 · PLT-LAX-001'); expect(dialog.props('confirmDisabled')).toBe(true) expect(dialog.get('[role="status"]').text()).toContain('正在读取'); expect(dialog.findAll('input')).toHaveLength(0) expect(dialog.findComponent({ name: 'ElDialog' }).props('showClose')).toBe(false) wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises(); expect(api.update).not.toHaveBeenCalled() reject(new ApiResponseError('读取失败', 'A0503', 503, null, 'pallet-read-request')); await flushPromises() expect(dialog.text()).toContain('pallet-read-request'); expect(dialog.props('confirmDisabled')).toBe(true) await dialog.findComponent(LoadErrorState).get('.load-error-state__actions button').trigger('click'); await flushPromises() expect(api.detail.mock.calls).toEqual([['9007199254740994', '9007199254740993'], ['9007199254740994', '9007199254740993']]) expect(field(dialog, '仓库').text()).toContain('洛杉矶仓(LAX)'); expect(field(dialog, '仓库').findAll('input')).toHaveLength(0) }) it('keeps version zero, immutable warehouse and editable facts after an update conflict', async () => { api.update.mockRejectedValueOnce(new ApiResponseError('版本冲突', 'A0409', 409, null, 'pallet-conflict-request')) const { dialog } = await open('编辑') await field(dialog, '托盘编码').get('input').setValue(' PLT-EDIT ') await field(dialog, '备注').get('textarea').setValue(' 修改备注 '); await confirm(dialog) expect(api.update).toHaveBeenCalledExactlyOnceWith('9007199254740994', '9007199254740993', { palletCode: 'PLT-EDIT', palletType: 'WOOD', fbaWarehouseCode: 'ABE3', capacityCbm: 2, unitPrice: 10, status: 'IDLE', locationId: null, remark: '修改备注', version: 0 }) expect(dialog.text()).toContain('pallet-conflict-request'); expect(dialog.props('modelValue')).toBe(true) expect((field(dialog, '备注').get('textarea').element as HTMLTextAreaElement).value).toBe(' 修改备注 ') expect(api.detail).toHaveBeenCalledOnce(); expect(api.create).not.toHaveBeenCalled() }) it('shows context failures inside creation and locks submission through the original candidate reload', async () => { api.fba.mockRejectedValueOnce(new ApiResponseError('FBA仓读取失败', 'A0503', 503, null, 'pallet-fba-request')) const { dialog } = await open() expect(dialog.text()).toContain('pallet-fba-request'); expect(dialog.props('confirmDisabled')).toBe(true) dialog.vm.$emit('confirm'); await flushPromises(); expect(api.create).not.toHaveBeenCalled() let resolve!: (value: Array<{ id: string; warehouseName: string; warehouseCode: string }>) => void api.fba.mockReturnValueOnce(new Promise(done => { resolve = done })) await dialog.findComponent(LoadErrorState).get('.load-error-state__actions button').trigger('click'); await flushPromises() expect(dialog.props('confirmLoading')).toBe(true); expect(dialog.findComponent({ name: 'ElDialog' }).props('showClose')).toBe(false) resolve([{ id: 'F1', warehouseName: 'ABE3仓', warehouseCode: 'ABE3' }]); await flushPromises() expect(api.fba).toHaveBeenLastCalledWith({ warehouseType: 'FBA', status: 'ACTIVE' }); expect(api.warehouses).toHaveBeenCalledTimes(2) expect(dialog.findComponent(LoadErrorState).exists()).toBe(false); await fill(dialog); await confirm(dialog); expect(api.create).toHaveBeenCalledOnce() }) it('keeps list-only access read-only and checks current permission again before saving', async () => { can.mockImplementation(code => code === permission.palletList) const wrapper = await page() expect(wrapper.findAll('button').some(button => ['新增托盘', '编辑'].includes(button.text()))).toBe(false) expect(wrapper.findAll('button').some(button => button.text() === '查看')).toBe(true) expect(wrapper.findAll('button').some(button => button.text() === '打印')).toBe(true) can.mockReturnValue(true) const other = await open(); await fill(other.dialog) can.mockReturnValue(false); other.dialog.vm.$emit('confirm'); await flushPromises() expect(api.create).not.toHaveBeenCalled(); expect(api.update).not.toHaveBeenCalled() }) it('resets a closed create form and leaves independent create and edit permissions intact', async () => { can.mockImplementation(code => code !== permission.palletEdit) const { wrapper, dialog } = await open(); await fill(dialog) expect(wrapper.findAll('button').some(button => button.text() === '编辑')).toBe(false) await dialog.get('.app-dialog__footer .el-button').trigger('click'); await flushPromises() await wrapper.findAll('button').find(button => button.text() === '新增托盘')!.trigger('click'); await flushPromises() expect((field(dialog, '托盘编码').get('input').element as HTMLInputElement).value).toBe('') expect(field(dialog, '容积(m³)').getComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(2) expect(api.create).not.toHaveBeenCalled(); expect(api.detail).not.toHaveBeenCalled() }) })