Files
admin-web/src/modules/overseas-warehouse/pages/RelabelOrdersView.test.ts
T
2026-10-16 17:43:36 +08:00

231 lines
15 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,
listRelabelWarehouseOptions,
} from '../api/relabelOrders'
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(),
listRelabelWarehouseOptions: vi.fn(),
}))
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('shows every SKU and quantity in the review dialog while keeping one box', async () => {
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()
expect(formItem(wrapper, '商品SKU').findComponent({ name: 'ElInput' }).props('modelValue')).toBe('SKU-A、SKU-B')
expect(formItem(wrapper, '新SKU').findComponent({ name: 'ElInput' }).props('modelValue')).toBe('NEW-A × 10 件;NEW-B × 5 件')
expect(formItem(wrapper, '箱数').findComponent({ name: 'ElInputNumber' }).props('modelValue')).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.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, '请选择 ACTIVE 客户')
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, '请选择 ACTIVE 客户')
customer.vm.$emit('update:modelValue', '10001'); customer.vm.$emit('change', '10001')
await flushPromises()
const warehouse = select(wrapper, '请选择 ACTIVE+RECEIVING 海外仓')
warehouse.vm.$emit('update:modelValue', 'WH-1')
formItem(wrapper, '关联订单').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', 'CO-1')
formItem(wrapper, '商品SKU').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', 'SKU-1')
formItem(wrapper, '中文品名').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', '商品一')
formItem(wrapper, '箱数').findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2)
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: 'ElSelect' }).vm.$emit('update:modelValue', '换SKU标')
formItem(wrapper, '货件号').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', 'SHIP-1')
formItem(wrapper, '新SKU').findComponent({ name: 'ElInput' }).vm.$emit('update:modelValue', 'SKU-2')
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,
items: [expect.objectContaining({ skuCode: 'SKU-1', newSkuCode: 'SKU-2', labelRequirement: '换SKU标' })],
}))
})
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()
expect(formItem(wrapper, '商品SKU').findComponent({ name: 'ElInput' }).props('modelValue')).toBe('SKU-NEW')
expect(formItem(wrapper, '中文品名').findComponent({ name: 'ElInput' }).props('modelValue')).toBe('中文商品')
expect(formItem(wrapper, '货件号').findComponent({ name: 'ElInput' }).props('modelValue')).toBe('SHIP-9')
expect(formItem(wrapper, '新SKU').findComponent({ name: 'ElInput' }).props('modelValue')).toBe('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' } })
})
})