139 lines
12 KiB
TypeScript
139 lines
12 KiB
TypeScript
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()
|
||
})
|
||
})
|