Files
admin-web/src/modules/overseas-warehouse/pages/RelabelOrdersView.test.ts
T
2026-10-20 17:24:50 +08:00

251 lines
17 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, { ElMessageBox } from 'element-plus'
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { listPlatformTenants } from '@/modules/iam'
import AppDialog from '@/shared/components/AppDialog.vue'
import {
changeRelabelOrderStatus,
createRelabelOrder,
getRelabelOrder,
listRelabelOrders,
getRelabelQuantities,
listRelabelWarehouseOptions,
} from '../api/relabelOrders'
import RelabelInventoryItemsEditor from '../components/RelabelInventoryItemsEditor.vue'
import RelabelOrdersView from './RelabelOrdersView.vue'
const routerPush = vi.hoisted(() => vi.fn())
vi.mock('vue-router', () => ({ useRouter: () => ({ push: routerPush }) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
vi.mock('../api/relabelOrders', () => ({
changeRelabelOrderStatus: vi.fn(),
createRelabelOrder: vi.fn(),
getRelabelOrder: vi.fn(),
listRelabelOrders: vi.fn(),
getRelabelQuantities: vi.fn(),
listRelabelWarehouseOptions: vi.fn(),
listRelabelInventoryCandidates: vi.fn().mockResolvedValue([]),
}))
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
function memoryStorage(): Storage { const values = new Map<string, string>(); 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) } }
function formItem(wrapper: VueWrapper<any>, label: string) {
const item = wrapper.findAllComponents({ name: 'ElFormItem' }).find((candidate) => candidate.props('label') === label)
if (!item) throw new Error(`Missing form item: ${label}`)
return item
}
function select(wrapper: VueWrapper<any>, placeholder: string) {
const target = wrapper.findAllComponents({ name: 'ElSelect' }).find((candidate) => candidate.props('placeholder') === placeholder)
if (!target) throw new Error(`Missing select: ${placeholder}`)
return target
}
enableAutoUnmount(afterEach)
afterAll(() => vi.unstubAllGlobals())
describe('RelabelOrdersView', () => {
it('hides filter totals and preserves row quantities and unknown boxes', async () => {
vi.mocked(listRelabelOrders).mockResolvedValue({ records: [{ id: '1', relabelNo: 'RL-PIECE', skuCodes: 'A', totalPackageCount: 100, status: 'PROCESSING' }], page: 1, size: 10, total: 45, pages: 5 })
vi.mocked(getRelabelQuantities).mockResolvedValue({ orderCount: 45, skuCount: 12, knownLabelPieceCount: 300, unknownPieceOrders: 1, knownCartonCount: 8, unknownCartonOrders: 2, orders: { 'RL-PIECE': { labelPieceCount: 100, cartonCount: null, skuCount: 1, duplicateCarton: false, quantityUnit: 'PIECE', sourcePalletCount: null, targetPalletCount: null } } })
const wrapper = mount(RelabelOrdersView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.find('[aria-label="全部筛选结果数量汇总"]').exists()).toBe(false)
const cells = wrapper.findAll('tbody tr').find(row => row.text().includes('RL-PIECE'))!.findAll('td')
expect(cells[5]!.text()).toBe('—')
expect(cells[5]!.text()).toBe(cells[8]!.text())
expect(cells[6]!.text()).toBe('100')
})
it('shows every SKU and quantity in the review dialog while keeping one box', async () => {
vi.mocked(getRelabelQuantities).mockResolvedValue({ orderCount: 1, skuCount: 2, knownLabelPieceCount: 15, unknownPieceOrders: 0, knownCartonCount: 1, unknownCartonOrders: 0, orders: { 'RL-MULTI': { labelPieceCount: 15, cartonCount: 1, skuCount: 2, duplicateCarton: false, quantityUnit: 'CARTON', sourcePalletCount: null, targetPalletCount: null } } })
vi.mocked(listRelabelOrders).mockResolvedValueOnce({ records: [{ id: '1', relabelNo: 'RL-MULTI', skuCodes: 'SKU-A,SKU-B', totalPackageCount: 1, status: 'PENDING_REVIEW' }], page: 1, size: 10, total: 1, pages: 1 })
vi.mocked(getRelabelOrder).mockResolvedValueOnce({ application: { id: '1', relabelNo: 'RL-MULTI', totalPackageCount: 1 }, items: [{ id: '1', skuCode: 'SKU-A', newSkuCode: 'NEW-A', skuItems: [{ skuCode: 'SKU-A', newSkuCode: 'NEW-A', quantity: 10 }, { skuCode: 'SKU-B', newSkuCode: 'NEW-B', quantity: 5 }] }], history: [] })
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('SKU-A,SKU-B')
await wrapper.findAll('button').find(button => button.text().trim() === '审核')!.trigger('click')
await flushPromises()
const facts = wrapper.get('[aria-label="换标审核原始资料"]')
expect(facts.findAll('input, textarea')).toHaveLength(0)
expect(facts.text()).toContain('SKU-A、SKU-B')
expect(facts.text()).toContain('NEW-A × 10 件;NEW-B × 5 件')
expect(facts.findAll('tr').find(row => row.text().startsWith('箱数'))!.get('td').text()).toBe('1')
})
it('renders product names and labeling requirements supplied by the list projection', async () => {
vi.mocked(listRelabelOrders).mockResolvedValueOnce({ records: [{ id: '1', relabelNo: 'RL-1', productNameZh: '商品甲、商品乙', labelRequirement: '贴标要求111', status: 'PENDING_PROCESSING' }], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('商品甲、商品乙')
expect(wrapper.text()).toContain('贴标要求111')
})
beforeEach(() => {
vi.mocked(getRelabelQuantities).mockReset().mockResolvedValue({ orderCount: 0, skuCount: 0, knownLabelPieceCount: 0, unknownPieceOrders: 0, knownCartonCount: 0, unknownCartonOrders: 0, orders: {} })
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('localStorage', memoryStorage())
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' } as never], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listRelabelOrders).mockReset().mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
vi.mocked(listRelabelWarehouseOptions).mockReset()
vi.mocked(createRelabelOrder).mockReset()
vi.mocked(changeRelabelOrderStatus).mockReset()
vi.mocked(getRelabelOrder).mockReset().mockResolvedValue({ application: { relabelNo: 'RL-1' }, items: [], history: [] } as never)
})
it('loads shared receiving warehouse options independently from the selected customer', async () => {
vi.mocked(listRelabelWarehouseOptions).mockResolvedValue({ records: [{ id: 'WH-B', customerId: '0', warehouseCode: 'B', warehouseName: 'B仓', warehouseType: 'OVERSEAS', status: 'ACTIVE' }], page: 1, size: 200, total: 1, pages: 1 })
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('新增换标单'))!.trigger('click')
await flushPromises()
const customer = select(wrapper, '请选择客户')
customer.vm.$emit('update:modelValue', '10001'); customer.vm.$emit('change', '10001')
await flushPromises()
const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => option.props('label'))
expect(listRelabelWarehouseOptions).toHaveBeenCalledOnce()
expect(labels).toContain('B仓 · B')
})
it('reuses one idempotency key after a failed create retry', async () => {
vi.mocked(listRelabelWarehouseOptions).mockResolvedValue({ records: [{ id: 'WH-1', customerId: '10001', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', warehouseType: 'OVERSEAS', status: 'ACTIVE' }], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(createRelabelOrder).mockRejectedValueOnce(new Error('temporary')).mockResolvedValueOnce({ application: { id: '1', relabelNo: 'RL-1' }, items: [], history: [] })
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('新增换标单'))!.trigger('click')
await flushPromises()
const customer = select(wrapper, '请选择客户')
customer.vm.$emit('update:modelValue', '10001'); customer.vm.$emit('change', '10001')
await flushPromises()
const warehouse = select(wrapper, '请选择公共海外收货仓')
warehouse.vm.$emit('update:modelValue', 'WH-1')
formItem(wrapper, '关联订单').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', 'CO-1')
formItem(wrapper, '换标费用(CNY)').findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 20)
formItem(wrapper, '截止时间').findComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-08-30T10:00:00')
formItem(wrapper, '联系人').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', '张三')
formItem(wrapper, '联系电话').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', '13800000000')
formItem(wrapper, '贴标要求').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', '换SKU标')
formItem(wrapper, '货件号').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', 'SHIP-1')
await flushPromises()
formItem(wrapper, '换标类型').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'SKU_LABEL')
await flushPromises()
wrapper.findComponent(RelabelInventoryItemsEditor).vm.$emit('update:items', [{ sourceInventoryBalanceId: '9007199254740999', sourceInventoryPositionId: '9007199254740998', sourceInboundOrderId: '9007199254740997', skuCode: 'SKU-1', productNameZh: '商品一', cartonMark: 'BOX-1', operationType: 'SKU_LABEL', totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: 'SHIP-1', newSkuCode: 'SKU-2', labelRequirement: '换SKU标', skuItems: [{ skuCode: 'SKU-1', newSkuCode: 'SKU-2', quantity: 8 }] }])
await flushPromises()
const dialog = wrapper.findAllComponents(AppDialog).find(candidate => candidate.props('title') === '新增换标单')!
dialog.vm.$emit('confirm')
await flushPromises()
expect(dialog.props('modelValue')).toBe(true)
dialog.vm.$emit('confirm')
await flushPromises()
expect(createRelabelOrder).toHaveBeenCalledTimes(2)
const firstKey = vi.mocked(createRelabelOrder).mock.calls[0]![1].idempotencyKey
const retryKey = vi.mocked(createRelabelOrder).mock.calls[1]![1].idempotencyKey
expect(firstKey).toBeTruthy()
expect(retryKey).toBe(firstKey)
expect(createRelabelOrder).toHaveBeenLastCalledWith('10001', expect.objectContaining({
customerOrderNo: 'CO-1', operationWarehouseId: 'WH-1', chargeAmount: 20, contactName: '张三', contactPhone: '13800000000', labelDeadline: '2026-08-30T10:00:00', remark: undefined, idempotencyKey: firstKey,
deliveryMethod: undefined, items: [expect.objectContaining({ sourceInventoryPositionId: '9007199254740998', cartonMark: 'BOX-1', totalPackageCount: 1, notifiedPackageCount: 1, skuItems: [{ skuCode: 'SKU-1', newSkuCode: 'SKU-2', quantity: 8 }] })],
}))
})
it('offers void for unfinished states only and submits the confirmed real status command', async () => {
const statuses = ['PENDING_REVIEW', 'PENDING_PROCESSING', 'PROCESSING', 'CUSTOMER_CONFIRM_PENDING', 'REJECTED', 'READY_FOR_OUTBOUND', 'COMPLETED', 'CANCELLED']
vi.mocked(listRelabelOrders).mockResolvedValueOnce({ records: statuses.map(status => ({ id: status, relabelNo: 'RL-' + status, status })), page: 1, size: 10, total: 8, pages: 1 })
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
const buttons = wrapper.findAll('tbody button').filter(button => button.text().trim() === '作废')
expect(buttons).toHaveLength(5)
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValueOnce('confirm' as never)
await buttons[0]!.trigger('click'); await flushPromises()
expect(changeRelabelOrderStatus).toHaveBeenCalledExactlyOnceWith('RL-PENDING_REVIEW', 'CANCELLED', '平台作废换标单')
confirm.mockRestore()
})
it('keeps the order unchanged when void confirmation is cancelled', async () => {
vi.mocked(listRelabelOrders).mockResolvedValueOnce({ records: [{ id: '1', relabelNo: 'RL-1', status: 'PROCESSING' }], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('cancel')
await wrapper.findAll('tbody button').find(button => button.text().trim() === '作废')!.trigger('click'); await flushPromises()
expect(changeRelabelOrderStatus).not.toHaveBeenCalled()
confirm.mockRestore()
})
it('hides the audit action after approval', async () => {
vi.mocked(listRelabelOrders).mockResolvedValueOnce({
records: [{ id: '1', relabelNo: 'RL-1', customerOrderNo: 'CO-1', skuCodes: 'SKU-1', totalPackageCount: 2, status: 'PENDING_PROCESSING' }],
page: 1, size: 10, total: 1, pages: 1,
})
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('button').some((button) => button.text().trim() === '审核')).toBe(false)
})
it('shows the audit action only while pending review', async () => {
vi.mocked(listRelabelOrders).mockResolvedValueOnce({
records: [{ id: '1', relabelNo: 'RL-1', customerOrderNo: 'CO-1', skuCodes: 'SKU-1', totalPackageCount: 2, status: 'PENDING_REVIEW' }],
page: 1, size: 10, total: 1, pages: 1,
})
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().trim() === '审核')!.trigger('click')
await flushPromises()
const labels = formItem(wrapper, '审核动作').findAllComponents({ name: 'ElOption' }).map((option) => option.props('label'))
expect(labels).toEqual(['审核通过', '已驳回'])
})
it('loads the full detail before opening audit so item fields are echoed', async () => {
vi.mocked(listRelabelOrders).mockResolvedValueOnce({
records: [{ id: '1', relabelNo: 'RL-1', customerOrderNo: 'CO-1', skuCodes: 'SKU-OLD', totalPackageCount: 2, chargeAmount: 0.01, status: 'PENDING_REVIEW' }],
page: 1, size: 10, total: 1, pages: 1,
})
vi.mocked(getRelabelOrder).mockResolvedValueOnce({
application: { relabelNo: 'RL-1', customerOrderNo: 'CO-1', chargeAmount: 12.5 },
remark: '请按新标签处理',
items: [{ skuCode: 'SKU-NEW', productNameZh: '中文商品', totalPackageCount: 3, shipmentNo: 'SHIP-9', newSkuCode: 'NEW-SKU-9', labelRequirement: '换箱唛和SKU标' }],
history: [],
} as never)
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().trim() === '审核')!.trigger('click')
await flushPromises()
const facts = wrapper.get('[aria-label="换标审核原始资料"]')
expect(facts.text()).toContain('SKU-NEW')
expect(facts.text()).toContain('中文商品')
expect(facts.text()).toContain('SHIP-9')
expect(facts.text()).toContain('NEW-SKU-9')
})
it('passes the relabel order number search to the backend', async () => {
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
const input = wrapper.find('input[placeholder="请输入换标单号"]')
expect(input.exists()).toBe(true)
await input.setValue('FBR202608272092853218068373506')
await wrapper.findAll('button').find((button) => button.text().includes('查询'))!.trigger('click')
await flushPromises()
expect(listRelabelOrders).toHaveBeenLastCalledWith(expect.objectContaining({
relabelNo: 'FBR202608272092853218068373506',
}))
})
it('opens the inventory source selector instead of treating relabel package counts as SKU quantities', async () => {
vi.mocked(listRelabelOrders).mockResolvedValueOnce({
records: [{ id: '1', relabelNo: 'RL-1', customerOrderNo: 'RETURN-1', skuCodes: 'SKU-TEST', totalPackageCount: 2, status: 'PENDING_PROCESSING' }],
page: 1, size: 10, total: 1, pages: 1,
})
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
const table = wrapper.findComponent({ name: 'ElTable' })
table.vm.$emit('selection-change', table.props('data'))
await flushPromises()
const createPlan = wrapper.get('.data-table__business-actions').findAll('button').find(button => button.text().includes('创建合箱计划(1)'))!
await createPlan.trigger('click')
await flushPromises()
expect(routerPush).toHaveBeenCalledWith({ name: 'overseas-return-inventory', query: { packing: '1' } })
})
})