Files
admin-web/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts
T

231 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 { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ElMessageBox } from 'element-plus'
import { ApiResponseError } from '@/shared/api/http'
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from '@/modules/unified-orders/api/coLoadGroups'
import UnifiedOrdersView from './UnifiedOrdersView.vue'
vi.mock('vue-router', () => ({ useRoute: () => ({ params: {}, query: {} }), useRouter: () => ({ push: vi.fn() }) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
addPlatformUnifiedOrderNote: vi.fn(), approvePlatformUnifiedOrder: vi.fn(), batchApprovePlatformUnifiedOrders: vi.fn(),
getPlatformUnifiedOrder: vi.fn(), listPlatformUnifiedOrderFeeDetails: vi.fn(), listPlatformUnifiedOrders: vi.fn(),
unifiedOrderStatusOptions: [{ value: 'WAIT_SPLIT', label: '待分柜' }],
}))
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
cancelPlatformCoLoadGroup: vi.fn(), createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(),
}))
const rows = ['5001', '5002'].map((orderId, index) => ({
orderId, tenantId: `600${index}`, orderNo: `ORD-${index + 1}`, mainStatus: 'WAIT_SPLIT' as const, nextAction: null,
relation: null, internalOrderStatus: 'WAIT_SPLIT', customerId: `600${index}`, customerName: `客户${index + 1}`,
channelCode: 'NY_EXX', channelName: '纽约渠道', receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8',
inboundId: `700${index}`, inboundVersion: 1, inboundCode: `IN-${index + 1}`, expectedShipDate: null, totalPackageCount: 5,
locationCodes: null, actualGrossWeightKg: 10, actualVolumeCbm: 1, measurement: null, preload: null, loading: null, delivery: null, transport: null,
createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null, totalPalletCount: null,
chargeableVolumeCbm: null, volumetricWeightKg: null, totalNetWeightKg: null, measurementCompletedAt: null, inboundCompletedAt: null,
expressCompany: null, expressTrackingNo: null, skuNos: null, totalQuantity: null, shipmentId: null, referenceId: null,
coLoadGroup: null,
}))
function mountView() {
return shallowMount(UnifiedOrdersView, {
global: { directives: { loading: () => undefined }, stubs: {
AdminPage: { template: '<main><slot /></main>' },
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
QueryToolbar: { template: '<div />' },
DataTable: { name: 'DataTable', props: ['data'], emits: ['selection-change'], template: '<section><slot name="toolbar-actions" /><button data-testid="select" @click="$emit(\'selection-change\', data)">勾选两票</button><template v-for="row in data" :key="row.orderId"><slot name="actions" :row="row" /><slot name="inboundCode" :row="row" /></template></section>' },
AppDialog: { name: 'AppDialog', props: ['modelValue', 'confirmText', 'confirmDisabled', 'confirmLoading'], emits: ['confirm', 'update:modelValue'], methods: { close() { this.$emit('update:modelValue', false) } }, template: '<section v-if="modelValue"><slot /><slot name="footer" :close="close" :submitting="confirmLoading"><button data-testid="dialog-confirm" :disabled="confirmDisabled" @click="$emit(\'confirm\')">{{ confirmText }}</button></slot></section>' },
ApiErrorMeta: { props: ['details'], template: '<div>{{ details.code }} {{ details.requestId }} {{ details.traceId }}</div>' },
CopyButton: { props: ['value', 'label'], template: '<button :data-copy="value">复制 ID</button>' },
'el-button': { props: ['disabled', 'loading'], emits: ['click'], template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>' },
'el-tag': { template: '<span><slot /></span>' },
'el-alert': { props: ['title'], template: '<div>{{ title }}</div>' },
} },
})
}
describe('UnifiedOrdersView co-load group behavior', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rows, page: 1, size: 20, total: 2, pages: 1 })
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue({ action: 'confirm', value: '' } as never)
})
it('keeps both selected orders and backend tracing evidence when create validation fails', async () => {
vi.mocked(createPlatformCoLoadGroup).mockRejectedValue(new ApiResponseError('成员路线不一致', 'A0409', 409, undefined, 'request-coload', 'trace-coload'))
const wrapper = mountView()
await flushPromises()
await wrapper.get('[data-testid="select"]').trigger('click')
await wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!.trigger('click')
await wrapper.get('[data-testid="dialog-confirm"]').trigger('click')
await flushPromises()
expect(createPlatformCoLoadGroup).toHaveBeenCalledWith(['5001', '5002'])
expect(wrapper.text()).toContain('设为同柜不可拆组(2)')
expect(wrapper.text()).toContain('成员路线不一致')
expect(wrapper.text()).toContain('A0409 request-coload trace-coload')
expect(wrapper.text()).not.toContain('5001')
expect(wrapper.text()).not.toContain('5002')
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
})
it.each([
['平台收货仓不同', [{ ...rows[0], receivingWarehouseId: '8', receivingWarehouseName: '义乌仓' }, { ...rows[1], receivingWarehouseId: '9', receivingWarehouseName: '东莞仓' }], '所选订单的平台收货仓不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:东莞仓;渠道:纽约渠道),不能设为同柜组'],
['渠道不同', [{ ...rows[0], channelCode: 'NY_EXX', channelName: '纽约渠道' }, { ...rows[1], channelCode: 'LA_SEA', channelName: '洛杉矶渠道' }], '所选订单的渠道不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:义乌仓;渠道:洛杉矶渠道),不能设为同柜组'],
['已有同柜组', [rows[0], { ...rows[1], coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-2', 'IN-3'] } }], '所选订单中已有同柜组,请先解除原同柜组'],
])('blocks create immediately when %s', async (_case, selectedRows, reason) => {
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 })
const wrapper = mountView()
await flushPromises()
await wrapper.get('[data-testid="select"]').trigger('click')
const createButton = wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!
expect(createButton.attributes('disabled')).toBeDefined()
expect(wrapper.text()).toContain(reason)
await createButton.trigger('click')
expect(createPlatformCoLoadGroup).not.toHaveBeenCalled()
})
it.each([
['平台收货仓', [{ ...rows[0], receivingWarehouseId: null, receivingWarehouseName: null }, rows[1]], '以下入仓码缺少平台收货仓:IN-1(仓库:未配置仓库;渠道:纽约渠道),请刷新后重试'],
['渠道', [{ ...rows[0], channelCode: null, channelName: null }, rows[1]], '以下入仓码缺少渠道:IN-1(仓库:义乌仓;渠道:未配置渠道),请刷新后重试'],
])('lists the affected inbound code and display names when %s is missing', async (_case, selectedRows, reason) => {
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 })
const wrapper = mountView()
await flushPromises()
await wrapper.get('[data-testid="select"]').trigger('click')
expect(wrapper.text()).toContain(reason)
expect(createPlatformCoLoadGroup).not.toHaveBeenCalled()
})
it('allows different display names for the same channel code', async () => {
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], channelName: '纽约渠道(新名称)' }], page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(createPlatformCoLoadGroup).mockResolvedValue({ groupId: '9001', groupNo: 'CLG-1', status: 'ACTIVE', warehouseId: '8', channelCode: 'NY_EXX', lockedPlanId: null, version: 1, members: [], fullPackageCount: 10, createdAt: '', updatedAt: '' })
const wrapper = mountView()
await flushPromises()
await wrapper.get('[data-testid="select"]').trigger('click')
await wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!.trigger('click')
await wrapper.get('[data-testid="dialog-confirm"]').trigger('click')
await flushPromises()
expect(createPlatformCoLoadGroup).toHaveBeenCalledWith(['5001', '5002'])
})
it('blocks identical display names when channel codes differ', async () => {
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], channelCode: 'LA_SEA', channelName: '纽约渠道' }], page: 1, size: 20, total: 2, pages: 1 })
const wrapper = mountView()
await flushPromises()
await wrapper.get('[data-testid="select"]').trigger('click')
expect(wrapper.text()).toContain('所选订单的渠道不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:义乌仓;渠道:纽约渠道),不能设为同柜组')
expect(createPlatformCoLoadGroup).not.toHaveBeenCalled()
})
it('uses inbound codes as the only row identifier and shows an explicit pending state', async () => {
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], inboundCode: null }], page: 1, size: 20, total: 2, pages: 1 })
const wrapper = mountView()
await flushPromises()
expect(wrapper.text()).toContain('IN-1')
expect(wrapper.text()).toContain('待生成入仓码')
expect(wrapper.text()).not.toContain('ORD-1')
expect(wrapper.text()).not.toContain('ORD-2')
expect(wrapper.text()).not.toContain('5001')
expect(wrapper.text()).not.toContain('7000')
expect(wrapper.text()).not.toContain('履约详情')
expect(wrapper.text()).not.toContain('关联数据')
})
it('shows a business-only group summary and all server-provided inbound codes in the dialog', async () => {
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-1-B', 'IN-2'] } }))
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 3, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-1-B' }, { inboundCode: 'IN-2' }] })
const wrapper = mountView()
await flushPromises()
expect(wrapper.text()).toContain('同柜组 · 2 票 · 未锁定')
expect(wrapper.text()).not.toContain('CLG-9001')
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
await flushPromises()
expect(getPlatformCoLoadGroupByInboundCode).toHaveBeenCalledWith('IN-1')
expect(wrapper.text()).toContain('3 个成员')
expect(wrapper.text()).toContain('IN-1')
expect(wrapper.text()).toContain('IN-1-B')
expect(wrapper.text()).toContain('IN-2')
expect(wrapper.text()).not.toContain('ORD-1')
expect(wrapper.text()).not.toContain('ORD-2')
expect(wrapper.text()).not.toContain('7003')
expect(wrapper.text()).not.toContain('5001')
})
it('cancels an ACTIVE group by the clicked member inbound code and preserves dialog error evidence', async () => {
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } }))
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] })
vi.mocked(cancelPlatformCoLoadGroup).mockRejectedValueOnce(new ApiResponseError('组已被并发锁定', 'A0409', 409, undefined, 'request-cancel', 'trace-cancel'))
const wrapper = mountView()
await flushPromises()
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
await flushPromises()
const cancelButton = wrapper.findAll('button').find((button) => button.text() === '解除同柜组')!
expect(cancelButton.exists()).toBe(true)
await cancelButton.trigger('click')
await flushPromises()
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('解除整个同柜组'), '解除同柜组', expect.objectContaining({ confirmButtonText: '确认解除' }))
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('IN-1', '重新调整同柜组成员')
expect(wrapper.text()).toContain('组已被并发锁定')
expect(wrapper.text()).toContain('A0409 request-cancel trace-cancel')
expect(wrapper.findAllComponents({ name: 'AppDialog' }).some((dialog) => dialog.props('modelValue') === true)).toBe(true)
expect(wrapper.text()).not.toContain('CLG-')
expect(wrapper.text()).not.toContain('5001')
})
it('closes and refreshes after one successful whole-group cancellation request', async () => {
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } }))
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] })
vi.mocked(cancelPlatformCoLoadGroup).mockResolvedValue({} as never)
const wrapper = mountView()
await flushPromises()
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
await flushPromises()
await wrapper.findAll('button').find((button) => button.text() === '解除同柜组')!.trigger('click')
await flushPromises()
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledTimes(1)
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('IN-1', '重新调整同柜组成员')
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
expect(wrapper.findAllComponents({ name: 'AppDialog' }).every((dialog) => dialog.props('modelValue') !== true)).toBe(true)
})
it.each(['LOCKED', 'CONSUMED'])('does not offer group cancellation for %s', async (status) => {
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status, memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } }))
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: status as 'LOCKED' | 'CONSUMED', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] })
const wrapper = mountView()
await flushPromises()
await wrapper.get(`[aria-label="查看同柜组 · 2 票 · ${status === 'LOCKED' ? '已锁定' : '已消费'}成员"]`).trigger('click')
await flushPromises()
expect(wrapper.findAll('button').some((button) => button.text() === '解除同柜组')).toBe(false)
expect(cancelPlatformCoLoadGroup).not.toHaveBeenCalled()
})
it.each([
['undefined', undefined],
['null', null],
['blank', ' '],
])('keeps the member dialog readable when a normalized inbound code is %s', async (_case, inboundCode) => {
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } }))
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 2, members: [{ inboundCode: inboundCode as unknown as string }, { inboundCode: 'IN-2' }] })
const wrapper = mountView()
await flushPromises()
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
await flushPromises()
expect(wrapper.text()).toContain('同柜组成员入仓码尚未返回,请刷新后重试')
expect(wrapper.text()).not.toContain('CLG-9001')
expect(wrapper.text()).not.toContain('ORD-1')
expect(wrapper.text()).not.toContain('5001')
})
})