Files
admin-web/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts
T

1270 lines
73 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ElementPlus, { ElCheckboxButton, ElInputNumber, ElMessage, ElMessageBox, ElOption, ElSelect } from 'element-plus'
import { AxiosError } from 'axios'
import { createPinia, setActivePinia } from 'pinia'
import { DOMWrapper, enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import GlobalSmartLoadingView from './GlobalSmartLoadingView.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import SmartLoadingDialog from '../components/SmartLoadingDialog.vue'
import { loadSmartLoadingPlanCoLoadGroups } from '../smartLoadingPlanGroups'
import type { ContainerBookingResource } from '../types'
import {
assignPendingReviewSmartLoadingOrder,
assignSmartLoadingGroup,
assignSmartLoadingItem,
assignSmartLoadingPallet,
getSmartLoadingPlanByBooking,
listContainers,
listSmartLoadingCandidates,
listSmartLoadingWorkbenchContainers,
recommendSmartLoading,
adjustSmartLoadingItem,
removeAllUnpickedSmartLoadingItems,
removeSmartLoadingGroup,
removeSmartLoadingItem,
removeSmartLoadingPallet,
} from '../api/platformContainerLoading'
enableAutoUnmount(afterEach)
const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() }))
vi.mock('vue-router', () => ({
useRouter: () => ({ push: routerPush }),
}))
vi.mock('@/core/access/accessPreview', () => ({
useAccessPreviewStore: () => ({ can: () => true }),
}))
vi.mock('@/modules/iam', async importOriginal => ({
...await importOriginal<typeof import('@/modules/iam')>(),
listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }),
}))
vi.mock('../api/platformContainerLoading', () => ({
listContainers: vi.fn(),
getSmartLoadingPlanByBooking: vi.fn(),
listSmartLoadingCandidates: vi.fn(),
listSmartLoadingWorkbenchContainers: vi.fn(),
createSmartLoadingPlan: vi.fn(),
assignSmartLoadingItem: vi.fn(),
assignSmartLoadingPallet: vi.fn(),
assignPendingReviewSmartLoadingOrder: vi.fn(),
assignSmartLoadingGroup: vi.fn(),
recommendSmartLoading: vi.fn(),
adjustSmartLoadingItem: vi.fn(),
removeSmartLoadingItem: vi.fn(),
removeSmartLoadingGroup: vi.fn(),
removeSmartLoadingPallet: vi.fn(),
removeAllUnpickedSmartLoadingItems: vi.fn(),
pushSmartLoadingPreload: vi.fn(),
}))
vi.mock('@/modules/warehouses', () => ({
listAllPlatformWarehouses: vi.fn().mockResolvedValue([
{ id: '8', warehouseName: '义乌仓', warehouseType: 'DOMESTIC', status: 'ACTIVE' },
{ id: '18', warehouseName: '东莞仓', warehouseType: 'DOMESTIC', status: 'ACTIVE' },
]),
}))
vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockReturnValue(new Map()) }))
const container = {
id: '9007199254740993', planId: '9007199254740994', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
warehouseId: '8', warehouseName: '义乌仓', channelGroupCode: 'MATSON_CLX', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HQ', planStatus: 'EDITING' as const, version: 1,
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: '31', shippingCompanyName: '美森轮船',
departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null,
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 68, maxWeightKg: 19500,
terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null,
estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null,
}
const plan = {
id: '99', bookingId: container.id, planNo: 'LP-99', status: 'EDITING' as const, containerNo: 'MSCU-1', voyageNo: null, version: 2,
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED' as const,
pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null,
coLoadGroups: [],
palletMembers: [],
items: [{ id: '101', loadingPlanId: '99', accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', customerName: '示例企业', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', skuCode: 'SKU-1', productNameZh: '服装', unitType: 'PACKAGE' as const, palletId: null, palletNo: null, indivisible: false, packageCount: 6, productQuantity: 6, grossWeightKg: 60, volumeCbm: 1.2, loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true }],
}
const candidate = {
accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2',
orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00',
warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9',
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类',
availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2,
availableVolumeCbm: 2.4, estimatedVolumeCbm: 3.5, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null,
locked: null, blockReason: null,
}
function candidatePage(records: unknown[], page = 1, size = 20, total = records.length) {
return {
records: records.map((record) => {
const candidate = record as { accountId?: string; customerId?: string; groupMembers?: unknown[] }
return { customerId: candidate.customerId || candidate.accountId || '', groupMembers: [], unitType: 'PACKAGE', palletId: null, palletNo: null, indivisible: false, ...candidate }
}),
page, size, total, pages: total ? Math.ceil(total / size) : 0,
} as Awaited<ReturnType<typeof listSmartLoadingCandidates>>
}
function containerPage(records: ContainerBookingResource[] = [container]) {
return {
records: records.map((item) => ({
bookingId: item.id, cabinetNo: item.cabinetNo, containerNo: item.containerNo,
warehouseId: item.warehouseId, warehouseName: item.warehouseName || '', channelGroupCode: item.channelGroupCode,
channelCode: item.channelCode, channelName: item.channelName,
containerType: item.containerType, capacityCbm: item.capacityCbm, maxWeightKg: item.maxWeightKg,
planId: item.planId, planNo: null, planStatus: item.planStatus, version: item.version,
})),
page: 1, size: 200, total: records.length, pages: records.length ? 1 : 0,
} as Awaited<ReturnType<typeof listSmartLoadingWorkbenchContainers>>
}
function mockContainerSearch(records: ContainerBookingResource[]) {
vi.mocked(listSmartLoadingWorkbenchContainers).mockImplementation(async (query) => containerPage(records.filter((item) => (
item.planStatus !== 'COMPLETED'
&& query.channelGroupCodes.includes(item.channelGroupCode)
))))
}
function assignmentDialog() {
const root = document.body.querySelector<HTMLElement>('.smart-loading-assignment-dialog')
if (!root) throw new Error('assignment dialog is missing')
return new DOMWrapper(root)
}
describe('GlobalSmartLoadingView', () => {
it('mounts assignment outside the parent workbench and keeps fullscreen and reopen states independent', async () => {
const wrapper = mount(SmartLoadingDialog, { attachTo: document.body, props: { modelValue: true }, global: { plugins: [ElementPlus] } })
try {
await flushPromises()
const workbench = wrapper.getComponent(GlobalSmartLoadingView)
const openAssignment = async () => {
await workbench.get('.candidate-pane .el-table__body-wrapper').findAll('button').find(button => button.text() === '加入')!.trigger('click')
await flushPromises()
}
await openAssignment()
const popup = workbench.getComponent(AppDialog)
const outer = document.querySelector('.smart-loading-dialog')!
const child = assignmentDialog()
expect(document.body.contains(child.element)).toBe(true)
expect(outer.contains(child.element)).toBe(false)
expect(child.element.closest('.global-loading-page')).toBeNull()
expect(child.classes()).toEqual(expect.arrayContaining(['admin-density-dialog', 'admin-workbench', 'is-dialog']))
popup.getComponent(ElInputNumber).vm.$emit('update:modelValue', 4)
await child.get('[aria-label="展开全屏"]').trigger('click')
expect(child.classes()).toContain('is-fullscreen')
expect(outer.classList.contains('is-fullscreen')).toBe(true)
await child.findAll('button').find(button => button.text() === '取消')!.trigger('click')
await flushPromises()
expect(popup.props('modelValue')).toBe(false)
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
expect(workbench.get('.container-option.selected').attributes('aria-pressed')).toBe('true')
await openAssignment()
expect(popup.props('modelValue')).toBe(true)
expect(assignmentDialog().classes()).not.toContain('is-fullscreen')
expect(popup.getComponent(ElInputNumber).props('modelValue')).toBe(candidate.availablePackageCount)
expect(outer.classList.contains('is-fullscreen')).toBe(true)
expect(assignSmartLoadingItem).not.toHaveBeenCalled()
} finally {
wrapper.unmount()
}
})
it.each(['PACKAGE', 'SEALED_PALLET'] as const)('分配窗口全屏保留来源及输入,按%s原接口提交', async (unitType) => {
if (unitType === 'SEALED_PALLET') {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
...candidate, unitType, palletId: '801', palletNo: 'DP-TEST-1', indivisible: true,
groupMembers: [
{ accountId: '1', inboundId: '180', orderId: '180', orderNo: 'O180', inboundCode: 'I180', customerName: '客户甲', availablePackageCount: 3, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
{ accountId: '1', inboundId: '181', orderId: '181', orderNo: 'O181', inboundCode: 'I181', customerName: '客户甲', availablePackageCount: 9, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
],
}]))
}
vi.mocked(assignSmartLoadingItem).mockResolvedValue(plan)
vi.mocked(assignSmartLoadingPallet).mockResolvedValue(plan)
const wrapper = mount(GlobalSmartLoadingView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.get('.candidate-pane .el-table__body-wrapper').findAll('button').find(button => button.text() === '加入')!.trigger('click')
await flushPromises()
const popup = wrapper.getComponent(AppDialog)
expect(assignmentDialog().get('table[aria-label="全局装柜分配来源"]').text()).toContain('CB-1')
if (unitType === 'PACKAGE') {
const input = popup.getComponent(ElInputNumber)
expect(input.props('modelValue')).toBe(candidate.availablePackageCount)
expect(input.props('max')).toBe(candidate.availablePackageCount)
input.vm.$emit('update:modelValue', 4)
} else {
expect(popup.findComponent(ElInputNumber).exists()).toBe(false)
expect(assignmentDialog().get('details').text()).toContain('I181:9 箱')
expect(popup.props('title')).toBe('整托加入当前柜次 · DP-TEST-1')
}
await assignmentDialog().get('[aria-label="展开全屏"]').trigger('click')
expect(assignmentDialog().classes()).toContain('is-fullscreen')
if (unitType === 'PACKAGE') expect(popup.getComponent(ElInputNumber).props('modelValue')).toBe(4)
popup.vm.$emit('confirm'); await flushPromises()
if (unitType === 'PACKAGE') {
expect(assignSmartLoadingItem).toHaveBeenCalledWith(plan.id, { inboundId: candidate.inboundId, packageCount: 4, expectedVersion: plan.version })
expect(assignSmartLoadingPallet).not.toHaveBeenCalled()
} else {
expect(assignSmartLoadingPallet).toHaveBeenCalledWith(plan.id, { palletId: '801', expectedVersion: plan.version })
expect(assignSmartLoadingItem).not.toHaveBeenCalled()
}
expect(popup.props('modelValue')).toBe(false)
wrapper.unmount()
})
it('uses one-line shared text clamps for block reasons and assigned-count summaries', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('<LineClampText v-if="row.blockReason" class="candidate-block" :content="row.blockReason" />')
expect(source).toContain('<LineClampText v-else-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" />')
expect(source).toContain("row.unitType === 'SEALED_PALLET'")
expect(source).toContain('.candidate-group { display: grid; width: 100%; min-width: 0;')
expect(source).toContain('.candidate-group :deep(.line-clamp-text) { width: 100%; }')
expect(source).not.toContain('<span v-for="summary in row.memberOrderSummaries"')
})
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [], page: 1, size: 50, total: 0, pages: 0 })
mockContainerSearch([container])
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan)
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([candidate]))
})
it('loads real containers, candidates and the selected plan into the three-column workbench', async () => {
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.container-option.selected').attributes('aria-pressed')).toBe('true')
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
expect(wrapper.text()).toContain('候选装柜明细')
expect(wrapper.text()).toContain('美森正班')
expect(wrapper.findAll('.channel-tags .el-checkbox-button')).toHaveLength(9)
expect(wrapper.text()).not.toContain('美森正班海卡包税')
expect(wrapper.text()).toContain('另一企业')
expect(wrapper.text()).toContain('MSCU-1 装柜方案')
expect(wrapper.text()).not.toContain('LP-99')
expect(wrapper.text()).toContain('示例企业')
expect(wrapper.text()).not.toContain('服装')
expect(wrapper.text()).not.toContain('鞋类')
expect(wrapper.text()).not.toContain('SKU-1')
expect(wrapper.text()).toContain('IN-1')
expect(wrapper.text()).toContain('IN-2')
expect(wrapper.text()).toContain('预估体积')
expect(wrapper.text()).toContain('3.5 m³')
expect(wrapper.text()).not.toContain('ORD-1')
expect(wrapper.text()).not.toContain('ORD-2')
expect(wrapper.get('.candidate-pane').text()).toContain('120 kg')
expect(wrapper.get('.candidate-pane').text()).toContain('2.4 m³')
})
it('uses the top-level container channel snapshot without expecting a nested snapshot', async () => {
vi.mocked(listContainers).mockResolvedValue({
records: [{ ...container, shippingCompanyId: '32', shippingCompanyName: 'EXX', channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁' }],
page: 1,
size: 100,
total: 1,
pages: 1,
})
mockContainerSearch([container])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('美森正班')
expect(wrapper.text()).not.toContain('美森正班海卡包税')
expect(wrapper.text()).not.toContain('缺少渠道快照')
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
})
it('shows one user-facing container number and hides internal manual or split references', async () => {
vi.mocked(listContainers).mockResolvedValue({
records: [{ ...container, cabinetNo: 'CAB-260814-0001', containerNo: 'SPLIT-814022117898chain1' }],
page: 1,
size: 100,
total: 1,
pages: 1,
})
mockContainerSearch([{ ...container, cabinetNo: 'CAB-260814-0001', containerNo: 'SPLIT-814022117898chain1' }])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('CAB-260814-0001')
expect(wrapper.text()).not.toContain('SPLIT-814022117898chain1')
expect(wrapper.text()).not.toContain('LP-99')
})
it('explains simulated empty cabinets instead of presenting an internal id as a container number', async () => {
vi.mocked(listContainers).mockResolvedValue({
records: [{
...container,
cabinetNo: '2088475410134773762',
containerNo: null,
channelName: '美森正班海卡包税',
warehouseName: '义乌国内仓',
containerType: '40HQ',
}],
page: 1,
size: 100,
total: 1,
pages: 1,
})
mockContainerSearch([{ ...container, cabinetNo: '2088475410134773762', containerNo: null, channelName: '美森正班海卡包税', warehouseName: '义乌国内仓', containerType: '40HQ' }])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const option = wrapper.find('.container-option')
expect(option.text()).toContain('模拟柜次')
expect(option.get('.container-option__top strong').text()).toBe('待生成')
expect(option.text()).not.toContain('空柜')
expect(option.text()).toContain('集装箱:待补录')
expect(option.text()).not.toContain('开始装柜前录入集装箱号')
expect(option.text()).toContain('渠道:美森正班')
expect(option.get('.container-option__meta').text()).toContain('义乌国内仓')
expect(option.get('.container-option__meta').text()).toContain('40HQ')
expect(option.text()).not.toContain('2088475410134773762')
expect(wrapper.text()).not.toContain('真实柜号')
})
it('keeps nine fixed channel tags and allows selecting multiple channels', async () => {
const other = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
containerNo: 'MSCU-2',
channelGroupCode: 'HEDE',
channelCode: 'HDHK',
channelName: '合德海卡包税',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, other], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, other])
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).not.toContain('MSCU-2')
expect(wrapper.text()).toContain('柜次1 个')
expect(wrapper.findAll('.channel-tags .el-checkbox-button').map((item) => item.text())).toEqual([
'美森正班', '美森加班', 'EXX', '合德', '洛杉矶普船', '芝加哥普船', '休斯顿普船', '萨凡纳普船', '纽约普船海卡',
])
const hedeChannel = wrapper.findAllComponents(ElCheckboxButton).find((item) => item.text() === '合德')!
await hedeChannel.find('input').setValue(true)
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(2)
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).toContain('MSCU-2')
expect(wrapper.text()).toContain('柜次2 个')
})
it('selects only the double-clicked channel as a shortcut', async () => {
const other = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
containerNo: 'MSCU-2',
channelGroupCode: 'HEDE',
channelCode: 'HDHK',
channelName: '合德海卡包税',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, other], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, other])
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const hedeChannel = wrapper.findAllComponents(ElCheckboxButton).find((item) => item.text() === '合德')!
await hedeChannel.find('input').setValue(true)
await flushPromises()
expect(wrapper.findAll('.channel-tags .el-checkbox-button.is-checked')).toHaveLength(2)
await hedeChannel.trigger('dblclick')
await flushPromises()
expect(wrapper.findAll('.channel-tags .el-checkbox-button.is-checked').map((item) => item.text())).toEqual(['合德'])
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(expect.objectContaining({ channelGroupCodes: ['HEDE'] }))
expect(listSmartLoadingWorkbenchContainers).toHaveBeenLastCalledWith({ channelGroupCodes: ['HEDE'], page: 1, size: 200 })
expect(wrapper.text()).toContain('MSCU-2')
expect(wrapper.text()).not.toContain('MSCU-1')
})
it('merges detailed pricing channels into the same confirmed display group', async () => {
const parcelContainer = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
containerNo: 'MSCU-2',
channelCode: 'MATSON_CLX_PARCEL',
channelName: '美森正班海派',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, parcelContainer], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, parcelContainer])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(2)
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).toContain('MSCU-2')
expect(wrapper.text()).not.toContain('美森正班海卡包税')
expect(wrapper.text()).not.toContain('美森正班海派')
})
it('keeps the shipping warehouse as a candidate-only query filter', async () => {
const dongguanContainer = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-DG',
containerNo: 'MSCU-DG',
warehouseId: '18',
warehouseName: '东莞仓',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, dongguanContainer], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, dongguanContainer])
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).toContain('MSCU-DG')
const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length
wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', createdRange: [], orderStatuses: [] })
await flushPromises()
expect(wrapper.text()).toContain('MSCU-DG')
expect(wrapper.text()).toContain('MSCU-1')
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], warehouseId: dongguanContainer.warehouseId, page: 1, size: 20 })
expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount)
})
it('keeps channel cabinets unchanged when another search field has no matching cabinet', async () => {
const dongguanOvertimeContainer = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-DG-MAX',
containerNo: 'MSCU-DG-MAX',
warehouseId: '18',
warehouseName: '东莞仓',
channelGroupCode: 'MATSON_MAX',
channelCode: 'MATSON_MAX_TRUCK',
channelName: '美森加班海卡',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, dongguanOvertimeContainer], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, dongguanOvertimeContainer])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length
wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', createdRange: [], orderStatuses: [] })
await flushPromises()
expect(wrapper.find('.channel-tags .el-checkbox-button.is-checked').text()).toBe('美森正班')
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).not.toContain('MSCU-DG-MAX')
expect(listSmartLoadingCandidates).toHaveBeenCalledTimes(2)
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], warehouseId: '18', page: 1, size: 20 })
expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount)
})
it('filters candidate details by inbound code without filtering channel cabinets', async () => {
const secondContainer = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
containerNo: 'MSCU-2',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, secondContainer], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, secondContainer])
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
vi.mocked(listSmartLoadingCandidates)
.mockResolvedValueOnce(candidatePage([candidate]))
.mockResolvedValueOnce(candidatePage([{ ...candidate, inboundCode: 'NYEXX2609030002' }]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
warehouseId: '', customerId: '', inboundCode: 'NYEXX2609030002', createdRange: [], orderStatuses: [],
})
await flushPromises()
expect(wrapper.get('.candidate-pane').text()).toContain('NYEXX2609030002')
expect(wrapper.findAll('.container-option')).toHaveLength(2)
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).toContain('MSCU-2')
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], inboundCode: 'NYEXX2609030002', page: 1, size: 20 })
expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount)
})
it('sends the active name-only customer selector to the server candidate query', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('useActiveCustomerOptions(')
expect(source).toContain('remoteMethod: loadCustomers')
expect(source).toContain("label: customer.tenantName")
expect(source).toContain('params.customerId = customerId')
expect(source).not.toContain('customer.tenantCode')
})
it('searches customers remotely, loads more options, and applies the selected customer to candidates', async () => {
const target = { id: '9007199254740999', tenantName: '柏悦物流', status: 'ACTIVE' } as PlatformTenantResource
vi.mocked(listPlatformTenants).mockImplementation(async query => ({
records: query.keyword || query.page === 2 ? [target] : [{ id: '1', tenantName: '跨客户车次-测试客户', status: 'ACTIVE' } as PlatformTenantResource],
page: query.page, size: 50, total: query.keyword ? 1 : 350, pages: query.keyword ? 1 : 7,
}))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const selector = wrapper.findAllComponents(ElSelect).find(item => item.props('placeholder') === '全部客户')!
expect(selector.props('remote')).toBe(true)
selector.vm.$emit('end-reached', 'bottom'); await flushPromises()
expect(listPlatformTenants).toHaveBeenLastCalledWith({ page: 2, size: 50, status: 'ACTIVE' })
await selector.props('remoteMethod')?.('柏悦'); await flushPromises()
expect(listPlatformTenants).toHaveBeenLastCalledWith({ page: 1, size: 50, status: 'ACTIVE', keyword: '柏悦' })
expect(selector.findAllComponents(ElOption).map(option => option.props('label'))).toContain('柏悦物流')
selector.vm.$emit('update:modelValue', target.id); await flushPromises()
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.$emit('search', toolbar.props('modelValue')); await flushPromises()
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(expect.objectContaining({ customerId: target.id }))
wrapper.unmount()
})
it('defaults ordinary candidate assignment to all available boxes', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain(': candidate.availablePackageCount')
})
it('filters candidates by creation, expected-arrival range and multiple order statuses', async () => {
vi.mocked(listSmartLoadingCandidates)
.mockResolvedValueOnce(candidatePage([
{ ...candidate, inboundId: '51', inboundCode: 'IN-MATCH', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-11T08:00:00' },
{ ...candidate, inboundId: '52', inboundCode: 'IN-WRONG-STATUS', orderStatusSnapshot: 'LOADING', expectedArrivalAt: '2026-09-11T08:00:00' },
{ ...candidate, inboundId: '53', inboundCode: 'IN-WRONG-DATE', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-20T08:00:00' },
]))
.mockResolvedValueOnce(candidatePage([{ ...candidate, inboundId: '51', inboundCode: 'IN-MATCH', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-11T08:00:00' }]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length
wrapper.getComponent(QueryToolbar).vm.$emit('search', {
customerId: '', inboundCode: '', orderStatuses: ['WAIT_SPLIT', 'LOADING'], createdRange: ['2026-09-10', '2026-09-12'],
expectedArrivalRange: ['2026-09-11', '2026-09-15'],
})
await flushPromises()
const candidates = wrapper.get('.candidate-pane').text()
expect(candidates).toContain('IN-MATCH')
expect(candidates).not.toContain('IN-WRONG-STATUS')
expect(candidates).not.toContain('IN-WRONG-DATE')
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], createdStart: '2026-09-10', createdEnd: '2026-09-12', expectedArrivalStart: '2026-09-11', expectedArrivalEnd: '2026-09-15', orderStatuses: ['WAIT_SPLIT', 'LOADING'], page: 1, size: 20 })
expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount)
expect(listSmartLoadingWorkbenchContainers).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 200 })
})
it('paginates large inbound candidate results with twenty rows by default', async () => {
vi.mocked(listSmartLoadingCandidates)
.mockResolvedValueOnce(candidatePage(Array.from({ length: 20 }, (_, index) => ({
...candidate,
inboundId: String(100 + index),
orderId: String(200 + index),
inboundCode: `IN-${String(index + 1).padStart(3, '0')}`,
})), 1, 20, 25))
.mockResolvedValueOnce(candidatePage(Array.from({ length: 5 }, (_, index) => ({
...candidate,
inboundId: String(120 + index),
orderId: String(220 + index),
inboundCode: `IN-${String(index + 21).padStart(3, '0')}`,
})), 2, 20, 25))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const pane = wrapper.get('.candidate-pane')
expect(pane.text()).toContain('IN-020')
expect(pane.text()).not.toContain('IN-021')
expect(pane.text()).toContain('共 25 条 · 1/2 页')
await pane.findAll('button').find((button) => button.attributes('aria-label') === '下一页')!.trigger('click')
await flushPromises()
expect(pane.text()).toContain('IN-021')
expect(pane.text()).not.toContain('IN-001')
})
it('aggregates a two-order candidate group and calls only whole-group assignment', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
accountId: '1', inboundId: '201', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '客户一、客户二', goodsSummary: '货物一、货物二', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', memberCount: 3, groupTotalBoxes: 12, locked: false, blockReason: null,
groupMembers: [
{ accountId: '1', inboundId: '201', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051', customerName: '客户一', availablePackageCount: 4, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
{ accountId: '1', inboundId: '203', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051-B', customerName: '客户一', availablePackageCount: 2, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
{ accountId: '2', inboundId: '202', orderId: '302', orderNo: 'ORD-202', inboundCode: 'NYEXX2608290052', customerName: '客户二', availablePackageCount: 6, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
],
}]))
vi.mocked(assignSmartLoadingGroup).mockResolvedValue(plan)
const wrapper = mount(GlobalSmartLoadingView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
const candidatePane = wrapper.get('.candidate-pane')
expect(candidatePane.get('.candidate-group').text()).toBe('合柜报关 · 2票整组不可拆')
expect(candidatePane.text()).toContain('NYEXX2608290051、NYEXX2608290051-B、NYEXX2608290052')
expect(candidatePane.text()).not.toContain('CLG-2093539779795795969')
expect(candidatePane.text()).not.toContain('ORD-201')
const buttons = wrapper.findAll('.el-table__body-wrapper button').filter((button) => button.text() === '加入')
expect(buttons).toHaveLength(1)
await buttons[0].trigger('click')
await flushPromises()
expect(document.body.textContent).not.toContain('不能部分输入、单独分配或单独移除')
const popup = wrapper.getComponent(AppDialog)
expect(popup.props('title')).toBe('整组加入当前柜次 · CLG-2093539779795795969')
const contextRows = assignmentDialog().findAll('table[aria-label="全局装柜分配来源"] tr')
expect(contextRows[0]!.findAll('td').map(cell => cell.text())).toEqual(['CB-1', 'MSCU-1'])
expect(contextRows[1]!.findAll('td').map(cell => cell.text())).toEqual(['义乌仓', '美森正班'])
await assignmentDialog().get('[aria-label="展开全屏"]').trigger('click')
expect(assignmentDialog().get('table[aria-label="全局装柜同柜组成员"]').text()).toContain('NYEXX2608290051-B')
expect(assignmentDialog().get('table[aria-label="全局装柜同柜组成员"]').text()).toContain('ORD-201:NYEXX2608290051、NYEXX2608290051-B')
expect(assignmentDialog().text()).toContain('3 个入仓片段')
expect(document.body.textContent).not.toContain('本次分配箱数')
;([...document.body.querySelectorAll('button')].find((button) => button.textContent?.includes('确认整组分配')) as HTMLButtonElement).click()
await flushPromises()
expect(assignSmartLoadingGroup).toHaveBeenCalledWith(plan.id, '201', plan.version)
expect(assignSmartLoadingItem).not.toHaveBeenCalled()
wrapper.unmount()
})
it('allows a pending-review order to join through the provisional assignment command', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
...candidate,
customerId: '',
inboundId: null,
orderStatusSnapshot: 'PENDING_REVIEW',
inboundCode: 'EXXHK2609020361',
channelCode: 'HEDE_TRUCK',
channelName: '合德海卡',
unitGrossWeightKg: null,
averageUnitVolumeCbm: null,
availableVolumeCbm: null,
blockReason: null,
}]))
vi.mocked(assignPendingReviewSmartLoadingOrder).mockResolvedValue(plan)
const wrapper = mount(GlobalSmartLoadingView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.candidate-pane').text()).toContain('待审核')
const add = wrapper.get('.candidate-pane .el-table__body-wrapper').findAll('button')
.find((button) => button.text().includes('加入'))!
expect(add.classes()).not.toContain('is-disabled')
await add.trigger('click')
await flushPromises()
const popup = wrapper.getComponent(AppDialog)
expect(popup.props('title')).toBe('加入当前柜次 · EXXHK2609020361')
const measurements = assignmentDialog().findAll('table[aria-label="全局装柜分配填写"] tr').find(row => row.text().includes('平均单箱毛重'))!
expect(measurements.findAll('td').map(cell => cell.text())).toEqual(['待测量', '待测量'])
const channel = assignmentDialog().findAll('table[aria-label="全局装柜分配来源"] tr').find(row => row.text().includes('订单渠道'))!
expect(channel.findAll('td')[0]!.text()).toBe('合德海卡')
expect(document.body.textContent).toContain('这是人工装柜分配,确认后仍会加入')
expect(document.body.textContent).toContain('智能推荐不会跨渠道')
;([...document.body.querySelectorAll('button')]
.find((button) => button.textContent?.includes('确认加入')) as HTMLButtonElement).click()
await flushPromises()
expect(assignPendingReviewSmartLoadingOrder).toHaveBeenCalledWith(plan.id, {
customerId: '1', orderId: '7', inboundCode: 'EXXHK2609020361',
packageCount: 12, expectedVersion: plan.version,
})
expect(assignSmartLoadingItem).not.toHaveBeenCalled()
wrapper.unmount()
})
it('blocks an incomplete three-member candidate group without exposing member assignment', async () => {
const members = ['401', '402', '403'].map((inboundId, index) => ({ accountId: `${index + 1}`, inboundId, orderId: `50${index}`, orderNo: `ORD-${index}`, inboundCode: `IN-${inboundId}`, customerName: `客户${index}`, availablePackageCount: 6, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null }))
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
...candidate, inboundId: '401', orderId: '500', inboundCode: 'IN-401', coLoadGroupId: '9900', groupNo: 'CLG-9900', memberCount: 3, groupTotalBoxes: 18, availablePackageCount: 18, locked: false, blockReason: '同柜不可拆组成员候选不齐,缺少 1 票', groupMembers: members,
}]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('合柜报关 · 3票')
expect(wrapper.text()).toContain('同柜不可拆组成员候选不齐,缺少 1 票')
const button = wrapper.findAll('.el-table__body-wrapper button').find((item) => item.text() === '加入')!
expect(button.attributes('disabled')).toBeDefined()
expect(assignSmartLoadingGroup).not.toHaveBeenCalled()
})
it('shows the package summary first and opens the editor from an icon', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('class="package-count-summary"')
expect(source).toContain('aria-label="调整箱数"')
expect(source).toContain('v-if="(row.remainingPackageCount ?? 0) > 0"')
expect(source).toContain('v-if="editingQuantityId === row.id"')
expect(source).toContain(':controls="false"')
expect(source).toContain('@blur="adjust(row)" @keyup.enter="adjust(row)"')
expect(source).not.toContain('<Save')
expect(source).toContain('.plan-pane :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }')
})
it('saves a typed package count through the adjustment API when editing finishes', async () => {
vi.mocked(adjustSmartLoadingItem).mockResolvedValue({
...plan,
version: 3,
items: [{ ...plan.items[0], packageCount: 4, version: 2 }],
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
await flushPromises()
const quantityInput = wrapper.findAllComponents(ElInputNumber).find((input) => input.props('modelValue') === 6)
expect(quantityInput?.props('controls')).toBe(false)
quantityInput?.vm.$emit('update:modelValue', 4)
await flushPromises()
quantityInput?.vm.$emit('blur')
await flushPromises()
expect(adjustSmartLoadingItem).toHaveBeenCalledTimes(1)
expect(adjustSmartLoadingItem).toHaveBeenCalledWith(plan.id, plan.items[0].id, 4, plan.version, plan.items[0].version)
})
it('shows a shared pallet once with exact members and a text-only join action', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
...candidate, unitType: 'SEALED_PALLET', palletId: '801', palletNo: 'DP-0025', indivisible: true,
availablePackageCount: 13, unitGrossWeightKg: 10,
groupMembers: [
{ accountId: '1', inboundId: '180', orderId: '180', orderNo: 'O180', inboundCode: 'I180', customerName: '客户', availablePackageCount: 3, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
{ accountId: '1', inboundId: '181', orderId: '181', orderNo: 'O181', inboundCode: 'I181', customerName: '客户', availablePackageCount: 10, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
],
}]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const buttons = wrapper.findAll('.el-table__body-wrapper button').filter(button => button.text() === '加入')
expect(buttons).toHaveLength(1)
expect(buttons[0].find('svg').exists()).toBe(false)
expect(wrapper.text()).toContain('I180:3 箱')
expect(wrapper.text()).toContain('I181:10 箱')
expect(wrapper.text()).toContain('130 kg')
expect(wrapper.get('.candidate-pane').text()).toContain('整托加入 · 不可拆')
expect(wrapper.get('.plan-pane').text()).toContain('按箱加入')
expect(wrapper.text()).toContain('装柜规则')
expect(wrapper.text()).not.toContain('分配单位')
wrapper.unmount()
})
it('uses one loading-rule column in both workbenches and keeps global list summaries compact', () => {
for (const page of ['GlobalSmartLoadingView.vue', 'SmartLoadingWorkbenchView.vue']) {
const source = readFileSync(resolve(process.cwd(), `src/modules/container-loading/pages/${page}`), 'utf8')
expect(source.match(/label: '装柜规则'/g)).toHaveLength(2)
expect(source).not.toContain("label: '分配单位'")
expect(source).not.toContain('<template #unitType=')
expect(source.match(/'整托加入 · 不可拆' : '按箱加入'/g)).toHaveLength(2)
}
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source.match(/slotName: 'coLoadGroup', showOverflowTooltip: false/g)).toHaveLength(2)
expect(source).not.toContain("row.memberOrderSummaries.join(';')")
})
it('shows compact locked plan groups with all inbound codes and fixed package totals', async () => {
const group = { groupId: 'G-1', groupNo: 'CLG-1', lockedPlanId: plan.id, memberOrderCount: 2, inboundFragmentCount: 3, fullPackageCount: 18 }
vi.mocked(loadSmartLoadingPlanCoLoadGroups).mockReturnValueOnce(new Map(['2', '3', '4'].map(id => [id, group])))
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
items: ['2', '3', '4'].map((inboundId, index) => ({ ...plan.items[0]!, id: `item-${inboundId}`, inboundId, inboundCode: `IN-${inboundId}`, orderId: index < 2 ? 'order-1' : 'order-2' })),
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const pane = wrapper.get('.plan-pane')
expect(pane.get('.candidate-group').text()).toBe('合柜报关 · 2票整组不可拆 · 已锁定')
expect(pane.text()).toContain('IN-2、IN-3、IN-4')
expect(pane.text()).toContain('18 箱(整组固定)')
expect(pane.text()).not.toContain('CLG-1')
expect(pane.text()).not.toContain('ORD-1')
expect(pane.findAll('.el-table__body-wrapper tr')).toHaveLength(1)
})
it('searches any shared-pallet inbound, retains its complete composition across pages, and resets the search', async () => {
const sharedPallet = {
...candidate, inboundId: '180', inboundCode: 'I180', unitType: 'SEALED_PALLET' as const,
palletId: '801', palletNo: 'DP-0025', indivisible: true, availablePackageCount: 13,
groupMembers: [
{ accountId: '1', inboundId: '180', orderId: '180', orderNo: 'O180', inboundCode: 'I180', customerName: '客户', availablePackageCount: 3, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
{ accountId: '1', inboundId: '181', orderId: '181', orderNo: 'O181', inboundCode: 'I181', customerName: '客户', availablePackageCount: 10, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
],
}
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([sharedPallet], 1, 20, 21))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const filters = { warehouseId: '', customerId: '', inboundCode: ' I181 ', createdRange: [], expectedArrivalRange: [], orderStatuses: [] }
wrapper.getComponent(QueryToolbar).vm.$emit('search', filters)
await flushPromises()
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], inboundCode: 'I181', page: 1, size: 20 })
const pane = wrapper.get('.candidate-pane')
expect(pane.text()).toContain('I180:3 箱')
expect(pane.text()).toContain('I181:10 箱')
expect(pane.findAll('.el-table__body-wrapper button').filter(button => button.text() === '加入')).toHaveLength(1)
await pane.get('button[aria-label="下一页"]').trigger('click')
await flushPromises()
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], inboundCode: 'I181', page: 2, size: 20 })
wrapper.getComponent(QueryToolbar).vm.$emit('reset', { ...filters, inboundCode: '' })
await flushPromises()
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
wrapper.unmount()
})
it('rounds capacity percentages and hides column settings in embedded tables', async () => {
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
items: [{ ...plan.items[0], volumeCbm: 0.187, grossWeightKg: 13.6 }],
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('0.18%')
expect(wrapper.text()).toContain('0.05%')
expect(wrapper.text()).not.toContain('0.169999999')
expect(wrapper.findAll('button[aria-label="列设置"]')).toHaveLength(0)
})
it('keeps the confirmed candidate filters without unrelated customs fields', async () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }>; props?: Record<string, unknown> }>
expect(fields.map((field) => field.key)).toEqual(['warehouseId', 'customerId', 'inboundCode', 'createdRange', 'expectedArrivalRange', 'orderStatuses'])
expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([
{ label: '义乌仓', value: '8' },
{ label: '东莞仓', value: '18' },
])
expect(fields.find((field) => field.key === 'orderStatuses')?.options).toEqual([
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'LOADING', label: '装柜中' },
])
expect(fields.find((field) => field.key === 'orderStatuses')?.props).toMatchObject({ multiple: true, collapseTags: true, maxCollapseTags: 2 })
expect(fields.find((field) => field.key === 'createdRange')?.props).toMatchObject({ startPlaceholder: '创建起始', endPlaceholder: '创建截止', 'aria-label': '创建日期范围' })
expect(fields.find((field) => field.key === 'expectedArrivalRange')?.props).toMatchObject({ startPlaceholder: '到货起始', endPlaceholder: '到货截止', 'aria-label': '预计到货日期范围' })
expect(source).not.toContain('目的仓')
expect(wrapper.text()).not.toContain('税率')
expect(wrapper.text()).not.toContain('是否退税')
})
it('relies on the server contract to exclude completed candidates', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([candidate]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.candidate-pane').text()).toContain('IN-2')
expect(wrapper.get('.candidate-pane').text()).not.toContain('IN-LOADED')
expect(wrapper.get('.candidate-pane').text()).not.toContain('IN-COMPLETED')
expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
})
it('prefers an editing container on first load while keeping loading containers available', async () => {
const pushed = { ...container, id: '9007199254740995', planStatus: 'LOADING' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [pushed, container], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([pushed, container])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
expect(wrapper.text()).toContain('MSCU-1 装柜方案')
})
it('keeps waiting and editing empty containers without exposing their status labels', async () => {
const waiting = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-WAIT', containerNo: null, planStatus: 'WAIT_LOADING' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [container, waiting], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, waiting])
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => (
bookingId === waiting.id
? { ...plan, bookingId: waiting.id, status: 'LOADING' as const }
: plan
))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const waitingOption = wrapper.findAll('.container-option').find((item) => item.text().includes('CB-WAIT'))!
expect(waitingOption.text()).toContain('CB-WAIT')
expect(waitingOption.text()).not.toContain('待装柜')
const candidateCalls = vi.mocked(listSmartLoadingCandidates).mock.calls.length
await waitingOption.trigger('click')
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(waiting.id)
expect(listSmartLoadingCandidates).toHaveBeenCalledTimes(candidateCalls)
expect(waitingOption.text()).toContain('CB-WAIT')
expect(waitingOption.text()).not.toContain('拣货中')
})
it('keeps picking containers available when the plan capability allows increments', async () => {
const pushed = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-2', planStatus: 'LOADING' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [pushed], page: 1, size: 100, total: 1, pages: 1 })
mockContainerSearch([pushed])
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, bookingId: pushed.id, status: 'LOADING', pdaTaskId: '700', pdaTaskStatus: 'LOADING', pdaTaskVersion: 4 })
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(pushed.id)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('CB-2')
expect(wrapper.text()).not.toContain('已下发 PDA,仍可维护未完成的装柜明细')
expect(wrapper.text()).not.toContain('刷新 PDA 明细与版本')
expect(wrapper.text()).toContain('示例企业')
})
it('hides completed containers from the smart loading workbench', async () => {
const completed = { ...container, id: '9007199254740997', planId: '9007199254740998', cabinetNo: 'CB-3', planStatus: 'COMPLETED' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [completed], page: 1, size: 100, total: 1, pages: 1 })
mockContainerSearch([completed])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled()
expect(wrapper.text()).not.toContain('CB-3')
expect(wrapper.text()).toContain('暂无符合条件的货柜')
expect(wrapper.text()).not.toContain('缺少渠道快照')
})
it('isolates a missing-channel row without hiding another valid container', async () => {
const missingChannel = { ...container, id: '9007199254740997', cabinetNo: 'CB-MISSING', channelGroupCode: '', channelCode: null, channelName: null }
vi.mocked(listContainers).mockResolvedValue({ records: [missingChannel, container], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([missingChannel, container])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('CB-1')
expect(wrapper.text()).not.toContain('CB-MISSING')
expect(wrapper.text()).not.toContain('缺少渠道快照')
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
})
it('renders the channel-filtered candidate page returned by the server', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([
{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-MATCH', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '匹配企业', goodsSummary: '匹配货物', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null },
]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('匹配企业')
expect(wrapper.text()).not.toContain('IN-OTHER')
expect(wrapper.text()).not.toContain('其他货物')
expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
})
it('keeps assignment failures and diagnostic IDs inside the open dialog without losing the quantity', async () => {
vi.spyOn(ElMessage, 'error').mockImplementation(() => undefined as never)
vi.mocked(assignSmartLoadingItem).mockRejectedValueOnce(new AxiosError(
'forbidden', 'ERR_BAD_REQUEST', undefined, undefined,
{ data: { code: 'A0403', message: 'Access denied', requestId: 'assign-request', traceId: 'assign-trace' }, status: 403, statusText: 'Forbidden', headers: {}, config: {} as never },
))
const wrapper = mount(GlobalSmartLoadingView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.get('.candidate-pane .el-table__body-wrapper').findAll('button').find(button => button.text() === '加入')!.trigger('click')
await flushPromises()
const popup = wrapper.getComponent(AppDialog)
const input = popup.getComponent(ElInputNumber)
input.vm.$emit('update:modelValue', 7)
await flushPromises()
// 箱数与上方资料使用同一标签列,保留输入名称与原范围。
const label = assignmentDialog().get('label[for="global-loading-assignment-count"]')
expect(label.element.parentElement?.tagName).toBe('TH')
expect(label.element.parentElement?.nextElementSibling?.getAttribute('colspan')).toBe('3')
expect(assignmentDialog().find('input#global-loading-assignment-count').exists()).toBe(true)
expect(input.props('max')).toBe(candidate.availablePackageCount)
popup.vm.$emit('confirm')
await flushPromises()
expect(assignSmartLoadingItem).toHaveBeenCalledWith(plan.id, { inboundId: candidate.inboundId, packageCount: 7, expectedVersion: plan.version })
expect(popup.props('modelValue')).toBe(true)
expect(input.props('modelValue')).toBe(7)
expect(assignmentDialog().text()).toContain('加入当前柜次失败')
expect(assignmentDialog().text()).toContain('当前账号无权访问该数据')
expect(assignmentDialog().text()).toContain('assign-request')
expect(assignmentDialog().text()).toContain('assign-trace')
expect(assignmentDialog().findAll('.load-error-state')).toHaveLength(1)
expect(wrapper.get('.global-loading-page').findAll('.load-error-state')).toHaveLength(0)
wrapper.unmount()
})
it('refreshes the latest plan after A0409 without losing the selected container', async () => {
const warningMessage = vi.spyOn(ElMessage, 'warning').mockImplementation(() => undefined as never)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, version: 3 })
vi.mocked(recommendSmartLoading).mockRejectedValueOnce(new AxiosError(
'conflict',
'ERR_BAD_REQUEST',
undefined,
undefined,
{ data: { code: 'A0409', message: '数据已被修改', requestId: 'request-1', traceId: 'trace-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never },
)).mockResolvedValueOnce({ ...plan, version: 4 })
const button = wrapper.findAll('button').find((item) => item.text().includes('智能推荐'))
expect(button).toBeDefined()
await button!.trigger('click')
await flushPromises()
expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 2, { customerId: '', inboundCode: '' })
expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 3, { customerId: '', inboundCode: '' })
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2)
expect(warningMessage).not.toHaveBeenCalled()
expect(wrapper.text()).not.toContain('装柜操作失败')
expect(wrapper.text()).not.toContain('request-1')
})
it('shows a picking-specific message when a picked order cannot be removed', async () => {
vi.mocked(removeSmartLoadingItem).mockRejectedValue(new AxiosError(
'conflict',
'ERR_BAD_REQUEST',
undefined,
undefined,
{ data: { code: 'A0409', message: '已有拣货记录的明细不能删除', requestId: 'request-remove-1', traceId: 'trace-remove-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never },
))
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
const errorMessage = vi.spyOn(ElMessage, 'error').mockImplementation(() => undefined as never)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const removeButton = wrapper.find('.row-actions .el-button--danger')
expect(removeButton.exists()).toBe(true)
await removeButton.trigger('click')
await flushPromises()
expect(removeSmartLoadingItem).toHaveBeenCalled()
expect(errorMessage).toHaveBeenCalledWith('该订单在拣货中不许删除')
expect(wrapper.text()).toContain('装柜操作失败')
expect(wrapper.text()).toContain('request-remove-1')
expect(wrapper.text()).toContain('trace-remove-1')
})
it('removes every unpicked item with one bulk request', async () => {
const secondItem = { ...plan.items[0], id: '102', inboundDetailId: '4', inboundCode: 'IN-2' }
const initialPlan = { ...plan, items: [plan.items[0], secondItem] }
const emptyPlan = { ...plan, version: 4, items: [] }
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(initialPlan).mockResolvedValue(emptyPlan)
vi.mocked(removeAllUnpickedSmartLoadingItems).mockResolvedValueOnce(emptyPlan)
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const removeAllButton = wrapper.findAll('button').find((button) => button.text().includes('移除未拣'))
expect(removeAllButton).toBeDefined()
await removeAllButton!.trigger('click')
await flushPromises()
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.any(String), '移除未拣明细 · CB-1', expect.objectContaining({ customClass: 'admin-density-confirm' }))
expect(removeAllUnpickedSmartLoadingItems).toHaveBeenCalledTimes(1)
expect(removeAllUnpickedSmartLoadingItems).toHaveBeenCalledWith(plan.id, 2)
expect(removeSmartLoadingItem).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('当前柜次暂无装柜明细')
})
it('shows one row per inbound code and removes all internal specification rows', async () => {
const secondDetail = {
...plan.items[0], id: '102', inboundDetailId: '4', skuCode: 'SKU-2', productNameZh: '鞋类',
packageCount: 4, grossWeightKg: 40, volumeCbm: 0.8, loadSequence: 2,
}
const groupedPlan = { ...plan, items: [plan.items[0], secondDetail] }
const afterFirst = { ...plan, version: 3, items: [secondDetail] }
const emptyPlan = { ...plan, version: 4, items: [] }
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(groupedPlan).mockResolvedValue(emptyPlan)
vi.mocked(removeSmartLoadingItem).mockResolvedValueOnce(afterFirst).mockResolvedValueOnce(emptyPlan)
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const inboundRows = wrapper.findAll('tr').filter((row) => row.text().includes('IN-1'))
expect(inboundRows).toHaveLength(1)
expect(inboundRows[0].text()).toContain('多规格,需删除后重加')
expect(inboundRows[0].text()).toContain('100 kg')
expect(inboundRows[0].text()).toContain('2 m³')
await inboundRows[0].find('.el-button--danger').trigger('click')
await flushPromises()
expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(1, plan.id, plan.items[0].id, 2, 1)
expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(2, plan.id, secondDetail.id, 3, 1)
})
it('uses item capabilities to protect picked quantities and removal', async () => {
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
status: 'LOADING',
pdaTaskId: '700',
pdaTaskStatus: 'LOADING',
pdaTaskVersion: 4,
items: [{ ...plan.items[0], pickedPackageCount: 3, minPackageCount: 3, canAdjust: true, canRemove: false }],
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('已拣 3 箱')
expect(wrapper.findAll('.row-actions .el-button--danger')).toHaveLength(0)
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
await flushPromises()
const quantityInput = wrapper.findAllComponents(ElInputNumber).find((input) => input.props('modelValue') === 6)
expect(quantityInput?.props('min')).toBe(3)
expect(wrapper.findAll('button').some((button) => button.text().includes('移除未拣'))).toBe(false)
})
it('removes a sealed pallet row through the whole-pallet command', async () => {
const palletItem = {
...plan.items[0], unitType: 'SEALED_PALLET' as const, palletId: '801', palletNo: 'DP-260914-0021',
indivisible: true, packageCount: 10, pickedPackageCount: 0, minPackageCount: 10,
canAdjust: false, canRemove: true,
}
const palletPlan = { ...plan, items: [palletItem] }
const emptyPlan = { ...palletPlan, version: 3, items: [] }
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(palletPlan)
vi.mocked(removeSmartLoadingPallet).mockResolvedValue(emptyPlan)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const row = wrapper.findAll('tr').find((candidateRow) => candidateRow.text().includes('DP-260914-0021'))
expect(row).toBeDefined()
await row!.find('.el-button--danger').trigger('click')
await flushPromises()
expect(removeSmartLoadingPallet).toHaveBeenCalledWith(plan.id, '801', 2)
expect(removeSmartLoadingItem).not.toHaveBeenCalled()
})
it('shows every inbound code and its box count for an assigned sealed pallet', async () => {
const palletItem = {
...plan.items[0], inboundId: '180', inboundCode: 'I180', unitType: 'SEALED_PALLET' as const,
palletId: '801', palletNo: 'DP-260914-0025', indivisible: true, packageCount: 11,
pickedPackageCount: 0, minPackageCount: 11, canAdjust: false, canRemove: true,
}
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
items: [palletItem],
palletMembers: [{ palletId: '801', members: [
{ accountId: '1', inboundId: '180', orderId: '280', orderNo: 'O180', inboundCode: 'I180', customerName: '客户', availablePackageCount: 6, orderStatusSnapshot: null, expectedArrivalAt: null },
{ accountId: '1', inboundId: '181', orderId: '281', orderNo: 'O181', inboundCode: 'I181', customerName: '客户', availablePackageCount: 5, orderStatusSnapshot: null, expectedArrivalAt: null },
] }],
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const row = wrapper.findAll('.plan-pane .el-table__body-wrapper tr').find((candidateRow) => candidateRow.text().includes('DP-260914-0025'))
expect(row).toBeDefined()
expect(row!.text()).toContain('2 个入仓码 · 展开')
expect(row!.text()).toContain('I180:6 箱')
expect(row!.text()).toContain('I181:5 箱')
})
it('renders final completion capability as read-only with the server reason', async () => {
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
canAssign: false,
canEdit: false,
blockedReason: '正在校验装柜证据',
finalCompletionStatus: 'IN_PROGRESS',
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('正在最终确认,装柜方案暂时只读')
expect(wrapper.text()).toContain('正在校验装柜证据')
expect(wrapper.text()).not.toContain('请先完成拖车')
expect(wrapper.findAll('.row-actions button')).toHaveLength(0)
expect(wrapper.findAll('button').some((button) => button.text().includes('加入'))).toBe(false)
})
it('uses the server supplied total package count as the adjustment maximum', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ accountId: '1', inboundId: '2', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsSummary: '服装', availablePackageCount: 4, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 0.8, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }]))
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
items: [{ ...plan.items[0], totalPackageCount: 10, remainingPackageCount: 4 }],
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('使用:6')
expect(wrapper.text()).toContain('未用:4')
expect(wrapper.text()).not.toContain('共 10 箱')
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
await flushPromises()
const maxima = wrapper.findAllComponents(ElInputNumber).map((input) => input.props('max'))
expect(maxima).toContain(10)
})
it('uses bordered assignment and adjustment actions at narrow widths', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('size="small"')
expect(source).toContain('type="danger" plain')
expect(source).toContain(':max="adjustmentMax(row)"')
expect(source).toContain("{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 120, slotName: 'customerName' }")
expect(source).not.toContain('link type="danger"')
expect(source).toContain('@media (max-width: 720px)')
expect(source.match(/<DataTable[\s\S]*?\bstripe\b/g)).toHaveLength(2)
})
it('bounds all three workbench panes and keeps their content independently scrollable', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('height: clamp(520px, 68vh, 720px); min-height: 0;')
expect(source).toContain('grid-template-columns: minmax(0, 1.05fr) 240px minmax(0, 1fr);')
expect(source).toContain('.dispatch-pane { display: flex; min-width: 0; min-height: 0; flex-direction: column; }')
expect(source).toContain('.candidate-pane :deep(.data-table),.plan-pane :deep(.data-table) { display: flex; min-height: 0; flex: 1; flex-direction: column; overflow: hidden; }')
expect(source).toContain(".candidate-pane :deep(.data-table__page-button[aria-label^='第']),.candidate-pane :deep(.data-table__page-ellipsis) { display: none; }")
expect(source).toContain('.candidate-pane :deep(.data-table__page-size .el-select) { width: 68px; }')
expect(source).toContain('.container-list { display: grid; min-height: 0; flex: 1;')
expect(source).toContain('grid-auto-rows: max-content;')
expect(source).toContain('overflow-wrap: anywhere;')
expect(source).toContain('.candidate-pane,.plan-pane { height: min(68vh, 560px); min-height: 420px; }')
expect(source).toContain('.container-pane { max-height: min(58vh, 460px); }')
expect(source).toContain('gap: var(--admin-space-1); padding: var(--admin-space-4);')
expect(source).toContain('.candidate-pane :deep(.data-table__pagination) { flex: 0 0 auto; }')
expect(source).toContain("label: '创建日期'")
expect(source).toContain("label: '预计到货时间'")
expect(source).toContain("label: '订单状态'")
expect(source).toContain('.smart-loading-query .query-toolbar__filters) { flex-wrap: wrap; gap: 8px; }')
expect(source).toContain('.smart-loading-query .query-toolbar__actions) { flex-wrap: nowrap; gap: 6px; }')
expect(source).toContain(':page-sizes="[20, 50, 100]"')
})
it('uses nine fixed multi-select channel tags with a compact right-aligned summary', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('<el-checkbox-group :model-value="selectedChannelCodes"')
expect(source).toContain('v-for="channel in channelOptions"')
expect(source).toContain('@dblclick.stop.prevent="selectOnlyChannel(channel.code)"')
expect(source).toContain('{{ channel.name }}')
expect(source).toContain("ElMessage.warning('至少选择一个渠道')")
expect(source).toContain('class="compact-summary" aria-label="已选渠道装柜统计"')
expect(source).toContain('<dt>候选明细</dt>')
expect(source).toContain('<dt>柜次</dt>')
expect(source).toContain('<dt>当前柜次分配</dt>')
expect(source).toContain('formatNumber(totalWeight, 1)')
expect(source).toContain('formatNumber(totalVolume)')
expect(source).toContain('.channel-tags :deep(.el-checkbox-button__inner)')
expect(source).toContain('flex-wrap: nowrap; gap: 6px;')
expect(source).toContain('height: 34px; padding: 0 10px;')
expect(source).toContain('font-size: var(--admin-font-body); line-height: 32px;')
expect(source).not.toContain('class="summary-band"')
expect(source).toContain('class="capacity-grid"')
expect(source).toContain('智能推荐')
expect(source).not.toContain('推送预装')
expect(source).toContain('缺少自动生成的预装计划')
expect(source).not.toContain('关联并放舱真实订舱')
expect(source).not.toContain('订舱放舱')
expect(source).not.toContain("canPush && plan?.status === 'EDITING' && selectedContainer?.containerNo")
})
})