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(); 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, 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, 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' } }) }) })