Files
admin-web/src/modules/overseas-warehouse/pages/PalletDialogs.test.ts
T

139 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<VueWrapper, 'findAllComponents'>, 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()
})
})