Files
admin-web/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts
T
2026-09-04 10:51:31 +08:00

882 lines
49 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 } from 'element-plus'
import { AxiosError } from 'axios'
import { createPinia, setActivePinia } from 'pinia'
import { flushPromises, mount } from '@vue/test-utils'
import { 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 GlobalSmartLoadingView from './GlobalSmartLoadingView.vue'
import { resolveSmartLoadingChannelGroup } from '../smartLoadingChannelGroups'
import type { ContainerBookingResource } from '../types'
import {
assignPendingReviewSmartLoadingOrder,
assignSmartLoadingGroup,
assignSmartLoadingItem,
getSmartLoadingPlanByBooking,
listContainers,
listSmartLoadingCandidates,
listSmartLoadingWorkbenchContainers,
recommendSmartLoading,
removeAllUnpickedSmartLoadingItems,
removeSmartLoadingGroup,
removeSmartLoadingItem,
} from '../api/platformContainerLoading'
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', () => ({
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(),
assignPendingReviewSmartLoadingOrder: vi.fn(),
assignSmartLoadingGroup: vi.fn(),
recommendSmartLoading: vi.fn(),
adjustSmartLoadingItem: vi.fn(),
removeSmartLoadingItem: vi.fn(),
removeSmartLoadingGroup: 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', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', 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: 76, maxWeightKg: 28000,
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: [],
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: '服装', 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, 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: [], ...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, bookingNo: item.bookingNo, cabinetNo: item.cabinetNo, containerNo: item.containerNo,
warehouseId: item.warehouseId, warehouseName: item.warehouseName || '', 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(resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code || '')
))))
}
describe('GlobalSmartLoadingView', () => {
it('uses one-line shared text clamps for co-load and assigned-count summaries', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('<LineClampText :content="row.memberOrderSummaries.join(\';\')" />')
expect(source).toContain('<LineClampText v-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" />')
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()
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(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()).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: '40HC',
}],
page: 1,
size: 100,
total: 1,
pages: 1,
})
mockContainerSearch([{ ...container, cabinetNo: '2088475410134773762', containerNo: null, channelName: '美森正班海卡包税', warehouseName: '义乌国内仓', containerType: '40HC' }])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const option = wrapper.find('.container-option')
expect(option.text()).toContain('模拟柜次')
expect(option.text()).toContain('柜次号:待生成')
expect(option.text()).toContain('集装箱号:待补录')
expect(option.text()).not.toContain('开始装柜前录入集装箱号')
expect(option.text()).toContain('渠道:美森正班')
expect(option.text()).toContain('仓库:义乌国内仓')
expect(option.text()).toContain('柜型:40HC')
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',
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('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: '东莞仓',
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("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
expect(source).toContain("label: customer.tenantName")
expect(source).toContain('params.customerId = customerId')
expect(source).not.toContain('customer.tenantCode')
})
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 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'],
})
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', 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()
expect(wrapper.text()).toContain('CLG-2093539779795795969 · 2 票')
expect(wrapper.text()).toContain('3 个入仓片段')
expect(wrapper.text()).toContain('ORD-201:NYEXX2608290051、NYEXX2608290051-B')
const buttons = wrapper.findAll('button').filter((button) => button.text().includes('整组分配'))
expect(buttons).toHaveLength(1)
await buttons[0].trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('不能部分输入、单独分配或单独移除')
expect(document.body.textContent).toContain('目标柜次:CB-1 · 集装箱号:MSCU-1')
expect(document.body.textContent).toContain('发货仓:义乌仓 · 柜次渠道:美森正班海卡包税')
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: 'SLOW',
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()
expect(document.body.textContent).toContain('平均单箱毛重 待测量')
expect(document.body.textContent).toContain('平均单箱体积 待测量')
expect(document.body.textContent).not.toContain('平均单箱毛重 0 kg')
expect(document.body.textContent).toContain('订单渠道:慢船')
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('CLG-9900 · 3 票')
expect(wrapper.text()).toContain('同柜不可拆组成员候选不齐,缺少 1 票')
const button = wrapper.findAll('button').find((item) => item.text().includes('整组分配'))!
expect(button.attributes('disabled')).toBeDefined()
expect(assignSmartLoadingGroup).not.toHaveBeenCalled()
})
it('keeps the package count editor inside its table cell without overflow ellipsis', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('<div v-else class="package-count-editor"><el-input-number')
expect(source).toContain('@change="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('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.16%')
expect(wrapper.text()).toContain('0.03%')
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', '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(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', 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('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)
expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 3)
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(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)
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('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('limits adjusted package count to the assigned count plus remaining availability', 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 }]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
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: 70, 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, 5fr) minmax(0, 2fr) minmax(0, 3fr);')
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) { min-height: 0; flex: 1; }')
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('min-height: 112px')
expect(source).toContain("label: '创建日期'")
expect(source).toContain("label: '订单状态'")
expect(source).toContain('.smart-loading-query .query-toolbar__filters) { flex-wrap: nowrap; gap: 8px; overflow-x: auto; }')
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('<el-checkbox-button v-for="channel in channelOptions"')
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('<dt>总重量</dt>')
expect(source).toContain('<dt>总体积</dt>')
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: 13px; 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")
})
})