2026-08-24 15:41:36 +08:00
|
|
|
|
import ElementPlus 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,
|
|
|
|
|
|
listRelabelOrders,
|
|
|
|
|
|
listRelabelWarehouseOptions,
|
|
|
|
|
|
} from '../api/relabelOrders'
|
|
|
|
|
|
import RelabelOrdersView from './RelabelOrdersView.vue'
|
|
|
|
|
|
|
2026-10-09 18:03:34 +08:00
|
|
|
|
const routerPush = vi.hoisted(() => vi.fn())
|
|
|
|
|
|
vi.mock('vue-router', () => ({ useRouter: () => ({ push: routerPush }) }))
|
2026-08-24 15:41:36 +08:00
|
|
|
|
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(),
|
|
|
|
|
|
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', () => {
|
|
|
|
|
|
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()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-26 20:39:55 +08:00
|
|
|
|
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 })
|
2026-08-24 15:41:36 +08:00
|
|
|
|
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'))
|
2026-08-26 20:39:55 +08:00
|
|
|
|
expect(listRelabelWarehouseOptions).toHaveBeenCalledOnce()
|
2026-08-24 15:41:36 +08:00
|
|
|
|
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()
|
2026-08-26 20:39:55 +08:00
|
|
|
|
const warehouse = select(wrapper, '请选择 ACTIVE+RECEIVING 海外仓')
|
2026-08-24 15:41:36 +08:00
|
|
|
|
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)
|
2026-09-05 22:26:37 +08:00
|
|
|
|
formItem(wrapper, '换标费用(CNY)').findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 20)
|
2026-08-24 15:41:36 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-09-30 14:27:41 +08:00
|
|
|
|
const dialog = wrapper.findAllComponents(AppDialog).find(candidate => candidate.props('title') === '新增换标单')!
|
2026-08-24 15:41:36 +08:00
|
|
|
|
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({
|
2026-09-28 15:24:33 +08:00
|
|
|
|
customerOrderNo: 'CO-1', operationWarehouseId: 'WH-1', chargeAmount: 20, contactName: '张三', contactPhone: '13800000000', labelDeadline: '2026-08-30T10:00:00', remark: undefined, idempotencyKey: firstKey,
|
2026-08-24 15:41:36 +08:00
|
|
|
|
items: [expect.objectContaining({ skuCode: 'SKU-1', newSkuCode: 'SKU-2', labelRequirement: '换SKU标' })],
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-20 15:23:26 +08:00
|
|
|
|
it('hides the audit action after approval', async () => {
|
2026-08-24 15:41:36 +08:00
|
|
|
|
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()
|
2026-09-20 15:23:26 +08:00
|
|
|
|
expect(wrapper.findAll('button').some((button) => button.text().trim() === '审核')).toBe(false)
|
|
|
|
|
|
})
|
2026-08-24 15:41:36 +08:00
|
|
|
|
|
2026-09-20 15:23:26 +08:00
|
|
|
|
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(['审核通过', '已驳回'])
|
2026-08-24 15:41:36 +08:00
|
|
|
|
})
|
2026-08-28 14:33:57 +08:00
|
|
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
2026-09-30 14:27:41 +08:00
|
|
|
|
|
2026-10-09 18:03:34 +08:00
|
|
|
|
it('opens the inventory source selector instead of treating relabel package counts as SKU quantities', async () => {
|
2026-09-30 14:27:41 +08:00
|
|
|
|
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()
|
2026-10-09 18:03:34 +08:00
|
|
|
|
expect(routerPush).toHaveBeenCalledWith({ name: 'overseas-return-inventory', query: { packing: '1' } })
|
2026-09-30 14:27:41 +08:00
|
|
|
|
})
|
2026-08-24 15:41:36 +08:00
|
|
|
|
})
|