Files
admin-web/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts
T

1450 lines
88 KiB
TypeScript
Raw Normal View History

2026-10-08 17:08:03 +08:00
const { submitExport } = vi.hoisted(() => ({ submitExport: vi.fn().mockResolvedValue(undefined) }))
vi.mock('@/modules/downloads', () => ({ useExportTask: () => submitExport }))
import ElementPlus, { ElMessageBox, ElMessage } from 'element-plus'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { createPinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2026-08-27 17:16:38 +08:00
import { ApiResponseError } from '@/shared/api/http'
2026-08-27 17:49:05 +08:00
import DataTable from '@/shared/components/DataTable.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
2026-08-28 13:54:44 +08:00
import { listAllPlatformWarehouses } from '@/modules/warehouses'
import { listContainerArrivalTimes } from '@/modules/container-loading'
2026-08-27 17:49:05 +08:00
import {
2026-09-07 15:07:39 +08:00
changeContainerReceivingWarehouse,
createManualInboundPlan,
2026-08-27 17:49:05 +08:00
convertLastMileContainerToInboundPlans,
2026-09-09 00:44:07 +08:00
getContainerInboundPlanPreview,
2026-08-28 13:54:44 +08:00
getContainerPlanProgress,
listContainerPlanProgress,
2026-08-27 17:49:05 +08:00
listLastMileOrders,
2026-10-20 14:00:49 +08:00
getReturnOrderStatistics,
reviewCargoDiscard,
2026-08-28 13:54:44 +08:00
transferLastMileOrders,
2026-10-10 14:58:15 +08:00
updateLastMileOrderPalletMerge,
getLastMileOrderManualNodes,
updateLastMileOrderHold,
updateLastMileOrderManualNodes,
2026-08-27 17:49:05 +08:00
} from '../api/lastMileOrders'
import LastMileOrdersView from './LastMileOrdersView.vue'
const canAccess = vi.hoisted(() => vi.fn<(permissionCode: string) => boolean>(() => true))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canAccess }) }))
vi.mock('@/shared/downloadCsv', () => ({ saveCsv: vi.fn() }))
2026-10-09 18:03:34 +08:00
const routerPush = vi.hoisted(() => vi.fn())
vi.mock('vue-router', () => ({ useRouter: () => ({ push: routerPush }) }))
2026-08-28 13:54:44 +08:00
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
vi.mock('@/modules/container-loading', async () => ({
...await import('../../container-loading/warehouseArrival'),
listContainerArrivalTimes: vi.fn(),
}))
vi.mock('../api/lastMileOrders', () => ({
2026-10-20 14:00:49 +08:00
getReturnOrderStatistics: vi.fn(),
2026-08-28 13:54:44 +08:00
listLastMileOrders: vi.fn(), listContainerPlanProgress: vi.fn(), getContainerPlanProgress: vi.fn(),
2026-09-07 15:07:39 +08:00
changeContainerReceivingWarehouse: vi.fn(),
2026-09-09 00:44:07 +08:00
createManualInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), getContainerInboundPlanPreview: vi.fn(), transferLastMileOrders: vi.fn(),
getLastMileOrderManualNodes: vi.fn(), updateLastMileOrderHold: vi.fn(), updateLastMileOrderManualNodes: vi.fn(),
2026-10-10 14:58:15 +08:00
reviewCargoDiscard: vi.fn(), updateLastMileOrderPalletMerge: vi.fn(),
}))
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
function memoryStorage(): Storage {
const values = new Map<string, string>()
return {
2026-08-27 16:42:32 +08:00
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),
}
}
2026-08-27 17:49:05 +08:00
const bookingId = '2092873463568597001'
const arrivalTime = {
sourceContainerBookingId: bookingId, estimatedWarehouseArrivalAt: '2026-09-01T08:00:00',
actualWarehouseArrivalAt: null, version: 1, planCount: 0, plannedDate: '2026-09-01', isDefault: false,
}
2026-10-20 14:00:49 +08:00
const emptyReturnStatistics = { totals: { orderCount: 0, skuTypes: 0, expectedQuantity: 0, receivedQuantity: 0, remainingQuantity: 0, receivedBoxCount: 0, receivedParcelCount: 0, receivedPalletCount: 0 }, pageDimensions: [] }
beforeEach(() => {
vi.mocked(getReturnOrderStatistics).mockReset().mockResolvedValue(emptyReturnStatistics)
})
2026-08-27 17:49:05 +08:00
const customerId = '2092618717861048321'
2026-08-27 17:16:38 +08:00
const summary = {
2026-08-28 13:54:44 +08:00
sourceContainerBookingId: bookingId, containerNo: 'CONT-15', totalOrderCount: 15,
2026-09-07 15:07:39 +08:00
overseasWarehouseId: '9007199254740995', overseasWarehouseCode: 'LAX01', overseasWarehouseName: '洛杉矶接柜仓',
assignmentVersion: 7, canChangeReceivingWarehouse: true, receivingWarehouseLockReason: null,
2026-08-28 13:54:44 +08:00
inboundCodeCount: 15, missingInboundCodeCount: 0, planCount: 10, plannedInboundCodeCount: 10,
2026-09-10 16:33:43 +08:00
pendingPlanCount: 5, resourcePendingPlanCount: 0, plannedArrivalAt: '2026-09-01T08:00:00', allPlansReady: false,
2026-08-28 13:54:44 +08:00
blockingReason: '存在5票尚未生成有效入库计划',
}
const fbaWarehouse = {
id: '9007199254740993', tenantId: '0', warehouseCode: 'ONT8', warehouseName: 'Ontario FBA',
warehouseType: 'FBA', countryCode: 'US', timezone: 'America/Los_Angeles', contactName: null,
contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null,
primaryAddress: {
id: '9007199254740994', tenantId: '0', warehouseId: '9007199254740993', addressType: 'OPERATING',
countryCode: 'US', stateProvince: 'CA', city: 'Ontario', district: null, postalCode: '91761',
addressLine1: 'FBA address', addressLine2: null, latitude: null, longitude: null,
},
addressSummary: 'CA Ontario',
2026-08-27 17:16:38 +08:00
}
2026-09-09 00:44:07 +08:00
const preview = {
sourceContainerBookingId: bookingId, containerNo: 'CONT-15',
overseasWarehouseId: summary.overseasWarehouseId, overseasWarehouseCode: summary.overseasWarehouseCode,
overseasWarehouseName: summary.overseasWarehouseName, snapshotVersion: 9, snapshotHash: 'a'.repeat(64),
totalOrderCount: 5, readyOrderCount: 5, blockedOrderCount: 0, pendingOrderCount: 5,
pendingPalletCount: 5, sourcePalletCount: 4, supplementPalletCount: 1,
existingPlannedPalletCount: 0, totalSuggestedPalletCount: 5,
2026-09-09 00:44:07 +08:00
totalPlannedPalletCount: 0, ready: true,
groups: [{
destinationType: 'FBA', destinationWarehouseId: null, destinationWarehouseCode: 'ONT8',
destinationWarehouseName: 'Ontario FBA', orderCount: 5, pendingOrderCount: 5, pendingPalletCount: 5,
sourcePalletCount: 4, supplementPalletCount: 1,
2026-09-09 00:44:07 +08:00
existingPlannedPalletCount: 0, suggestedPalletCount: 5, plannedPalletCount: 0,
orders: [{
customerId, customerName: '测试客户', orderId: '2092873463568596994', businessOrderNo: 'LMO-1',
inboundCode: 'IN-1', orderType: 'FIRST_LEG', destinationType: 'FBA', destinationWarehouseId: null,
destinationWarehouseCode: 'ONT8', destinationWarehouseName: 'Ontario FBA', packageCount: 10,
volumeCbm: 1, suggestedPalletCount: 5, sourcePalletCount: 4, targetPalletCount: 5,
supplementPalletCount: 1, plannedPalletCount: null, existingPlanNo: null,
2026-09-09 00:44:07 +08:00
planStatus: null, orderVersion: 3, readiness: 'READY', blockers: [],
}],
}],
}
// Recorded LOCAL_COMMITTED shape: complete local plan/pallet, zero new work, PLANNED,
// and an explicit server resourceRetry holding the original command identity.
const resourceCommittedPreview = {
...preview, snapshotVersion: 10, snapshotHash: 'b'.repeat(64),
totalOrderCount: 1, readyOrderCount: 1, pendingOrderCount: 0, pendingPalletCount: 0,
sourcePalletCount: 0, supplementPalletCount: 0, existingPlannedPalletCount: 1,
totalSuggestedPalletCount: 1, totalPlannedPalletCount: 1,
groups: [{
...preview.groups[0], destinationType: 'FBA' as const,
orderCount: 1, pendingOrderCount: 0, pendingPalletCount: 0, sourcePalletCount: 0,
supplementPalletCount: 0, existingPlannedPalletCount: 1, suggestedPalletCount: 1, plannedPalletCount: 1,
orders: [{ ...preview.groups[0].orders[0], destinationType: 'FBA' as const, fbaWarehouseCode: 'ONT8',
packageCount: 1, suggestedPalletCount: 1, sourcePalletCount: 1, targetPalletCount: 1,
supplementPalletCount: 0, plannedPalletCount: 1, existingPlanNo: 'LIP-ORIGINAL',
planStatus: 'PENDING_ASSIGNMENT', readiness: 'PLANNED' as const, blockers: [],
}],
}],
resourceRetry: { idempotencyKey: 'original-container-command', snapshotVersion: 9, snapshotHash: 'a'.repeat(64) },
}
const originalResourcePlans = [{ planNo: 'LIP-ORIGINAL', pallets: [{ id: 'PALLET-ORIGINAL', palletNo: 'PLT-ORIGINAL' }] }]
2026-09-07 15:07:39 +08:00
const receivingWarehouse = {
...fbaWarehouse,
id: '9007199254740996', warehouseCode: 'LAX02', warehouseName: '洛杉矶二号接柜仓', warehouseType: 'OVERSEAS',
}
const currentReceivingWarehouse = {
...receivingWarehouse,
id: summary.overseasWarehouseId, warehouseCode: summary.overseasWarehouseCode, warehouseName: summary.overseasWarehouseName,
}
2026-08-27 17:49:05 +08:00
function selectedRow(overrides: Record<string, unknown> = {}) {
return {
id: '2092873463568596994', customerId, sourceContainerBookingId: bookingId, orderNo: 'LMO-260827-0001',
overseasWarehouseId: summary.overseasWarehouseId, overseasWarehouseCode: summary.overseasWarehouseCode,
warehouse: summary.overseasWarehouseName, inboundCode: 'IN-LOCKED', version: 3, pallets: 2,
2026-09-15 12:35:46 +08:00
estimatedWarehouseAt: '2026-09-01T08:00:00', warehouseEta: '2026-09-01 08:00',
...overrides,
}
2026-08-27 17:16:38 +08:00
}
enableAutoUnmount(afterEach)
afterAll(() => vi.unstubAllGlobals())
2026-08-27 17:49:05 +08:00
async function mountView() {
2026-08-27 17:16:38 +08:00
const wrapper = mount(LastMileOrdersView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
2026-08-27 17:49:05 +08:00
return wrapper
}
async function openAfterDeparture() {
2026-08-27 17:49:05 +08:00
const wrapper = await mountView()
const departedTab = wrapper.findAll('[role="tab"]').find((item) => item.text() === '开船后')
await departedTab!.trigger('click')
2026-08-27 17:16:38 +08:00
await flushPromises()
return wrapper
}
async function openHiddenContainerWorkbench(wrapper: Awaited<ReturnType<typeof mountView>>) {
const view = wrapper.vm as unknown as {
statusTab: '全部' | '开船前' | '开船后' | '柜子'
changeStatusTab(): void
}
view.statusTab = '柜子'
view.changeStatusTab()
await flushPromises()
}
2026-08-27 17:49:05 +08:00
async function selectRows(wrapper: Awaited<ReturnType<typeof mountView>>, rows: object[]) {
wrapper.findComponent(DataTable).vm.$emit('selection-change', rows)
await wrapper.vm.$nextTick()
}
/** 定向验证入库弹框,独立于列表页签和批量操作入口。 */
async function openArrivalPlan(wrapper: Awaited<ReturnType<typeof mountView>>, source = selectedRow()) {
;(wrapper.vm as unknown as { openInboundPlan(source: object): void }).openInboundPlan(source)
await flushPromises()
}
2026-08-27 17:49:05 +08:00
function button(wrapper: Awaited<ReturnType<typeof mountView>>, text: string) {
return wrapper.findAll('button').find((item) => item.text().includes(text))
}
function cabinetDialog(wrapper: Awaited<ReturnType<typeof mountView>>) {
return wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('title').startsWith('按柜生成入库计划预览'))!
}
2026-08-27 17:49:05 +08:00
async function openContainerDialog(wrapper: Awaited<ReturnType<typeof mountView>>) {
await button(wrapper, '按柜批量生成')!.trigger('click')
await flushPromises()
}
async function chooseContainer(wrapper: Awaited<ReturnType<typeof mountView>>) {
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
const containerSelect = selects[selects.length - 1]
expect(containerSelect).toBeDefined()
containerSelect.vm.$emit('update:modelValue', bookingId)
2026-09-09 00:44:07 +08:00
containerSelect.vm.$emit('change', bookingId)
await flushPromises()
2026-08-27 17:49:05 +08:00
await wrapper.vm.$nextTick()
}
describe('LastMileOrdersView pending order and container conversion', () => {
2026-10-20 23:34:11 +08:00
it('keeps return row statistics across pages without rendering the long summary blocks', async () => {
2026-10-20 14:00:49 +08:00
const totals = { orderCount: 3, skuTypes: 3, expectedQuantity: 19, receivedQuantity: 14, remainingQuantity: 5, receivedBoxCount: 3, receivedParcelCount: 2, receivedPalletCount: 1 }
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 30, pages: 3 })
vi.mocked(getReturnOrderStatistics).mockResolvedValue({ totals, pageDimensions: [] })
const wrapper = await mountView()
const summary = () => wrapper.find('[aria-label="退货全筛选汇总"]')
2026-10-20 23:34:11 +08:00
expect(summary().exists()).toBe(false)
expect(wrapper.find('[aria-label="仓储转运全筛选汇总"]').exists()).toBe(false)
expect(wrapper.text()).not.toContain('当前筛选中的退货')
2026-10-20 14:00:49 +08:00
const table = wrapper.findComponent(DataTable)
table.vm.$emit('update:page', 2)
await flushPromises()
expect(getReturnOrderStatistics).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2 }))
2026-10-20 23:34:11 +08:00
expect(summary().exists()).toBe(false)
2026-10-20 14:00:49 +08:00
vi.mocked(getReturnOrderStatistics).mockRejectedValueOnce(new Error('统计暂不可用'))
table.vm.$emit('update:page', 3)
await flushPromises()
2026-10-20 17:24:50 +08:00
expect(wrapper.find('[aria-label="退货全筛选汇总"]').exists()).toBe(false)
2026-10-20 14:00:49 +08:00
expect(wrapper.text()).toContain('尾程订单加载失败')
expect(table.props('data')).toEqual([])
})
it('shows commercial source alongside fulfillment type and sends source filters to the server', async () => {
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{ id: '1', customerId: 'A', orderNo: 'WT-1', orderType: 'WAREHOUSE_TRANSFER', businessSource: { businessType: 'LOCAL_LOOSEBOARD', label: '本土散板 · 拼车需求池', businessOrderNo: 'LB-1', externalBookingNo: 'FB-1' } }], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = await mountView()
const table = wrapper.findComponent(DataTable)
expect(table.props('data')[0]).toMatchObject({ businessSource: '本土散板 · 拼车需求池', businessOrderNo: 'LB-1', externalBookingNo: 'FB-1', orderType: '仓储转运', orderTypeCode: 'WAREHOUSE_TRANSFER' })
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.$emit('search', { ...toolbar.props('modelValue'), businessType: 'LOCAL_LOOSEBOARD', businessOrderNo: ' LB-1 ', externalBookingNo: ' FB-1 ' })
await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ businessType: 'LOCAL_LOOSEBOARD', businessOrderNo: 'LB-1', externalBookingNo: 'FB-1' }))
toolbar.vm.$emit('reset'); await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ businessType: undefined, businessOrderNo: undefined, externalBookingNo: undefined }))
})
it('supports keyboard status switching and keeps selection and queries in sync', async () => {
const wrapper = await mountView()
2026-10-20 23:34:11 +08:00
const tabs = wrapper.get('.data-table__toolbar-start .workbench-list-tabs').findAll('[role="tab"]')
expect(tabs.map((tab) => tab.text())).toEqual(['全部', '开船前', '开船后'])
expect(tabs[0]!.attributes('aria-selected')).toBe('true')
expect(tabs[1]!.attributes('tabindex')).toBe('-1')
await selectRows(wrapper, [selectedRow()])
await tabs[0]!.trigger('keydown', { key: 'ArrowRight' })
await flushPromises()
expect(tabs[1]!.attributes('aria-selected')).toBe('true')
expect(tabs[1]!.classes()).toContain('is-active')
expect(document.activeElement).toBe(tabs[1]!.element)
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, orderType: 'FIRST_LEG', sailingStage: 'BEFORE_DEPARTURE', currentStatus: undefined }))
expect((wrapper.vm as unknown as { selectedRows: unknown[] }).selectedRows).toEqual([])
const callCount = vi.mocked(listLastMileOrders).mock.calls.length
await tabs[1]!.trigger('click')
await flushPromises()
expect(vi.mocked(listLastMileOrders)).toHaveBeenCalledTimes(callCount)
await tabs[1]!.trigger('keydown', { key: 'Home' })
await flushPromises()
expect(tabs[0]!.attributes('aria-selected')).toBe('true')
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ currentStatus: undefined, sailingStage: undefined, orderType: undefined }))
await tabs[0]!.trigger('keydown', { key: 'End' })
await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ sailingStage: 'AFTER_DEPARTURE', orderType: 'FIRST_LEG' }))
expect(tabs[2]!.attributes('aria-selected')).toBe('true')
})
it('exports the same stage and filters as the list and restores other types in all', async () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), orderType: 'RETURN_RELABEL', detained: 'false' })
await wrapper.vm.$nextTick()
for (const [label, stage] of [['开船前', 'BEFORE_DEPARTURE'], ['开船后', 'AFTER_DEPARTURE']] as const) {
await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === label)!.trigger('click')
await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ orderType: 'FIRST_LEG', sailingStage: stage, detained: false }))
expect(toolbar.props('availableFields')?.some((field) => field.key === 'orderType')).toBe(false)
if (stage === 'BEFORE_DEPARTURE') {
expect(button(wrapper, '导出订单')).toBeUndefined()
} else {
await button(wrapper, '导出订单')!.trigger('click')
await flushPromises()
expect(submitExport).toHaveBeenLastCalledWith('LAST_MILE_ORDER_LIST', '尾程订单', vi.mocked(listLastMileOrders).mock.lastCall![0])
}
}
await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === '全部')!.trigger('click')
await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ orderType: 'RETURN_RELABEL', sailingStage: undefined }))
expect(toolbar.props('availableFields')?.some((field) => field.key === 'orderType')).toBe(true)
})
it('preserves advanced conditions when collapsed and clears them on reset', async () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
expect(toolbar.props('fields').map((field) => field.key)).toEqual(['orderNo', 'inboundCode', 'warehouse'])
const initialCalls = vi.mocked(listLastMileOrders).mock.calls.length
await button(wrapper, '高级筛选')!.trigger('click')
expect(toolbar.props('fields')).toHaveLength(16)
toolbar.vm.$emit('update:modelValue', {
...toolbar.props('modelValue'), containerNo: ' CONT-15 ', abnormal: 'false',
createdAt: ['2026-09-01', '2026-09-30'],
})
await wrapper.vm.$nextTick()
await button(wrapper, '收起筛选')!.trigger('click')
expect(vi.mocked(listLastMileOrders)).toHaveBeenCalledTimes(initialCalls)
expect(toolbar.props('modelValue').containerNo).toBe(' CONT-15 ')
await button(wrapper, '查询')!.trigger('click')
await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({
containerNo: 'CONT-15', abnormal: false,
createdFrom: '2026-09-01T00:00:00', createdTo: '2026-09-30T23:59:59',
}))
await button(wrapper, '重置')!.trigger('click')
await flushPromises()
expect(vi.mocked(listLastMileOrders)).toHaveBeenCalledTimes(initialCalls + 2)
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({
page: 1, containerNo: undefined, abnormal: undefined, createdFrom: undefined, createdTo: undefined,
}))
await button(wrapper, '高级筛选')!.trigger('click')
expect(toolbar.props('modelValue').containerNo).toBe('')
expect(toolbar.props('modelValue').createdAt).toEqual([])
})
it('keeps container filters separate from the advanced order fields', async () => {
const wrapper = await mountView()
await button(wrapper, '高级筛选')!.trigger('click')
await openHiddenContainerWorkbench(wrapper)
expect(wrapper.findComponent(QueryToolbar).props('fields').map((field) => field.key)).toEqual(['containerNo', 'createdAt'])
expect(button(wrapper, '收起筛选')).toBeUndefined()
expect(button(wrapper, '高级筛选')).toBeUndefined()
})
it.each([null, 'ESTIMATED_DEPARTURE'])('keeps a first-leg order without actual departure read-only: %s', async containerNode => {
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{ id: 'before-1', customerId, orderNo: 'LM-BEFORE', currentStatus: 'PENDING_ASSIGNMENT', orderType: 'FIRST_LEG', containerNode }], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = await mountView()
const actions = wrapper.get('.el-table__body-wrapper .row-actions')
expect(actions.findAll('button').map(button => button.text().trim())).toEqual(['查看'])
await selectRows(wrapper, [(wrapper.findComponent(DataTable).props('data') as object[])[0]])
expect((wrapper.vm as unknown as { selectedRows: object[] }).selectedRows).toEqual([expect.objectContaining({ orderTypeCode: 'FIRST_LEG', containerNode: containerNode || '—' })])
expect(wrapper.find('.data-table__business-actions').exists()).toBe(false)
expect(createManualInboundPlan).not.toHaveBeenCalled()
expect(transferLastMileOrders).not.toHaveBeenCalled()
})
2026-09-17 13:36:31 +08:00
it('keeps the original carton count and adds only the signed difference in the list', async () => {
canAccess.mockReturnValue(false)
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: 'transfer-1', customerId, orderNo: 'WT-1', orderType: 'WAREHOUSE_TRANSFER', currentStatus: 'WAIT_CUSTOMER_CONFIRMATION',
packageCount: 10, receivedPackageCount: 8, packageDifference: -2,
}], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = await mountView()
const table = wrapper.findComponent(DataTable)
const columns = table.props('columns') as Array<{ key: string; label: string }>
expect(columns).toEqual(expect.arrayContaining([expect.objectContaining({ key: 'cartonDifference', label: '差异(箱)' })]))
expect(columns.some((column) => ['declaredCartons', 'receivedCartons'].includes(column.key))).toBe(false)
expect((table.props('data') as Array<{ cartons: unknown }>)[0]?.cartons).toBe(10)
expect(wrapper.get('.package-difference--nonzero').text()).toBe('-2')
})
2026-09-14 20:21:02 +08:00
it.each([
[[], ['查看'], false],
2026-10-19 17:56:31 +08:00
[['inbound-order:create'], ['查看', '入库'], false],
[['last-mile-order:edit'], ['查看', '修改节点'], false],
[['inbound-order:create', 'last-mile-order:edit'], ['查看', '入库', '修改节点'], false],
2026-09-14 20:21:02 +08:00
])('only collapses row actions above three for %j', async (allowed, labels, overflow) => {
canAccess.mockImplementation(code => allowed.some(action => code === 'platform:overseas-warehouse:' + action))
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{ id: 'order-1', customerId, orderNo: 'LM-1', currentStatus: 'PENDING_ASSIGNMENT', orderType: 'FIRST_LEG', containerNode: 'ACTUAL_DEPARTURE' }], page: 1, size: 10, total: 1, pages: 1 })
2026-09-14 20:21:02 +08:00
const wrapper = await mountView()
const actions = wrapper.get('.el-table__body-wrapper .row-actions')
expect(actions.findAll('button').map(button => button.text().trim())).toEqual(labels)
expect(actions.find('.el-dropdown').exists()).toBe(overflow)
})
beforeEach(() => {
vi.restoreAllMocks()
vi.mocked(reviewCargoDiscard).mockReset().mockResolvedValue({})
canAccess.mockReset().mockReturnValue(true)
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('localStorage', memoryStorage())
vi.mocked(listLastMileOrders).mockReset().mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
2026-08-28 13:54:44 +08:00
vi.mocked(listContainerPlanProgress).mockReset().mockResolvedValue({ records: [summary], page: 1, size: 50, total: 1, pages: 1 })
vi.mocked(getContainerPlanProgress).mockReset().mockResolvedValue({
summary,
orders: [{ orderId: '2092873463568596994', customerId, orderNo: 'LMO-1', inboundCode: 'IN-1', orderStatus: 'INBOUND_PLANNED', planId: '2092873463568597010', planNo: 'LIP-1', planStatus: 'PENDING_RECEIVING', planValid: true, plannedArrivalAt: '2026-09-01T08:00:00' }],
})
vi.mocked(createManualInboundPlan).mockReset().mockResolvedValue({} as never)
vi.mocked(listContainerArrivalTimes).mockReset().mockResolvedValue([arrivalTime])
2026-09-09 00:44:07 +08:00
vi.mocked(getContainerInboundPlanPreview).mockReset().mockResolvedValue(preview as never)
vi.mocked(getLastMileOrderManualNodes).mockReset().mockResolvedValue({ orderId: '2092873463568596994', manualOverrideEnabled: false, nodes: [], version: 0 })
vi.mocked(updateLastMileOrderHold).mockReset().mockResolvedValue({ orderId: '2092873463568596994', held: true, chargeable: true, version: 1 })
vi.mocked(updateLastMileOrderManualNodes).mockReset().mockResolvedValue({ orderId: '2092873463568596994', manualOverrideEnabled: true, nodes: [], version: 1 })
2026-09-09 00:44:07 +08:00
vi.mocked(convertLastMileContainerToInboundPlans).mockReset().mockResolvedValue([
{ planNo: 'LIP-1', pallets: [{ id: 'PALLET-1', palletNo: 'PLT-001' }] },
{ planNo: 'LIP-2', pallets: [{ id: 'PALLET-2', palletNo: 'PLT-002' }] },
] as never)
2026-09-07 15:07:39 +08:00
vi.mocked(changeContainerReceivingWarehouse).mockReset().mockResolvedValue({ ...summary, overseasWarehouseId: '9007199254740996', overseasWarehouseCode: 'LAX02', overseasWarehouseName: '洛杉矶二号接柜仓', assignmentVersion: 8 })
2026-08-28 13:54:44 +08:00
vi.mocked(listAllPlatformWarehouses).mockReset().mockResolvedValue([fbaWarehouse] as never)
vi.mocked(transferLastMileOrders).mockReset().mockResolvedValue(2)
})
async function openDiscard(overrides: Record<string, unknown> = {}) {
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: '2092873463568596994', customerId, orderNo: 'CD-TEST-1', sourceOrderNo: 'CD-TEST-1',
orderType: 'CARGO_DISCARD', currentStatus: 'PENDING_REVIEW', inboundOrderNo: 'CD-TEST-1', ...overrides,
}], page: 1, size: 10, total: 1, pages: 1 })
return mountView()
}
it.each([
['lastleg:cargo-discard:approve', 'PENDING_REVIEW', true],
['platform:overseas-warehouse:last-mile-order:edit', 'PENDING_REVIEW', false],
['lastleg:cargo-discard:approve', 'APPROVED', false],
['lastleg:cargo-discard:approve', 'CANCELLED', false],
])('gates discard review on exact permission %s and status %s', async (code, currentStatus, visible) => {
canAccess.mockImplementation(permissionCode => permissionCode === code)
const wrapper = await openDiscard({ currentStatus })
expect(Boolean(button(wrapper, '审核'))).toBe(visible)
expect(reviewCargoDiscard).not.toHaveBeenCalled()
})
it.each([{ orderNo: '' }, { customerId: '' }])('does not expose discard review without a business target %j', async (overrides) => {
const wrapper = await openDiscard(overrides)
expect(button(wrapper, '审核')).toBeUndefined()
expect(reviewCargoDiscard).not.toHaveBeenCalled()
})
it('approves the selected discard once and reloads the server status', async () => {
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
const wrapper = await openDiscard()
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: '2092873463568596994', customerId, orderNo: 'CD-TEST-1',
orderType: 'CARGO_DISCARD', currentStatus: 'APPROVED', inboundOrderNo: 'CD-TEST-1',
}], page: 1, size: 10, total: 1, pages: 1 })
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
expect(confirm).toHaveBeenCalledWith(expect.stringContaining('CD-TEST-1'), '废弃申请审核 · CD-TEST-1', expect.objectContaining({ distinguishCancelAndClose: true }))
expect(reviewCargoDiscard).toHaveBeenCalledExactlyOnceWith('CD-TEST-1', customerId, 'APPROVE', undefined)
expect(button(wrapper, '审核')).toBeUndefined()
})
it('rejects with a trimmed mandatory reason without approving first', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockRejectedValue('cancel')
const prompt = vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue({ value: ' 箱数不符 ', action: 'confirm' } as Awaited<ReturnType<typeof ElMessageBox.prompt>>)
const wrapper = await openDiscard()
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
const options = prompt.mock.calls[0]?.[2]
expect(options?.inputValidator?.(' ')).not.toBe(true)
expect(options?.inputValidator?.('甲'.repeat(1001))).not.toBe(true)
expect(options?.inputValidator?.('箱数不符')).toBe(true)
expect(reviewCargoDiscard).toHaveBeenCalledExactlyOnceWith('CD-TEST-1', customerId, 'REJECT', '箱数不符')
})
it('closing review or cancelling the reason makes no request', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('close').mockRejectedValueOnce('cancel')
vi.spyOn(ElMessageBox, 'prompt').mockRejectedValue('cancel')
const wrapper = await openDiscard()
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
expect(reviewCargoDiscard).not.toHaveBeenCalled()
expect(listLastMileOrders).toHaveBeenCalledTimes(1)
})
it('locks repeat clicks while review is pending and allows retry after an API failure', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
const errorToast = vi.spyOn(ElMessage, 'error')
let rejectRequest!: (error: Error) => void
vi.mocked(reviewCargoDiscard).mockImplementationOnce(() => new Promise((_, reject) => { rejectRequest = reject }))
const wrapper = await openDiscard()
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
expect((button(wrapper, '审核')!.element as HTMLButtonElement).disabled).toBe(true)
await button(wrapper, '审核')!.trigger('click')
expect(reviewCargoDiscard).toHaveBeenCalledTimes(1)
rejectRequest(new Error('审批请求失败'))
await flushPromises()
expect(errorToast).toHaveBeenCalled()
expect((button(wrapper, '审核')!.element as HTMLButtonElement).disabled).toBe(false)
expect(listLastMileOrders).toHaveBeenCalledTimes(1)
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
expect(reviewCargoDiscard).toHaveBeenCalledTimes(2)
})
2026-09-18 09:59:12 +08:00
it.each([
2026-09-20 22:04:59 +08:00
['PENDING_ASSIGNMENT', false],
['INBOUND_COMPLETED', true],
])('only shows a new hold after inbound completion for %s', async (currentStatus, visible) => {
2026-09-18 09:59:12 +08:00
canAccess.mockImplementation(code => code === 'platform:overseas-warehouse:last-mile-order:edit')
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: 'order-1', customerId, orderNo: 'LM-1', currentStatus, orderType: 'FIRST_LEG', containerNode: 'ACTUAL_DEPARTURE', detained: false,
2026-09-18 09:59:12 +08:00
}], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = await mountView()
const holdButton = wrapper.get('.el-table__body-wrapper .row-actions').findAll('button')
2026-09-20 22:04:59 +08:00
.find(item => item.text().includes('暂扣订单'))
expect(Boolean(holdButton)).toBe(visible)
if (holdButton) expect((holdButton.element as HTMLButtonElement).disabled).toBe(false)
})
it('keeps release hold available for an already detained order', async () => {
canAccess.mockImplementation(code => code === 'platform:overseas-warehouse:last-mile-order:edit')
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: 'order-held', customerId, orderNo: 'LM-HELD', currentStatus: 'READY_FOR_DISPATCH', orderType: 'FIRST_LEG', containerNode: 'ACTUAL_DEPARTURE', detained: true,
2026-09-20 22:04:59 +08:00
}], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = await mountView()
const releaseButton = wrapper.get('.el-table__body-wrapper .row-actions').findAll('button')
.find(item => item.text().includes('解除暂扣'))
expect(releaseButton).toBeTruthy()
expect((releaseButton!.element as HTMLButtonElement).disabled).toBe(false)
2026-09-18 09:59:12 +08:00
})
2026-09-20 21:52:12 +08:00
it('removes generation and hold actions after an inbound plan has been submitted', async () => {
canAccess.mockReturnValue(true)
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: 'order-planned', customerId, orderNo: 'LM-PLANNED', currentStatus: 'INBOUND_PLANNED', orderType: 'FIRST_LEG', containerNode: 'ACTUAL_DEPARTURE', detained: false,
2026-09-20 21:52:12 +08:00
}], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = await mountView()
const actions = wrapper.get('.el-table__body-wrapper .row-actions')
expect(actions.text()).toContain('查看')
2026-10-19 17:56:31 +08:00
expect(actions.text()).not.toContain('入库')
2026-09-20 21:52:12 +08:00
expect(actions.text()).not.toContain('暂扣订单')
await selectRows(wrapper, [(wrapper.findComponent(DataTable).props('data') as object[])[0]])
2026-10-19 17:56:31 +08:00
expect((button(wrapper, '入库')!.element as HTMLButtonElement).disabled).toBe(true)
2026-09-20 21:52:12 +08:00
})
it('keeps after-departure order actions in the existing table without a container view switch', async () => {
const wrapper = await openAfterDeparture()
2026-08-27 17:49:05 +08:00
expect(wrapper.text()).toContain('尾程单号')
expect(wrapper.text()).toContain('海外仓')
expect(wrapper.text()).toContain('转仓')
2026-10-19 17:56:31 +08:00
expect(wrapper.text()).toContain('入库')
2026-08-27 17:49:05 +08:00
expect(wrapper.text()).toContain('按柜批量生成')
expect(wrapper.text()).not.toContain('按订单')
expect(wrapper.text()).not.toContain('待分配整柜')
expect(wrapper.text()).not.toContain('柜子工作台')
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, size: 10, currentStatus: undefined, orderType: 'FIRST_LEG', sailingStage: 'AFTER_DEPARTURE' }))
2026-08-28 13:54:44 +08:00
expect(listContainerPlanProgress).not.toHaveBeenCalled()
2026-08-27 17:16:38 +08:00
})
it('forwards complete searches and switches every status tab without stale filter failures', async () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
const model = {
orderNo: 'LMO-SEARCH', inboundCode: 'IN-SEARCH', lxCode: 'LX-SEARCH', orderType: 'FIRST_LEG',
warehouse: 'LAX', sourceOrderNo: 'SOURCE-SEARCH', containerNo: 'CONT-SEARCH', transferWarehouseCode: 'ONT8',
status: 'DELIVERED', abnormal: 'true', detained: 'false', createdAt: ['2026-09-01', '2026-09-07'],
}
toolbar.vm.$emit('search', model)
await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith({
page: 1, size: 10, orderNo: 'LMO-SEARCH', inboundCode: 'IN-SEARCH', lxCode: 'LX-SEARCH', orderType: 'FIRST_LEG',
warehouse: 'LAX', sourceOrderNo: 'SOURCE-SEARCH', containerNo: 'CONT-SEARCH', transferWarehouseCode: 'ONT8',
currentStatus: 'DELIVERED', abnormal: true, detained: false,
createdFrom: '2026-09-01T00:00:00', createdTo: '2026-09-07T23:59:59',
})
const availableFields = toolbar.props('availableFields') as Array<{ key: string; label: string; placeholder?: string }>
expect(availableFields.find((field) => field.key === 'lxCode')).toMatchObject({
label: 'LX代码', placeholder: '请输入LX代码',
})
2026-09-18 11:41:27 +08:00
expect(availableFields.filter((field) => ['orderType', 'status', 'abnormal', 'detained'].includes(field.key)))
.toEqual(expect.arrayContaining([
expect.objectContaining({ key: 'orderType', placeholder: '请选择履约类型' }),
2026-09-18 11:41:27 +08:00
expect.objectContaining({ key: 'status', placeholder: '请选择当前状态' }),
expect.objectContaining({ key: 'abnormal', placeholder: '请选择是否异常' }),
expect.objectContaining({ key: 'detained', placeholder: '请选择是否暂扣' }),
]))
expect(availableFields.map((field) => field.key)).not.toEqual(expect.arrayContaining(['planNo', 'inboundOrderNo']))
await wrapper.findAll('[role="tab"]').find((item) => item.text() === '开船前')!.trigger('click')
await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ currentStatus: 'DELIVERED', sailingStage: 'BEFORE_DEPARTURE', orderType: 'FIRST_LEG', lxCode: 'LX-SEARCH' }))
await openHiddenContainerWorkbench(wrapper)
expect(listContainerPlanProgress).toHaveBeenLastCalledWith({
page: 1, size: 10, containerNo: 'CONT-SEARCH',
createdFrom: '2026-09-01T00:00:00', createdTo: '2026-09-07T23:59:59',
})
await wrapper.findAll('[role="tab"]').find((item) => item.text() === '全部')!.trigger('click')
await flushPromises()
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ currentStatus: 'DELIVERED', lxCode: 'LX-SEARCH' }))
const callsBeforeReset = vi.mocked(listLastMileOrders).mock.calls.length
;(toolbar.vm as unknown as { reset(): void }).reset()
await flushPromises()
expect(vi.mocked(listLastMileOrders).mock.calls).toHaveLength(callsBeforeReset + 1)
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1 }))
expect(vi.mocked(listLastMileOrders).mock.calls.at(-1)?.[0].lxCode).toBeUndefined()
expect(wrapper.text()).not.toContain('尾程订单加载失败')
})
it('opens the required single-order inbound-plan form without invoking the cabinet conversion', async () => {
const wrapper = await openAfterDeparture()
2026-08-27 17:49:05 +08:00
await selectRows(wrapper, [selectedRow()])
2026-10-19 17:56:31 +08:00
await button(wrapper, '入库')!.trigger('click')
2026-08-27 17:16:38 +08:00
await flushPromises()
expect(document.body.textContent).toContain('计划入库仓')
2026-09-15 12:35:46 +08:00
expect(document.body.textContent).toContain('预计到仓时间')
expect(document.body.textContent).toContain('托盘类型')
const sourceFacts = wrapper.get('[aria-label="入库来源"]')
expect(sourceFacts.find('input').exists()).toBe(false)
expect(sourceFacts.text()).toContain('IN-LOCKED')
expect(sourceFacts.text()).toContain('LAX01 · 洛杉矶接柜仓')
expect(sourceFacts.text()).toContain('2026-09-01 08:00')
expect(wrapper.find('button[aria-label="查看入库来源说明"]').exists()).toBe(true)
expect(listAllPlatformWarehouses).not.toHaveBeenCalled()
expect(createManualInboundPlan).not.toHaveBeenCalled()
2026-08-27 17:49:05 +08:00
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
2026-08-28 13:54:44 +08:00
expect(listContainerPlanProgress).not.toHaveBeenCalled()
2026-08-27 17:16:38 +08:00
})
2026-09-15 12:35:46 +08:00
it('shows cabinet-frozen facts but never sends arrival time or per-order warehouse overrides', async () => {
const wrapper = await openAfterDeparture()
await selectRows(wrapper, [selectedRow()])
2026-10-19 17:56:31 +08:00
await button(wrapper, '入库')!.trigger('click')
await flushPromises()
const confirm = [...document.body.querySelectorAll('button')]
.find((item) => item.textContent?.includes('保存并生成入库单')) as HTMLButtonElement
confirm.click()
await flushPromises()
expect(createManualInboundPlan).toHaveBeenCalledWith(expect.objectContaining({
customerId, orderId: '2092873463568596994', expectedOrderVersion: 3,
2026-09-15 12:35:46 +08:00
palletCount: 5, palletType: 'WOOD',
}), expect.stringContaining('manual-inbound-plan-'))
const payload = vi.mocked(createManualInboundPlan).mock.calls[0][0] as unknown as Record<string, unknown>
expect(payload).not.toHaveProperty('inboundCode')
2026-09-15 12:35:46 +08:00
expect(payload).not.toHaveProperty('plannedArrivalDate')
expect(payload).not.toHaveProperty('plannedWarehouseId')
expect(payload).not.toHaveProperty('plannedWarehouseCode')
expect(payload).not.toHaveProperty('plannedWarehouseName')
})
2026-09-20 21:52:12 +08:00
it('blocks a held order from generating its inbound plan', async () => {
const wrapper = await openAfterDeparture()
2026-09-18 09:59:12 +08:00
await selectRows(wrapper, [selectedRow({
detained: true,
orderTypeCode: 'ONE_CLICK_DROPSHIP',
currentStatusCode: 'PENDING_ASSIGNMENT',
})])
2026-10-19 17:56:31 +08:00
await button(wrapper, '入库')!.trigger('click')
2026-09-18 09:59:12 +08:00
await flushPromises()
2026-09-20 21:52:12 +08:00
expect(getContainerInboundPlanPreview).not.toHaveBeenCalled()
2026-09-18 09:59:12 +08:00
const confirm = [...document.body.querySelectorAll('button')]
.find((item) => item.textContent?.includes('保存并生成入库单')) as HTMLButtonElement
2026-09-20 21:52:12 +08:00
expect(confirm).toBeUndefined()
2026-09-18 09:59:12 +08:00
})
it.each([
[{ ...arrivalTime, estimatedWarehouseArrivalAt: '2026-10-25T09:30:00', plannedDate: '2026-10-25' }, '2026-10-25 09:30'],
[{ ...arrivalTime, estimatedWarehouseArrivalAt: null, version: 0, plannedDate: '2026-11-01', isDefault: true }, '2026-11-01(默认推算)'],
[{ ...arrivalTime, estimatedWarehouseArrivalAt: null, version: 0, planCount: 1, plannedDate: '2026-11-02' }, '2026-11-02'],
])('reads the latest cabinet arrival on opening and permits planning with %s', async (arrival, expectedText) => {
vi.mocked(listContainerArrivalTimes).mockResolvedValue([arrival])
const wrapper = await openAfterDeparture()
await openArrivalPlan(wrapper, arrival.estimatedWarehouseArrivalAt ? selectedRow() : selectedRow({ estimatedWarehouseAt: '', warehouseEta: '—' }))
expect(listContainerArrivalTimes).toHaveBeenCalledWith([bookingId])
const source = wrapper.get('[aria-label="入库来源"]')
expect(source.text()).toContain(expectedText)
if (!arrival.estimatedWarehouseArrivalAt) expect(source.text()).not.toContain('00:00')
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('入库计划 ·'))!
expect(dialog.props('confirmDisabled')).toBe(false)
dialog.vm.$emit('confirm')
2026-09-15 12:35:46 +08:00
await flushPromises()
expect(createManualInboundPlan).toHaveBeenCalledTimes(1)
expect(vi.mocked(createManualInboundPlan).mock.calls[0][0]).not.toHaveProperty('plannedArrivalDate')
})
it.each([
{ arrivals: [] },
{ arrivals: [{ ...arrivalTime, estimatedWarehouseArrivalAt: null, plannedDate: null }] },
{ arrivals: [{ ...arrivalTime, version: -1 }] },
])('blocks manual planning when the authoritative cabinet arrival is missing or inconsistent: $arrivals', async ({ arrivals }) => {
vi.mocked(listContainerArrivalTimes).mockResolvedValue(arrivals)
const wrapper = await openAfterDeparture()
await openArrivalPlan(wrapper)
2026-09-15 12:35:46 +08:00
const confirm = [...document.body.querySelectorAll('button')]
.find((item) => item.textContent?.includes('保存并生成入库单')) as HTMLButtonElement
expect(confirm.disabled).toBe(true)
expect(createManualInboundPlan).not.toHaveBeenCalled()
})
it('shows arrival read failures and does not reuse the list timestamp', async () => {
vi.mocked(listContainerArrivalTimes).mockRejectedValue(new ApiResponseError('到仓来源暂不可读', 'A0503', 503))
const wrapper = await openAfterDeparture()
await openArrivalPlan(wrapper)
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('入库计划 ·'))!
expect(dialog.text()).toContain('到仓来源暂不可读')
expect(dialog.get('[aria-label="入库来源"]').text()).not.toContain('2026-09-01')
expect(dialog.props('confirmDisabled')).toBe(true)
dialog.vm.$emit('confirm'); await flushPromises()
expect(createManualInboundPlan).not.toHaveBeenCalled()
})
it('keeps confirmation blocked during arrival loading and ignores an older opening of the same order', async () => {
let resolveOld!: (value: typeof arrivalTime[]) => void
vi.mocked(listContainerArrivalTimes).mockReturnValueOnce(new Promise(resolve => { resolveOld = resolve }))
const wrapper = await openAfterDeparture()
await openArrivalPlan(wrapper)
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('入库计划 ·'))!
expect(dialog.props('confirmDisabled')).toBe(true)
dialog.vm.$emit('confirm'); await flushPromises()
expect(createManualInboundPlan).not.toHaveBeenCalled()
dialog.vm.$emit('update:modelValue', false)
await wrapper.vm.$nextTick()
vi.mocked(listContainerArrivalTimes).mockResolvedValueOnce([{ ...arrivalTime, estimatedWarehouseArrivalAt: '2026-10-26T10:00:00', plannedDate: '2026-10-26' }])
await openArrivalPlan(wrapper)
expect(dialog.get('[aria-label="入库来源"]').text()).toContain('2026-10-26 10:00')
resolveOld([arrivalTime]); await flushPromises()
expect(dialog.get('[aria-label="入库来源"]').text()).toContain('2026-10-26 10:00')
expect(dialog.props('confirmDisabled')).toBe(false)
})
it('locks one-click dropship manual plans to the frozen source pallet count', async () => {
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({
...preview,
sourcePalletCount: 1,
groups: [{
...preview.groups[0], destinationType: 'ONE_CLICK_DROPSHIP', sourcePalletCount: 1,
orders: [{
...preview.groups[0].orders[0], orderType: 'ONE_CLICK_DROPSHIP',
destinationType: 'ONE_CLICK_DROPSHIP', sourcePalletCount: 1,
targetPalletCount: 1, supplementPalletCount: 0,
}],
}],
} as never)
const wrapper = await openAfterDeparture()
await selectRows(wrapper, [selectedRow({ orderTypeCode: 'ONE_CLICK_DROPSHIP', pallets: 2 })])
2026-10-19 17:56:31 +08:00
await button(wrapper, '入库')!.trigger('click')
await flushPromises()
expect(wrapper.find('.command-form').findComponent({ name: 'ElInputNumber' }).exists()).toBe(false)
expect(wrapper.get('.frozen-pallet-count').text()).toBe('1')
expect(document.body.textContent).toContain('固定按头程已封托托盘数整体入库')
;([...document.body.querySelectorAll('button')]
.find((item) => item.textContent?.includes('保存并生成入库单')) as HTMLButtonElement).click()
await flushPromises()
expect(createManualInboundPlan).toHaveBeenCalledWith(expect.objectContaining({ palletCount: 1 }),
expect.stringContaining('manual-inbound-plan-'))
})
2026-08-27 17:49:05 +08:00
it('opens a cabinet selector without requiring order selection and loads server summaries only then', async () => {
const wrapper = await openAfterDeparture()
2026-08-27 17:49:05 +08:00
await openContainerDialog(wrapper)
2026-08-28 13:54:44 +08:00
expect(listContainerPlanProgress).toHaveBeenCalledTimes(1)
expect(listContainerPlanProgress).toHaveBeenCalledWith({ page: 1, size: 50, containerNo: undefined })
2026-08-27 17:49:05 +08:00
expect(document.body.textContent).toContain('选择待分配柜')
await cabinetDialog(wrapper).get('button[aria-label="查看按柜生成说明"]').trigger('focus')
await new Promise(resolve => setTimeout(resolve, 200)); await flushPromises()
2026-09-09 00:44:07 +08:00
expect(document.body.textContent).toContain('确认后整柜只提交一次生成命令')
expect(getContainerInboundPlanPreview).not.toHaveBeenCalled()
2026-08-27 17:49:05 +08:00
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
2026-08-27 17:16:38 +08:00
})
2026-08-27 17:49:05 +08:00
it('selects one cabinet and sends one long-id convert-container request without using current-page row counts', async () => {
const wrapper = await openAfterDeparture()
2026-08-27 17:49:05 +08:00
await openContainerDialog(wrapper)
await chooseContainer(wrapper)
2026-09-09 00:44:07 +08:00
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
2026-08-27 17:49:05 +08:00
expect(confirm.disabled).toBe(false)
confirm.click()
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1)
2026-09-09 00:44:07 +08:00
expect(getContainerInboundPlanPreview).toHaveBeenCalledTimes(2)
expect(getContainerInboundPlanPreview).toHaveBeenNthCalledWith(1, bookingId)
expect(getContainerInboundPlanPreview).toHaveBeenNthCalledWith(2, bookingId)
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({
sourceContainerBookingId: bookingId,
snapshotVersion: 9,
snapshotHash: 'a'.repeat(64),
}, expect.stringContaining('container-inbound-plan-'))
expect(createManualInboundPlan).not.toHaveBeenCalled()
2026-09-09 00:44:07 +08:00
expect(document.body.textContent).toContain('新建 2 个 / 已有 0 个')
expect(document.body.textContent).toContain('PLT-001')
2026-08-27 17:49:05 +08:00
expect(listLastMileOrders).toHaveBeenCalledTimes(3)
})
it.each(['A0404', 'A0409'])('keeps %s request evidence visible after cabinet conversion failure', async (code) => {
2026-08-27 17:16:38 +08:00
vi.mocked(convertLastMileContainerToInboundPlans).mockRejectedValueOnce(new ApiResponseError(
'柜级生成失败', code, 200, undefined, `request-${code}`, `trace-${code}`,
))
const wrapper = await openAfterDeparture()
2026-08-27 17:49:05 +08:00
await openContainerDialog(wrapper)
await chooseContainer(wrapper)
2026-09-09 00:44:07 +08:00
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
2026-08-27 17:49:05 +08:00
confirm.click()
2026-08-27 17:16:38 +08:00
await flushPromises()
2026-08-27 17:49:05 +08:00
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1)
2026-08-27 17:16:38 +08:00
expect(wrapper.text()).toContain(code)
expect(wrapper.text()).toContain(`request-${code}`)
expect(wrapper.text()).toContain(`trace-${code}`)
})
2026-08-28 13:54:44 +08:00
2026-09-09 00:44:07 +08:00
it('keeps the latest cabinet preview when an older request resolves last', async () => {
let resolveFirst!: (value: typeof preview) => void
let resolveSecond!: (value: typeof preview) => void
vi.mocked(getContainerInboundPlanPreview).mockImplementation((id) => new Promise((resolve) => {
if (id === bookingId) resolveFirst = resolve
else resolveSecond = resolve
}) as never)
const wrapper = await openAfterDeparture()
2026-09-09 00:44:07 +08:00
await openContainerDialog(wrapper)
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
const containerSelect = selects[selects.length - 1]
containerSelect.vm.$emit('update:modelValue', bookingId)
containerSelect.vm.$emit('change', bookingId)
await wrapper.vm.$nextTick()
containerSelect.vm.$emit('update:modelValue', '2092873463568597002')
containerSelect.vm.$emit('change', '2092873463568597002')
await wrapper.vm.$nextTick()
resolveSecond({ ...preview, sourceContainerBookingId: '2092873463568597002', containerNo: 'CONT-NEW', pendingPalletCount: 2 })
await flushPromises()
expect(document.body.querySelector('.container-preview__identity')?.textContent).toContain('CONT-NEW')
resolveFirst(preview)
await flushPromises()
expect(document.body.querySelector('.container-preview__identity')?.textContent).toContain('CONT-NEW')
})
it('reuses the same idempotency key when an uncertain confirmation is retried', async () => {
vi.mocked(convertLastMileContainerToInboundPlans)
.mockRejectedValueOnce(new Error('network uncertain'))
.mockResolvedValueOnce([{ planNo: 'LIP-RETRY', pallets: [{ id: 'PALLET-R', palletNo: 'PLT-RETRY' }] }] as never)
const wrapper = await openAfterDeparture()
2026-09-09 00:44:07 +08:00
await openContainerDialog(wrapper)
await chooseContainer(wrapper)
const findConfirm = () => [...document.body.querySelectorAll('button')]
.find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
findConfirm().click()
await flushPromises()
findConfirm().click()
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(2)
expect(vi.mocked(convertLastMileContainerToInboundPlans).mock.calls[0][1]).toBe(
vi.mocked(convertLastMileContainerToInboundPlans).mock.calls[1][1],
)
expect(document.body.textContent).toContain('PLT-RETRY')
})
it('renders all cabinets as an arrival-ordered workbench with the remaining inbound-code count', async () => {
2026-08-28 13:54:44 +08:00
const nullArrival = { ...summary, sourceContainerBookingId: '2092873463568597002', containerNo: 'CONT-NULL', plannedArrivalAt: null, planCount: 0, pendingPlanCount: 15 }
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [summary, nullArrival], page: 1, size: 10, total: 2, pages: 1 })
2026-08-28 13:54:44 +08:00
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-08-28 13:54:44 +08:00
expect(listContainerPlanProgress).toHaveBeenLastCalledWith({ page: 1, size: 10, containerNo: undefined, createdFrom: undefined, createdTo: undefined })
expect(wrapper.text()).not.toContain('柜子工作台(2)')
expect(wrapper.text()).toContain('货柜工作台')
2026-10-20 15:48:27 +08:00
expect(wrapper.text()).not.toContain('按柜查看和修改海外接柜仓;整柜计划齐全后自动生成托盘码')
2026-09-02 14:39:34 +08:00
expect(wrapper.text()).toContain('5个入仓码待生成计划')
expect(wrapper.text()).toContain('共 15 个入仓码')
expect(wrapper.text()).not.toContain('待补入仓码')
expect(wrapper.text()).not.toContain('缺入仓码')
expect(wrapper.text()).not.toContain('已生成 10 个计划')
expect(wrapper.text()).not.toContain('存在5票尚未生成有效入库计划')
expect(wrapper.text()).toContain('2 个货柜')
2026-08-28 13:54:44 +08:00
expect(wrapper.text().indexOf('CONT-15')).toBeLessThan(wrapper.text().indexOf('CONT-NULL'))
})
2026-09-10 17:05:48 +08:00
it('presents a resource-pending cabinet as an exceptional pallet-code retry', async () => {
const ready = {
...summary,
planCount: summary.inboundCodeCount,
plannedInboundCodeCount: summary.inboundCodeCount,
pendingPlanCount: 0,
2026-09-10 16:33:43 +08:00
resourcePendingPlanCount: 1,
allPlansReady: true,
blockingReason: null,
}
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [ready], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
expect(wrapper.text()).toContain('CONT-15')
expect(wrapper.text()).toContain('0个入仓码待生成计划')
2026-09-10 17:05:48 +08:00
expect(wrapper.text()).toContain('托盘码自动生成异常,可重试')
expect(wrapper.text()).not.toContain('待补真实托盘')
expect((button(wrapper, '重试生成托盘码')!.element as HTMLButtonElement).disabled).toBe(false)
expect((button(wrapper, '修改接柜仓')!.element as HTMLButtonElement).disabled).toBe(false)
})
it('restores the original resource command after reopening a locally committed cabinet', async () => {
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({
records: [{ ...summary, pendingPlanCount: 0, resourcePendingPlanCount: 1, allPlansReady: true }],
page: 1, size: 10, total: 1, pages: 1,
})
vi.mocked(getContainerInboundPlanPreview).mockResolvedValueOnce(resourceCommittedPreview)
vi.mocked(convertLastMileContainerToInboundPlans).mockResolvedValue(originalResourcePlans as never)
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
await button(wrapper, '重试生成托盘码')!.trigger('click')
await flushPromises()
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(false)
expect(cabinetDialog(wrapper).props('confirmText')).toBe('重试资源确认')
cabinetDialog(wrapper).vm.$emit('confirm')
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({
sourceContainerBookingId: bookingId, snapshotVersion: 9, snapshotHash: 'a'.repeat(64),
}, 'original-container-command')
})
it('reopens a complete local plan and confirms only the original resource key/version/hash without showing zero generation', async () => {
const success = vi.spyOn(ElMessage, 'success').mockImplementation(() => ({ close: vi.fn() } as never))
vi.mocked(getContainerInboundPlanPreview)
.mockResolvedValueOnce(resourceCommittedPreview)
.mockResolvedValueOnce(resourceCommittedPreview)
.mockResolvedValue({ ...resourceCommittedPreview, resourceRetry: undefined })
vi.mocked(convertLastMileContainerToInboundPlans).mockResolvedValue(originalResourcePlans as never)
const wrapper = await openAfterDeparture()
await openContainerDialog(wrapper); await chooseContainer(wrapper)
cabinetDialog(wrapper).vm.$emit('update:modelValue', false); await flushPromises()
await openContainerDialog(wrapper); await chooseContainer(wrapper)
const dialog = cabinetDialog(wrapper)
expect(dialog.props('confirmDisabled')).toBe(false)
expect(dialog.props('confirmText')).toBe('重试资源确认')
expect(document.body.textContent).toContain('资源仍待确认')
expect(document.body.textContent).not.toContain('最终生成 0 个托盘')
expect(document.body.textContent).not.toContain('最终 0 托')
dialog.vm.$emit('confirm'); await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledExactlyOnceWith({
sourceContainerBookingId: bookingId, snapshotVersion: 9, snapshotHash: 'a'.repeat(64),
}, 'original-container-command')
expect(document.body.textContent).toContain('资源确认完成')
expect(document.body.textContent).toContain('PLT-ORIGINAL')
expect(cabinetDialog(wrapper).props('confirmText')).toBe('已确认')
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
expect(success).toHaveBeenCalledWith('已完成 1 个已有计划的资源确认')
expect(createManualInboundPlan).not.toHaveBeenCalled()
})
it('keeps ordinary zero-pending previews without resourceRetry disabled in both UI and handler', async () => {
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({ ...resourceCommittedPreview, resourceRetry: undefined })
const wrapper = await openAfterDeparture(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
expect(cabinetDialog(wrapper).props('confirmText')).toBe('确认生成')
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
})
it('uses the visible missing-container label in the preview tooltip without exposing the booking ID', async () => {
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({ ...preview, containerNo: null } as never)
const wrapper = await openAfterDeparture(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
const identity = document.body.querySelector('.container-preview__identity')!
expect(identity.textContent).toBe('未录入货柜号')
expect(identity.getAttribute('title')).toBe('未录入货柜号')
expect(identity.getAttribute('title')).not.toContain(bookingId)
})
it('keeps server readiness blocks effective even for a resource retry', async () => {
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({ ...resourceCommittedPreview, ready: false, blockedOrderCount: 1 })
const wrapper = await openAfterDeparture(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
})
it('guards resource retry at the handler when the caller lacks create permission', async () => {
canAccess.mockImplementation(code => code !== 'platform:overseas-warehouse:inbound-order:create')
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue(resourceCommittedPreview)
const wrapper = await openAfterDeparture()
const view = wrapper.vm as unknown as { openContainerConversion(): void }
view.openContainerConversion(); await flushPromises(); await chooseContainer(wrapper)
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
})
it('keeps preview and candidate loading guards effective for recovery commands', async () => {
let resolvePreview!: (value: typeof resourceCommittedPreview) => void
let resolveOptions!: (value: Awaited<ReturnType<typeof listContainerPlanProgress>>) => void
vi.mocked(getContainerInboundPlanPreview).mockReturnValue(new Promise(resolve => { resolvePreview = resolve }))
vi.mocked(listContainerPlanProgress).mockReturnValue(new Promise(resolve => { resolveOptions = resolve }))
const wrapper = await openAfterDeparture(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
resolvePreview(resourceCommittedPreview); await flushPromises()
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
resolveOptions({ records: [summary], page: 1, size: 50, total: 1, pages: 1 }); await flushPromises()
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(false)
})
it('preserves the original recovery command on failure and blocks duplicate or completed submissions', async () => {
const success = vi.spyOn(ElMessage, 'success').mockImplementation(() => ({ close: vi.fn() } as never))
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue(resourceCommittedPreview)
vi.mocked(convertLastMileContainerToInboundPlans).mockRejectedValueOnce(new ApiResponseError('资源确认失败', 'S0503', 503))
const wrapper = await openAfterDeparture(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
expect(cabinetDialog(wrapper).props('modelValue')).toBe(true)
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(false)
expect(success).not.toHaveBeenCalled()
let resolveCommand!: (value: Awaited<ReturnType<typeof convertLastMileContainerToInboundPlans>>) => void
vi.mocked(convertLastMileContainerToInboundPlans).mockReturnValueOnce(new Promise(resolve => { resolveCommand = resolve }))
cabinetDialog(wrapper).vm.$emit('confirm'); cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(2)
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
resolveCommand(originalResourcePlans as never); await flushPromises()
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(2)
for (const args of vi.mocked(convertLastMileContainerToInboundPlans).mock.calls) expect(args).toEqual([
{ sourceContainerBookingId: bookingId, snapshotVersion: 9, snapshotHash: 'a'.repeat(64) }, 'original-container-command',
])
})
it('renders the all-cabinet empty state and unit', async () => {
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [], page: 1, size: 10, total: 0, pages: 0 })
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
expect(wrapper.text()).toContain('暂无货柜数据')
expect(wrapper.text()).not.toContain('已全部生成计划的柜不再展示')
})
2026-09-02 14:39:34 +08:00
it('uses independent cabinet pagination and opens the complete long-id detail in a dialog', async () => {
2026-08-28 13:54:44 +08:00
vi.mocked(listContainerPlanProgress).mockResolvedValue({ records: [summary], page: 1, size: 10, total: 21, pages: 3 })
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-09-02 14:39:34 +08:00
const pagination = wrapper.findComponent({ name: 'ElPagination' })
pagination.vm.$emit('current-change', 2)
2026-08-28 13:54:44 +08:00
await flushPromises()
expect(listContainerPlanProgress).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2, size: 10 }))
2026-09-02 15:15:47 +08:00
await wrapper.get('.container-workbench-card__detail').trigger('click')
2026-08-28 13:54:44 +08:00
await flushPromises()
expect(getContainerPlanProgress).toHaveBeenCalledWith(bookingId)
2026-09-02 14:39:34 +08:00
expect(document.body.textContent).toContain('柜子详情 · CONT-15')
2026-08-28 13:54:44 +08:00
expect(document.body.textContent).toContain('LMO-1')
expect(document.body.textContent).toContain('IN-1')
expect(document.body.textContent).toContain('LIP-1')
expect(document.body.textContent).toContain('已生成入库计划')
expect(document.body.textContent).toContain('待收货')
expect(document.body.textContent).not.toContain('INBOUND_PLANNED')
expect(document.body.textContent).not.toContain('PENDING_RECEIVING')
2026-09-02 14:39:34 +08:00
expect(document.body.querySelector('.el-dialog')).not.toBeNull()
expect(document.body.querySelector('.el-drawer')).toBeNull()
2026-08-28 13:54:44 +08:00
})
2026-09-02 15:15:47 +08:00
it('starts cabinet plan generation directly from the workbench card', async () => {
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-10-19 17:56:31 +08:00
await button(wrapper, '入库')!.trigger('click')
2026-09-02 15:15:47 +08:00
await flushPromises()
expect(document.body.textContent).toContain('头程已有 4 个托盘码,海外仓补充 1 个,最终生成 5 个托盘')
expect(document.body.textContent).toContain('头程已有 4 托 / 海外仓补充 1 托')
2026-09-09 00:44:07 +08:00
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
2026-09-02 15:15:47 +08:00
expect(confirm.disabled).toBe(false)
confirm.click()
await flushPromises()
2026-09-09 00:44:07 +08:00
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({
sourceContainerBookingId: bookingId,
snapshotVersion: 9,
snapshotHash: 'a'.repeat(64),
}, expect.stringContaining('container-inbound-plan-'))
2026-09-02 15:15:47 +08:00
expect(getContainerPlanProgress).not.toHaveBeenCalled()
})
2026-09-07 16:36:21 +08:00
it('keeps an invalid historical cabinet readable while blocking plan generation', async () => {
const invalid = {
...summary,
overseasWarehouseId: null,
overseasWarehouseCode: null,
overseasWarehouseName: null,
blockingReason: '柜内接柜仓业务快照不一致',
}
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [invalid], page: 1, size: 10, total: 1, pages: 1 } as never)
vi.mocked(getContainerPlanProgress).mockResolvedValueOnce({ summary: invalid, orders: [] } as never)
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-09-07 16:36:21 +08:00
expect(wrapper.text()).toContain('柜内接柜仓业务快照不一致')
2026-10-19 17:56:31 +08:00
expect((button(wrapper, '入库')!.element as HTMLButtonElement).disabled).toBe(true)
2026-09-07 16:36:21 +08:00
await wrapper.get('.container-workbench-card__detail').trigger('click')
await flushPromises()
expect(getContainerPlanProgress).toHaveBeenCalledWith(bookingId)
expect(document.body.textContent).toContain('柜子详情 · CONT-15')
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
})
2026-09-07 15:07:39 +08:00
it('shows the receiving warehouse and changes it from ACTIVE OVERSEAS master data with versioning', async () => {
vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([currentReceivingWarehouse, receivingWarehouse] as never)
2026-09-07 15:07:39 +08:00
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-09-07 15:07:39 +08:00
expect(wrapper.text()).toContain('海外接柜仓')
expect(wrapper.text()).toContain('LAX01 · 洛杉矶接柜仓')
expect(wrapper.get('.container-workbench-card__warehouse strong').attributes('title')).toBe('LAX01 · 洛杉矶接柜仓')
expect(listAllPlatformWarehouses).not.toHaveBeenCalled()
2026-09-07 15:07:39 +08:00
await button(wrapper, '修改接柜仓')!.trigger('click')
await flushPromises()
const receivingDialog = wrapper.findAllComponents(AppDialog).find((item) => item.props('title') === '修改海外接柜仓')!
expect(receivingDialog.props('width')).toBe('min(600px, calc(100vw - 28px))')
2026-09-07 15:07:39 +08:00
expect(listAllPlatformWarehouses).toHaveBeenLastCalledWith({ warehouseType: 'OVERSEAS', status: 'ACTIVE' })
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
2026-09-07 15:07:39 +08:00
const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择 ACTIVE 海外接柜仓')!
expect(warehouseSelect.props('modelValue')).toBe('')
expect(wrapper.text()).not.toContain('9007199254740995')
const warehouseOptions = wrapper.findAllComponents({ name: 'ElOption' })
expect(warehouseOptions.find((item) => item.props('value') === summary.overseasWarehouseId)?.props('disabled')).toBe(true)
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement
warehouseSelect.vm.$emit('update:modelValue', summary.overseasWarehouseId)
await wrapper.vm.$nextTick()
expect(confirm.disabled).toBe(true)
expect(wrapper.text()).toContain('新的海外接柜仓必须与当前仓不同')
2026-09-07 15:07:39 +08:00
warehouseSelect.vm.$emit('update:modelValue', '9007199254740996')
await wrapper.vm.$nextTick()
expect(confirm.disabled).toBe(false)
2026-09-07 15:07:39 +08:00
confirm.click()
await flushPromises()
expect(changeContainerReceivingWarehouse).toHaveBeenCalledWith(bookingId, {
overseasWarehouseId: '9007199254740996', expectedVersion: 7, remark: undefined,
}, expect.stringMatching(/^container-receiving-warehouse-/))
expect(listContainerPlanProgress).toHaveBeenCalledTimes(2)
})
it('keeps the receiving-warehouse confirmation disabled while candidates are loading', async () => {
let resolveCandidates!: (value: typeof receivingWarehouse[]) => void
vi.mocked(listAllPlatformWarehouses).mockReturnValueOnce(new Promise((resolve) => { resolveCandidates = resolve }) as never)
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
await button(wrapper, '修改接柜仓')!.trigger('click')
await wrapper.vm.$nextTick()
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement
expect(confirm.disabled).toBe(true)
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
resolveCandidates([receivingWarehouse])
await flushPromises()
expect(confirm.disabled).toBe(true)
2026-09-07 15:07:39 +08:00
})
it('keeps a disabled receiving-warehouse action discoverable when the server locks the container', async () => {
2026-09-07 15:07:39 +08:00
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({
records: [{ ...summary, canChangeReceivingWarehouse: false, receivingWarehouseLockReason: '海外仓已开始拆柜收货' }],
page: 1, size: 10, total: 1, pages: 1,
})
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-09-07 15:07:39 +08:00
expect(wrapper.text()).toContain('海外仓已开始拆柜收货')
const changeButton = button(wrapper, '修改接柜仓')!
expect(changeButton.exists()).toBe(true)
expect((changeButton.element as HTMLButtonElement).disabled).toBe(true)
expect(changeButton.attributes('title')).toBe('海外仓已开始拆柜收货')
expect(changeContainerReceivingWarehouse).not.toHaveBeenCalled()
})
it('keeps a disabled receiving-warehouse action discoverable without edit permission', async () => {
canAccess.mockImplementation((permissionCode: string) => permissionCode !== 'platform:overseas-warehouse:last-mile-order:edit')
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
const changeButton = button(wrapper, '修改接柜仓')!
expect(changeButton.exists()).toBe(true)
expect((changeButton.element as HTMLButtonElement).disabled).toBe(true)
expect(changeButton.attributes('title')).toBe('暂无修改接柜仓权限,请联系管理员开通尾程订单编辑权限')
await changeButton.trigger('click')
expect(listAllPlatformWarehouses).not.toHaveBeenCalled()
2026-09-07 15:07:39 +08:00
expect(changeContainerReceivingWarehouse).not.toHaveBeenCalled()
})
it('keeps conflict metadata visible and refreshes the stale container summary', async () => {
vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([receivingWarehouse] as never)
vi.mocked(changeContainerReceivingWarehouse).mockRejectedValueOnce(new ApiResponseError(
'version conflict', 'A0409', 200, undefined, 'request-receiving', 'trace-receiving',
))
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-09-07 15:07:39 +08:00
await button(wrapper, '修改接柜仓')!.trigger('click')
await flushPromises()
const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择 ACTIVE 海外接柜仓')!
warehouseSelect.vm.$emit('update:modelValue', '9007199254740996')
await wrapper.vm.$nextTick()
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement
confirm.click()
await flushPromises()
expect(wrapper.text()).toContain('接柜仓信息已被其他操作更新,请刷新后重试')
expect(wrapper.text()).toContain('request-receiving')
expect(wrapper.text()).toContain('trace-receiving')
expect(listContainerPlanProgress).toHaveBeenCalledTimes(2)
})
it('renders an explicit empty state when there are no ACTIVE OVERSEAS receiving warehouses', async () => {
vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([])
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-09-07 15:07:39 +08:00
await button(wrapper, '修改接柜仓')!.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('暂无可用的 ACTIVE 海外接柜仓')
expect(changeContainerReceivingWarehouse).not.toHaveBeenCalled()
})
it('keeps receiving-warehouse candidate failure metadata visible without an FBA fallback', async () => {
vi.mocked(listAllPlatformWarehouses).mockRejectedValueOnce(new ApiResponseError(
'receiving warehouse candidate failed', 'A0503', 200, undefined, 'request-receiving-options', 'trace-receiving-options',
))
const wrapper = await mountView()
await openHiddenContainerWorkbench(wrapper)
2026-09-07 15:07:39 +08:00
await button(wrapper, '修改接柜仓')!.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('request-receiving-options')
expect(document.body.textContent).toContain('trace-receiving-options')
expect(document.body.textContent).not.toContain('请选择目的 FBA 仓')
expect(changeContainerReceivingWarehouse).not.toHaveBeenCalled()
})
it('keeps dense cabinet fields and actions on one line with ellipsis at 1920px', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/LastMileOrdersView.vue'), 'utf8')
expect(source).toContain('.container-detail-summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }')
2026-10-20 23:34:11 +08:00
expect(source).toContain('.container-workbench-card__warehouse strong { min-width: 0; overflow: hidden; color: var(--workbench-text); font-size: var(--admin-font-section); text-overflow: ellipsis; white-space: nowrap; }')
2026-09-14 20:03:27 +08:00
expect(source).toContain('.container-workbench-card__actions { display: flex; justify-content: flex-end; padding-top: 2px; white-space: nowrap; }')
2026-09-14 20:21:02 +08:00
expect(source).toContain("{ key: 'actions', label: '操作', width: 330, fixed: 'left'")
})
2026-08-28 13:54:44 +08:00
it('loads only active FBA master data and submits exact string fields for a cross-customer transfer', async () => {
const wrapper = await openAfterDeparture()
2026-08-28 13:54:44 +08:00
await selectRows(wrapper, [selectedRow(), selectedRow({ id: '2092873463568596995', customerId: '2092618717861048322' })])
await button(wrapper, '转仓')!.trigger('click')
await flushPromises()
expect(listAllPlatformWarehouses).toHaveBeenCalledWith({ warehouseType: 'FBA', status: 'ACTIVE' })
const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择目的 FBA 仓')!
expect(warehouseSelect).toBeDefined()
const option = wrapper.findAllComponents({ name: 'ElOption' }).find((item) => item.props('value') === '9007199254740993')!
expect(option.props('label')).toBe('ONT8 · Ontario FBA · CA / Ontario')
warehouseSelect.vm.$emit('update:modelValue', '9007199254740993')
await wrapper.vm.$nextTick()
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认转仓')) as HTMLButtonElement
expect(confirm.disabled).toBe(false)
confirm.click()
await flushPromises()
expect(transferLastMileOrders).toHaveBeenCalledWith({
orderIds: ['2092873463568596994', '2092873463568596995'],
warehouseId: '9007199254740993', warehouseCode: 'ONT8', warehouseName: 'Ontario FBA', remark: undefined,
})
expect(vi.mocked(transferLastMileOrders).mock.calls[0]?.[0].warehouseId).toBeTypeOf('string')
expect(wrapper.html()).not.toContain('请输入目的仓 ID')
})
it('clears all selected warehouse facts when the FBA candidate is cleared', async () => {
const wrapper = await openAfterDeparture()
2026-08-28 13:54:44 +08:00
await selectRows(wrapper, [selectedRow()])
await button(wrapper, '转仓')!.trigger('click')
await flushPromises()
const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择目的 FBA 仓')!
warehouseSelect.vm.$emit('update:modelValue', '9007199254740993')
await wrapper.vm.$nextTick()
warehouseSelect.vm.$emit('update:modelValue', '')
await wrapper.vm.$nextTick()
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认转仓')) as HTMLButtonElement
expect(confirm.disabled).toBe(true)
expect(transferLastMileOrders).not.toHaveBeenCalled()
})
it('renders an explicit empty state when there are no active FBA candidates', async () => {
vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([])
const wrapper = await openAfterDeparture()
2026-08-28 13:54:44 +08:00
await selectRows(wrapper, [selectedRow()])
await button(wrapper, '转仓')!.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('暂无可用 FBA 仓候选')
expect(transferLastMileOrders).not.toHaveBeenCalled()
})
it('keeps candidate load request metadata visible and prevents free-form fallback', async () => {
vi.mocked(listAllPlatformWarehouses).mockRejectedValueOnce(new ApiResponseError(
'FBA candidate failed', 'A0503', 200, undefined, 'request-fba', 'trace-fba',
))
const wrapper = await openAfterDeparture()
2026-08-28 13:54:44 +08:00
await selectRows(wrapper, [selectedRow()])
await button(wrapper, '转仓')!.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('A0503')
expect(document.body.textContent).toContain('request-fba')
expect(document.body.textContent).toContain('trace-fba')
expect(wrapper.html()).not.toContain('请输入目的仓 ID')
expect(wrapper.html()).not.toContain('请输入目的仓代码')
expect(wrapper.html()).not.toContain('请输入目的仓名称')
expect(transferLastMileOrders).not.toHaveBeenCalled()
})
it('keeps the return packing entry and excludes return orders from manual inbound generation', async () => {
const wrapper = await mountView()
2026-10-09 18:03:34 +08:00
await selectRows(wrapper, [selectedRow({ orderTypeCode: 'RETURN_RELABEL', currentStatusCode: 'INBOUND_COMPLETED', sourceOrderNo: 'RETURN-1' })])
const actions = wrapper.get('.data-table__business-actions')
2026-10-19 17:56:31 +08:00
expect(actions.text()).not.toContain('入库')
const packing = actions.findAll('button').find(action => action.text().includes('创建退货合箱计划(1)'))!
await packing.trigger('click')
await flushPromises()
2026-10-09 18:03:34 +08:00
expect(routerPush).toHaveBeenCalledWith({ name: 'overseas-return-inventory', query: expect.objectContaining({ packing: '1', returnNos: 'RETURN-1' }) })
})
it('keeps manual-plan drafts through native fullscreen with plain frozen source facts', async () => {
const wrapper = await mountView()
await openArrivalPlan(wrapper)
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('入库计划 ·'))!
expect(dialog.props('width')).toBe('min(1160px, calc(100vw - 48px))')
expect(dialog.findComponent({ name: 'ElDialog' }).props('top')).toBe('6vh')
expect(dialog.get('[aria-label="入库来源"] h3').text()).toContain('来源摘要')
expect(dialog.get('[aria-label="入库计划登记"] h3').text()).toBe('计划设置')
const sourceRows = dialog.findAll('.inbound-plan-source tbody tr')
expect(sourceRows[0].get('td').attributes('colspan')).toBe('3')
expect(sourceRows[1].findAll('th').map(cell => cell.text())).toEqual(['计划入库仓', '入仓码'])
expect(dialog.props('confirmText')).toBe('保存并生成入库单')
expect(dialog.props('title')).toContain('LMO-260827-0001')
expect(dialog.get('[data-tone="source"]').find('input').exists()).toBe(false)
await dialog.get('textarea').setValue('全屏后保留')
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true)
expect((dialog.get('textarea').element as HTMLTextAreaElement).value).toBe('全屏后保留')
})
it('locks manual-plan form edits and preserves a failed draft and request context', async () => {
let reject!: (reason: unknown) => void
vi.mocked(createManualInboundPlan).mockReturnValue(new Promise((_, no) => { reject = no }))
const wrapper = await mountView()
await openArrivalPlan(wrapper)
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('入库计划 ·'))!
await dialog.get('textarea').setValue('保留入库备注')
await dialog.findAll('button').find(item => item.text() === '保存并生成入库单')!.trigger('click'); await flushPromises()
expect(dialog.findComponent({ name: 'ElDialog' }).props()).toMatchObject({ showClose: false, closeOnPressEscape: false })
expect(dialog.get('textarea').attributes('disabled')).toBeDefined()
reject(new ApiResponseError('入库失败', 'TEST_REJECTED', 409, null, 'plan-test-request'))
await flushPromises()
expect((dialog.get('textarea').element as HTMLTextAreaElement).value).toBe('保留入库备注')
expect(dialog.props('modelValue')).toBe(true)
expect(document.body.textContent).toContain('plan-test-request')
})
it.each([false, true])('keeps hold/release flags, reason and zero version for detained=%s', async detained => {
const wrapper = await mountView()
const row = selectedRow({ detained, currentStatusCode: 'INBOUND_COMPLETED', version: 0 })
const vm = wrapper.vm as unknown as { openHold(row: object): void }
vm.openHold(row); await flushPromises()
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith(detained ? '解除暂扣 ·' : '暂扣订单 ·'))!
await dialog.get('textarea').setValue(' 原因 ')
dialog.findAllComponents({ name: 'ElRadioGroup' })[1]!.vm.$emit('update:modelValue', false)
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
await dialog.findAll('button').find(item => item.text() === '保存')!.trigger('click'); await flushPromises()
expect(updateLastMileOrderHold).toHaveBeenCalledWith(row.id, { held: !detained, chargeable: false, reason: '原因', expectedVersion: 0 }, expect.stringContaining('last-mile-hold-'))
})
it('edits only customs nodes while preserving hidden node facts and zero version', async () => {
vi.mocked(getLastMileOrderManualNodes).mockResolvedValue({ orderId: '2092873463568596994', manualOverrideEnabled: true, nodes: [{ nodeCode: 'ACTUAL_DEPARTURE', nodeAt: '2026-10-19T08:00:00' }], version: 0 })
const wrapper = await mountView()
const row = selectedRow()
await (wrapper.vm as unknown as { openManualNodes(row: object): Promise<void> }).openManualNodes(row)
await flushPromises()
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('修改节点 ·'))!
expect(dialog.findAll('.manual-nodes-editor tbody tr').map(row => row.find('th').text())).toEqual(['国内报关完成', '国外清关完成'])
const dates = dialog.findAllComponents({ name: 'ElDatePicker' })
expect(dates).toHaveLength(2)
dates[0]!.vm.$emit('update:modelValue', '2026-10-20T08:00:00')
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
await dialog.findAll('button').find(item => item.text() === '保存')!.trigger('click'); await flushPromises()
expect(updateLastMileOrderManualNodes).toHaveBeenCalledWith(row.id, 0, [
{ nodeCode: 'DOMESTIC_CUSTOMS_COMPLETED', nodeAt: '2026-10-20T08:00:00' },
{ nodeCode: 'ESTIMATED_DEPARTURE', nodeAt: null }, { nodeCode: 'ACTUAL_DEPARTURE', nodeAt: '2026-10-19T08:00:00' },
{ nodeCode: 'ESTIMATED_ARRIVAL', nodeAt: null }, { nodeCode: 'OVERSEAS_CUSTOMS_COMPLETED', nodeAt: null },
{ nodeCode: 'ACTUAL_ARRIVAL_WAITING_UNLOAD', nodeAt: null }, { nodeCode: 'PICKUP_APPOINTED', nodeAt: null }, { nodeCode: 'UNLOADED', nodeAt: null },
], expect.stringContaining('last-mile-manual-nodes-'))
})
it('keeps every destination group in the main preview body through fullscreen', async () => {
const original = await getContainerInboundPlanPreview(bookingId)
const group = original.groups[0]!
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({ ...original, totalOrderCount: 13, readyOrderCount: 13, pendingOrderCount: 13, pendingPalletCount: 13, sourcePalletCount: 13, supplementPalletCount: 0,
groups: Array.from({ length: 13 }, (_, index) => ({ ...group, destinationWarehouseCode: 'FBA-' + index, orderCount: 1, pendingOrderCount: 1, pendingPalletCount: 1, sourcePalletCount: 1, supplementPalletCount: 0,
orders: [{ ...group.orders[0]!, orderId: 'order-' + index, businessOrderNo: '业务单-' + index, sourcePalletCount: 1, targetPalletCount: 1, supplementPalletCount: 0 }] })),
})
const wrapper = await openAfterDeparture()
await openContainerDialog(wrapper); await chooseContainer(wrapper)
const dialog = cabinetDialog(wrapper)
expect(dialog.findAll('[aria-label="目的地分组明细"]')).toHaveLength(13)
expect(dialog.findAll('.el-table__body-wrapper tr')).toHaveLength(13)
expect(dialog.text()).toContain('业务单-12')
expect(dialog.find('.container-preview__scroll').exists()).toBe(false)
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true)
})
it('keeps transfer drafts locked during submission and editable after rejection', async () => {
let reject!: (reason: unknown) => void
vi.mocked(transferLastMileOrders).mockReturnValueOnce(new Promise((_, no) => { reject = no }))
const wrapper = await openAfterDeparture()
await selectRows(wrapper, [selectedRow()])
await button(wrapper, '转仓')!.trigger('click'); await flushPromises()
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('转仓 ·'))!
dialog.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', fbaWarehouse.id)
await dialog.get('textarea').setValue('保留转仓备注')
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
await dialog.findAll('button').find(item => item.text() === '确认转仓')!.trigger('click'); await flushPromises()
expect(dialog.get('textarea').attributes('disabled')).toBeDefined()
reject(new ApiResponseError('转仓失败', 'TEST_REJECTED', 409, null, 'transfer-test-request')); await flushPromises()
expect((dialog.get('textarea').element as HTMLTextAreaElement).value).toBe('保留转仓备注')
expect(dialog.props('modelValue')).toBe(true)
})
it('keeps rework selection and reason through fullscreen without issuing a command', async () => {
const wrapper = await mountView()
;(wrapper.vm as unknown as { reworkRelabel(row: object): void }).reworkRelabel(selectedRow({ relabelOrderNo: 'RL-TEST-1', relabelStatusCode: 'CUSTOMER_CONFIRM_PENDING' }))
await flushPromises()
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('换标退回重做 ·'))!
expect(dialog).toBeDefined()
await dialog.get('textarea').setValue('保留重做原因')
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
expect((dialog.get('textarea').element as HTMLTextAreaElement).value).toBe('保留重做原因')
expect(dialog.findComponent({ name: 'ElSelect' }).props('modelValue')).toBe('RL-TEST-1')
})
})
2026-10-10 14:58:15 +08:00
describe('tail-leg pallet merge toolbar', () => {
beforeEach(() => {
canAccess.mockImplementation(() => true)
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
vi.mocked(updateLastMileOrderPalletMerge).mockReset().mockResolvedValue(null)
})
it('submits the selected group and cancellation with the customer scope', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
const wrapper = await mountView()
const row = { id: '2092873463568596994', customerId, currentStatusCode: 'PENDING_ASSIGNMENT', palletMergeFlag: '' }
expect(wrapper.findAll('button').find(b => b.text() === '合托')!.attributes('disabled')).toBeDefined()
await selectRows(wrapper, [row])
await wrapper.findAll('button').find(b => b.text() === '合托')!.trigger('click')
await flushPromises()
expect(updateLastMileOrderPalletMerge).toHaveBeenLastCalledWith([row.id], customerId, true)
await selectRows(wrapper, [{ ...row, palletMergeFlag: 'REQUIRED' }])
await wrapper.findAll('button').find(b => b.text() === '取消合托')!.trigger('click')
await flushPromises()
expect(updateLastMileOrderPalletMerge).toHaveBeenLastCalledWith([row.id], customerId, false)
})
it('blocks completed and mixed-customer selections and hides unauthorized actions', async () => {
const wrapper = await mountView()
for (const rows of [
[{ id: '1', customerId, currentStatusCode: 'INBOUND_COMPLETED', palletMergeFlag: 'REQUIRED' }],
[{ id: '1', customerId, palletMergeFlag: 'MERGED' }],
[{ id: '1', customerId }, { id: '2', customerId: 'other' }],
]) {
await selectRows(wrapper, rows)
expect(wrapper.findAll('button').find(b => b.text() === '合托')!.attributes('disabled')).toBeDefined()
expect(wrapper.findAll('button').find(b => b.text() === '取消合托')!.attributes('disabled')).toBeDefined()
}
canAccess.mockImplementation(() => false)
const denied = await mountView()
expect(denied.findAll('button').some(b => b.text() === '合托' || b.text() === '取消合托')).toBe(false)
})
})