124 lines
14 KiB
TypeScript
124 lines
14 KiB
TypeScript
import { flushPromises, mount, enableAutoUnmount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import ElementPlus from 'element-plus'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import WarehouseTransferMovementPanel from './WarehouseTransferMovementPanel.vue'
|
|
import { getWarehouseTransfer, outboundWarehouseTransfer, receiveWarehouseTransfer, listTransferReceivingWarehouses, type WarehouseTransferDetailDto, type WarehouseTransferReceiptCommandDto } from '../api/warehouseTransfers'
|
|
import { getPallet, listPallets } from '../api/pallets'
|
|
import { getOverseasLocation } from '../api/locations'
|
|
const { can } = vi.hoisted(() => ({ can: vi.fn() }))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
|
vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn(), outboundWarehouseTransfer: vi.fn(), receiveWarehouseTransfer: vi.fn(), listTransferReceivingWarehouses: vi.fn() }))
|
|
vi.mock('../api/pallets', () => ({ getPallet: vi.fn(), listPallets: vi.fn() }))
|
|
vi.mock('../api/locations', () => ({ getOverseasLocation: vi.fn() }))
|
|
enableAutoUnmount(afterEach)
|
|
const detail = (status = 'ALLOCATED'): WarehouseTransferDetailDto => ({ id: 'hidden', transferNo: 'WT-1', packageCount: 1, status, needRelabel: false, fbaWarehouseCode: 'ABE4', destinationWarehouseCode: 'ABE4', items: [], cartons: [], pallets: [], relabelBindings: [], receiptCommands: [], allocations: [{ inventoryPositionId: 'hidden-source', inboundOrderId: 'hidden-inbound', inboundCode: 'IN-1', palletId: 'hidden-la', palletNo: 'LA-CODE', skuCode: 'NEW', packageCount: 1, status: status === 'ALLOCATED' ? 'RESERVED' : 'SHIPPED' }] })
|
|
const render = (value = detail()) => mount(WarehouseTransferMovementPanel, { props: { customerId: 'customer', detail: value }, global: { plugins: [ElementPlus] } })
|
|
const button = (wrapper: ReturnType<typeof render>, label: string) => wrapper.findAll('button').find(x => x.text() === label)!
|
|
describe('transfer movement UI', () => {
|
|
beforeEach(() => { vi.clearAllMocks(); sessionStorage.clear(); can.mockReturnValue(true) })
|
|
it('uses the overseas resource warehouse permission and does not require warehouse master access', async () => {
|
|
can.mockImplementation(code => code !== 'platform:warehouse:list')
|
|
const permitted = render(detail('OUTBOUNDED')); expect(permitted.text()).toContain('登记目标仓实收')
|
|
can.mockImplementation(code => code !== 'platform:overseas-warehouse:warehouse:list')
|
|
const denied = render(detail('OUTBOUNDED')); expect(denied.text()).not.toContain('登记目标仓实收'); expect(listTransferReceivingWarehouses).not.toHaveBeenCalled()
|
|
})
|
|
it.each([
|
|
{ cause: new ApiResponseError('目标不可用', 'A0409', 200), recorded: false, canReselect: true },
|
|
{ cause: new Error('response lost'), recorded: false, canReselect: false },
|
|
{ cause: new ApiResponseError('目标不可用', 'A0409', 200), recorded: true, canReselect: false },
|
|
])('allows target correction only after definite rejection plus authoritative no-command readback: $canReselect', async ({ cause, recorded, canReselect }) => {
|
|
const body = { sourceInventoryPositionId: 'hidden-source', destinationOverseasWarehouseId: 'NY', destinationOverseasWarehouseCode: 'NY-CODE', destinationOverseasWarehouseName: '纽约', destinationWarehouseCode: 'ABE4', destinationPalletId: 'P-NY', destinationPalletNo: 'PALLET-NY', warehouseLocationId: 'L-NY', warehouseLocationCode: 'LOC-NY', batchNo: 'WT-1', idempotencyKey: 'original-key' }
|
|
sessionStorage.setItem('warehouse-transfer-movement:customer:WT-1', JSON.stringify({ pending: { type: 'receipt', body } }))
|
|
const fresh = detail('OUTBOUNDED')
|
|
if (recorded) fresh.receiptCommands = [{ sourceInventoryPositionId: 'hidden-source', status: 'PREPARED', requestHash: 'hash', request: body, target: { warehouseId: 'NY', warehouseCode: 'NY-CODE', warehouseName: '纽约', palletId: 'P-NY', palletNo: 'PALLET-NY', locationId: 'L-NY', locationCode: 'LOC-NY', fbaCode: 'ABE4' } }]
|
|
vi.mocked(receiveWarehouseTransfer).mockRejectedValue(cause); vi.mocked(getWarehouseTransfer).mockResolvedValue(fresh)
|
|
const wrapper = render(detail('OUTBOUNDED')); await button(wrapper, '重试原操作').trigger('click'); await flushPromises()
|
|
expect(wrapper.text()).not.toContain('重新选择目标')
|
|
await button(wrapper, '只读核对当前状态').trigger('click'); await flushPromises()
|
|
expect(wrapper.text().includes('重新选择目标')).toBe(canReselect)
|
|
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(1)
|
|
expect(JSON.parse(sessionStorage.getItem('warehouse-transfer-movement:customer:WT-1')!).pending.body).toEqual(body)
|
|
if (canReselect) {
|
|
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [] })
|
|
await button(wrapper, '重新选择目标').trigger('click'); await flushPromises()
|
|
expect(JSON.parse(sessionStorage.getItem('warehouse-transfer-movement:customer:WT-1')!).pending).toBeNull()
|
|
expect(wrapper.text()).toContain('上次目标选择被拒绝:目标不可用')
|
|
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(1)
|
|
}
|
|
})
|
|
it('recovers a reserved original server command in a fresh browser and never queries arbitrary occupied resources', async () => {
|
|
const command: WarehouseTransferReceiptCommandDto = { sourceInventoryPositionId: 'hidden-source', status: 'RESERVED', requestHash: 'server-hash', request: { sourceInventoryPositionId: 'hidden-source', destinationOverseasWarehouseId: 'NY', destinationOverseasWarehouseCode: 'NY-CODE', destinationOverseasWarehouseName: '纽约', destinationWarehouseCode: 'ABE4', destinationPalletId: 'P-NY', destinationPalletNo: 'PALLET-NY', warehouseLocationId: 'L-NY', warehouseLocationCode: 'LOC-NY', batchNo: 'WT-1', idempotencyKey: 'server-original' }, target: { warehouseId: 'NY', warehouseCode: 'NY-CODE', warehouseName: '纽约', palletId: 'P-NY', palletNo: 'PALLET-NY', locationId: 'L-NY', locationCode: 'LOC-NY', fbaCode: 'ABE4' } }
|
|
const d = detail('OUTBOUNDED'); d.receiptCommands = [command]
|
|
const completed = detail('RECEIVED'); completed.allocations![0]!.status = 'RECEIVED'; completed.receiptCommands = [{ ...command, status: 'COMPLETED' }]
|
|
vi.mocked(receiveWarehouseTransfer).mockResolvedValue(completed)
|
|
const wrapper = render(d); await flushPromises()
|
|
expect(wrapper.text()).toContain('从本票服务端记录恢复'); expect(wrapper.text()).not.toContain('登记目标仓实收')
|
|
await button(wrapper, '重试原操作').trigger('click'); await flushPromises()
|
|
expect(receiveWarehouseTransfer).toHaveBeenCalledExactlyOnceWith('customer', 'WT-1', command.request)
|
|
expect(wrapper.emitted('updated')?.[0]?.[0]).toMatchObject({ status: 'RECEIVED' })
|
|
expect(listPallets).not.toHaveBeenCalled(); expect(getPallet).not.toHaveBeenCalled()
|
|
})
|
|
it('does not use a browser remembered occupied target when server receipt history is missing', async () => {
|
|
sessionStorage.setItem('warehouse-transfer-movement:customer:WT-1', JSON.stringify({ pending: null, previous: { warehouse: { id: 'OTHER' }, pallet: { id: 'OTHER', status: 'OCCUPIED' } } }))
|
|
const d = detail('PARTIALLY_RECEIVED'); delete d.receiptCommands
|
|
const wrapper = render(d); expect(wrapper.text()).toContain('尚未返回服务端实收操作记录'); expect(wrapper.text()).not.toContain('登记目标仓实收'); expect(getPallet).not.toHaveBeenCalled()
|
|
})
|
|
it('permits same occupied target from completed server history in a fresh browser', async () => {
|
|
const d = detail('PARTIALLY_RECEIVED')
|
|
const completed: WarehouseTransferReceiptCommandDto = { sourceInventoryPositionId: 'already-received', status: 'COMPLETED', requestHash: 'hash', request: { sourceInventoryPositionId: 'already-received', destinationOverseasWarehouseId: 'NY', destinationOverseasWarehouseCode: 'NY-CODE', destinationOverseasWarehouseName: '纽约', destinationWarehouseCode: 'ABE4', destinationPalletId: 'P-NY', destinationPalletNo: 'PALLET-NY', warehouseLocationId: 'L-NY', warehouseLocationCode: 'LOC-NY', batchNo: 'WT-1', idempotencyKey: 'first-key' }, target: { warehouseId: 'NY', warehouseCode: 'NY-CODE', warehouseName: '纽约', palletId: 'P-NY', palletNo: 'PALLET-NY', locationId: 'L-NY', locationCode: 'LOC-NY', fbaCode: 'ABE4' } }
|
|
d.receiptCommands = [completed]
|
|
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [{ id: 'NY', customerId: '0', warehouseCode: 'NY-CODE', warehouseName: '纽约', warehouseType: 'OVERSEAS', status: 'ACTIVE' }] })
|
|
vi.mocked(getPallet).mockResolvedValue({ id: 'P-NY', customerId: '0', warehouseId: 'NY', locationId: 'L-NY', palletCode: 'PALLET-NY', palletType: 'STANDARD', fbaWarehouseCode: 'ABE4', capacityCbm: 2, unitPrice: 0, status: 'OCCUPIED', version: 1 })
|
|
vi.mocked(getOverseasLocation).mockResolvedValue({ id: 'L-NY', warehouseId: 'NY', locationCode: 'LOC-NY', locationType: 'NORMAL', sharedOverflow: false, status: 'OCCUPIED', version: 1 })
|
|
const wrapper = render(d); await button(wrapper, '登记目标仓实收').trigger('click'); await flushPromises()
|
|
expect(wrapper.text()).toContain('目标:NY-CODE / PALLET-NY / LOC-NY')
|
|
expect(getPallet).toHaveBeenCalledExactlyOnceWith('NY', 'P-NY')
|
|
expect(listPallets).not.toHaveBeenCalled()
|
|
await wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'hidden-source'); await flushPromises()
|
|
expect(button(wrapper, '确认本来源实收').attributes('disabled')).toBeUndefined()
|
|
expect(receiveWarehouseTransfer).not.toHaveBeenCalled()
|
|
})
|
|
it('requires permission and explicit actual-outbound confirmation; unknown outcome survives remount with same body', async () => {
|
|
vi.mocked(outboundWarehouseTransfer).mockRejectedValueOnce(new Error('response lost')).mockResolvedValueOnce(detail('OUTBOUNDED'))
|
|
const first = render(); await button(first, '确认本票出库').trigger('click'); expect(outboundWarehouseTransfer).not.toHaveBeenCalled()
|
|
await button(first, '确认实际出库').trigger('click'); await flushPromises()
|
|
const original = JSON.parse(JSON.stringify(vi.mocked(outboundWarehouseTransfer).mock.calls[0]))
|
|
expect(first.text()).toContain('原操作内容已保留'); first.unmount()
|
|
const second = render(detail('OUTBOUNDED')); await flushPromises()
|
|
expect(second.text()).not.toContain('hidden-source'); expect(second.text()).toContain('重试原操作')
|
|
await button(second, '重试原操作').trigger('click'); await flushPromises()
|
|
expect(vi.mocked(outboundWarehouseTransfer).mock.calls[1]).toEqual(original)
|
|
expect(second.emitted('updated')?.[0]?.[0]).toMatchObject({ status: 'OUTBOUNDED' })
|
|
can.mockReturnValue(false); const denied = render(); expect(denied.text()).not.toContain('确认本票出库')
|
|
})
|
|
it('queries only selected warehouse page, reads exact bound location and keeps receipt request after response loss', async () => {
|
|
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [{ id: 'NY', customerId: '0', warehouseCode: 'NY-CODE', warehouseName: '纽约', warehouseType: 'OVERSEAS', status: 'ACTIVE' }] })
|
|
const p = { id: 'P-NY', customerId: '0', warehouseId: 'NY', locationId: 'L-NY', palletCode: 'PALLET-NY', palletType: 'STANDARD', fbaWarehouseCode: 'ABE4', capacityCbm: 2, unitPrice: 0, status: 'IDLE', version: 0 }
|
|
vi.mocked(listPallets).mockResolvedValue({ records: [p], page: 1, size: 20, total: 1, pages: 1 })
|
|
vi.mocked(getPallet).mockResolvedValue(p)
|
|
vi.mocked(getOverseasLocation).mockResolvedValue({ id: 'L-NY', warehouseId: 'NY', locationCode: 'LOC-NY', locationType: 'NORMAL', sharedOverflow: false, status: 'IDLE', version: 0 })
|
|
vi.mocked(receiveWarehouseTransfer).mockRejectedValueOnce(new Error('response lost')).mockResolvedValueOnce(detail('RECEIVED'))
|
|
const wrapper = render(detail('OUTBOUNDED')); await button(wrapper, '登记目标仓实收').trigger('click'); await flushPromises()
|
|
expect(listPallets).not.toHaveBeenCalled()
|
|
await wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'hidden-source'); await wrapper.findAllComponents({ name: 'ElSelect' })[1]!.vm.$emit('update:modelValue', 'NY')
|
|
await wrapper.get('input[aria-label="目标托盘码查询"]').setValue('PALLET-NY'); await button(wrapper, '查询目标托盘').trigger('click'); await flushPromises()
|
|
expect(listPallets).toHaveBeenCalledExactlyOnceWith('NY', { page: 1, size: 20, keyword: 'PALLET-NY', status: 'IDLE' })
|
|
wrapper.findAllComponents({ name: 'ElSelect' })[2]!.vm.$emit('update:modelValue', 'P-NY'); await flushPromises()
|
|
expect(getOverseasLocation).toHaveBeenCalledExactlyOnceWith('NY', 'L-NY')
|
|
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
|
|
const original = JSON.parse(JSON.stringify(vi.mocked(receiveWarehouseTransfer).mock.calls[0]))
|
|
expect(original[2]).toMatchObject({ sourceInventoryPositionId: 'hidden-source', destinationPalletNo: 'PALLET-NY', warehouseLocationCode: 'LOC-NY' })
|
|
await button(wrapper, '只读核对当前状态').trigger('click'); await flushPromises()
|
|
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(1)
|
|
await button(wrapper, '重试原操作').trigger('click'); await flushPromises()
|
|
expect(vi.mocked(receiveWarehouseTransfer).mock.calls[1]).toEqual(original)
|
|
expect(wrapper.text()).not.toContain('hidden-source')
|
|
})
|
|
it('does not request resources when permission is absent or source already received', async () => {
|
|
can.mockImplementation(code => code !== 'platform:overseas-warehouse:location:list')
|
|
const denied = render(detail('OUTBOUNDED')); expect(denied.text()).not.toContain('登记目标仓实收'); expect(listTransferReceivingWarehouses).not.toHaveBeenCalled()
|
|
can.mockReturnValue(true); const done = detail('RECEIVED'); done.allocations![0]!.status = 'RECEIVED'; const wrapper = render(done); expect(wrapper.text()).not.toContain('登记目标仓实收')
|
|
})
|
|
})
|