2026-08-10 15:36:53 +08:00
|
|
|
|
import ElementPlus from 'element-plus'
|
|
|
|
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
|
|
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
|
|
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
2026-08-11 18:06:35 +08:00
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
|
import { resolve } from 'node:path'
|
2026-08-10 15:36:53 +08:00
|
|
|
|
import ContainersView from './ContainersView.vue'
|
|
|
|
|
|
import { listAllPlatformWarehouses } from '@/modules/warehouses'
|
2026-08-11 18:06:35 +08:00
|
|
|
|
import { listPlatformPartners } from '@/modules/master-data'
|
2026-08-15 11:12:22 +08:00
|
|
|
|
import { listSuppliers } from '@/modules/suppliers'
|
2026-08-25 19:56:37 +08:00
|
|
|
|
import { getOceanBooking, listOceanBookingLinkCandidates, listOceanBookings } from '@/modules/ocean-bookings'
|
2026-08-31 12:12:51 +08:00
|
|
|
|
import { getContainer, getContainerOverview, linkContainerOceanBooking, listContainers, updateContainer } from '../api/platformContainerLoading'
|
2026-08-25 20:18:59 +08:00
|
|
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
2026-08-10 15:36:53 +08:00
|
|
|
|
|
|
|
|
|
|
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
|
2026-08-11 18:06:35 +08:00
|
|
|
|
vi.mock('@/modules/master-data', () => ({ listPlatformPartners: vi.fn() }))
|
2026-08-15 11:12:22 +08:00
|
|
|
|
vi.mock('@/modules/suppliers', () => ({ listSuppliers: vi.fn() }))
|
2026-08-25 19:56:37 +08:00
|
|
|
|
vi.mock('@/modules/ocean-bookings', () => ({ getOceanBooking: vi.fn(), listOceanBookingLinkCandidates: vi.fn(), listOceanBookings: vi.fn(), listWarehouseLogisticsChannels: vi.fn() }))
|
2026-08-14 12:50:50 +08:00
|
|
|
|
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
|
2026-08-11 18:06:35 +08:00
|
|
|
|
const accessCan = vi.hoisted(() => vi.fn())
|
|
|
|
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: accessCan }) }))
|
2026-08-31 12:12:51 +08:00
|
|
|
|
vi.mock('../api/platformContainerLoading', () => ({ listContainers: vi.fn(), getContainer: vi.fn(), getContainerOverview: vi.fn(), getContainerClearance: vi.fn(), uploadContainerClearance: vi.fn(), completeContainerClearance: vi.fn(), uploadContainerDocument: vi.fn(), downloadContainerDocument: vi.fn(), downloadContainerPdaPhoto: vi.fn(), markContainerException: vi.fn(), cancelContainerException: vi.fn(), createContainerSimulation: vi.fn(), linkContainerOceanBooking: vi.fn(), unlinkContainerOceanBooking: vi.fn(), updateContainer: vi.fn() }))
|
2026-08-10 15:36:53 +08:00
|
|
|
|
|
2026-08-11 11:15:44 +08:00
|
|
|
|
function createTestRouter() {
|
|
|
|
|
|
return createRouter({ history: createMemoryHistory(), routes: [
|
|
|
|
|
|
{ path: '/', component: ContainersView },
|
|
|
|
|
|
{ path: '/container-loading/:containerId/smart-loading', name: 'smart-loading-workbench', component: { template: '<div />' } },
|
|
|
|
|
|
{ path: '/loading-plans', name: 'loading-plans', component: { template: '<div />' } },
|
|
|
|
|
|
{ path: '/loading-lists', name: 'loading-lists', component: { template: '<div />' } },
|
|
|
|
|
|
] })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 15:36:53 +08:00
|
|
|
|
describe('ContainersView', () => {
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
setActivePinia(createPinia())
|
2026-08-11 18:06:35 +08:00
|
|
|
|
accessCan.mockReset(); accessCan.mockReturnValue(true)
|
2026-08-10 15:36:53 +08:00
|
|
|
|
vi.mocked(listAllPlatformWarehouses).mockResolvedValue([])
|
2026-08-11 18:06:35 +08:00
|
|
|
|
vi.mocked(listPlatformPartners).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 1 })
|
2026-08-15 11:12:22 +08:00
|
|
|
|
vi.mocked(listSuppliers).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 1 })
|
2026-08-11 18:06:35 +08:00
|
|
|
|
vi.mocked(listOceanBookings).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 1 })
|
2026-08-25 19:56:37 +08:00
|
|
|
|
vi.mocked(listOceanBookingLinkCandidates).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
2026-08-17 13:24:55 +08:00
|
|
|
|
vi.mocked(getOceanBooking).mockResolvedValue({} as never)
|
2026-08-25 19:56:37 +08:00
|
|
|
|
vi.mocked(listContainers).mockResolvedValue({ records: [{ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', sourceType: 'SIMULATION', oceanBookingId: null, warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null }], page: 1, size: 20, total: 1, pages: 1 })
|
|
|
|
|
|
vi.mocked(getContainer).mockResolvedValue({ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', sourceType: 'SIMULATION', oceanBookingId: null, warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null })
|
2026-08-13 11:57:44 +08:00
|
|
|
|
vi.mocked(getContainerOverview).mockResolvedValue({ container: { id: '9001', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'WAIT_LOADING', warehouseId: '8', warehouseName: '义乌仓', voyageNo: null, vesselName: null, version: 0 }, loadingPlanId: null, pda: null, documents: [], exception: { active: false, reason: null, markedBy: null, markedByName: null, markedAt: null }, exceptionHistory: [], timeline: [] })
|
2026-08-10 15:36:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('loads the real paged contract and renders core container fields', async () => {
|
2026-08-11 11:15:44 +08:00
|
|
|
|
const router = createTestRouter()
|
2026-08-10 15:36:53 +08:00
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
expect(listContainers).toHaveBeenCalledWith(expect.objectContaining({ page: 1, size: 20 }))
|
2026-08-11 11:52:30 +08:00
|
|
|
|
expect(wrapper.text()).toContain('待装柜')
|
2026-08-10 15:36:53 +08:00
|
|
|
|
expect(wrapper.text()).toContain('CB-1')
|
|
|
|
|
|
expect(wrapper.text()).toContain('MSCU-1')
|
|
|
|
|
|
expect(wrapper.text()).toContain('宁波 → 长滩')
|
|
|
|
|
|
expect(wrapper.find('button[aria-label="查看 CB-1"]').exists()).toBe(true)
|
|
|
|
|
|
})
|
2026-08-10 17:42:17 +08:00
|
|
|
|
|
2026-08-11 11:15:44 +08:00
|
|
|
|
it('keeps the cross-warehouse list usable when warehouse form options fail', async () => {
|
|
|
|
|
|
vi.mocked(listAllPlatformWarehouses).mockRejectedValueOnce(new Error('warehouse unavailable'))
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(wrapper.text()).toContain('CB-1')
|
|
|
|
|
|
expect(wrapper.text()).not.toContain('装柜仓库加载失败')
|
|
|
|
|
|
expect(wrapper.find('button[aria-label="查看 CB-1"]').exists()).toBe(true)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-10 17:42:17 +08:00
|
|
|
|
it('uses stable view labels when cabinetNo is missing', async () => {
|
|
|
|
|
|
const base = {
|
2026-08-17 12:31:27 +08:00
|
|
|
|
id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', containerType: '40HC', planStatus: 'WAIT_LOADING' as const, version: 0,
|
2026-08-10 17:42:17 +08:00
|
|
|
|
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
|
|
|
|
|
|
departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩',
|
2026-08-17 12:31:27 +08:00
|
|
|
|
destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000,
|
2026-08-25 19:56:37 +08:00
|
|
|
|
terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null,
|
2026-08-10 17:42:17 +08:00
|
|
|
|
estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null,
|
|
|
|
|
|
}
|
|
|
|
|
|
vi.mocked(listContainers).mockResolvedValueOnce({
|
|
|
|
|
|
records: [
|
|
|
|
|
|
{ ...base, id: '9002', cabinetNo: undefined, containerNo: 'MSCU-2', bookingNo: 'BK-2' },
|
|
|
|
|
|
{ ...base, id: '9003', cabinetNo: '', containerNo: null, bookingNo: 'BK-3' },
|
|
|
|
|
|
{ ...base, id: '9004', cabinetNo: null, containerNo: null, bookingNo: '' },
|
|
|
|
|
|
],
|
|
|
|
|
|
page: 1, size: 20, total: 3, pages: 1,
|
|
|
|
|
|
} as never)
|
2026-08-11 11:15:44 +08:00
|
|
|
|
const router = createTestRouter()
|
2026-08-10 17:42:17 +08:00
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
const labels = wrapper.findAll('.el-table__body button')
|
|
|
|
|
|
.map((button) => button.attributes('aria-label'))
|
|
|
|
|
|
.filter((label): label is string => Boolean(label))
|
|
|
|
|
|
expect(labels).toEqual(expect.arrayContaining(['查看 MSCU-2', '查看 BK-3', '查看货柜详情']))
|
|
|
|
|
|
expect(labels.every((label) => !/undefined|null/.test(label))).toBe(true)
|
|
|
|
|
|
})
|
2026-08-11 11:15:44 +08:00
|
|
|
|
|
|
|
|
|
|
it('routes each plan status to its owning workflow without reopening pushed drafts', async () => {
|
|
|
|
|
|
const base = {
|
2026-08-17 12:31:27 +08:00
|
|
|
|
id: '9001', planId: '9101', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', version: 0,
|
2026-08-11 11:15:44 +08:00
|
|
|
|
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
|
|
|
|
|
|
departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩',
|
2026-08-17 12:31:27 +08:00
|
|
|
|
destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000,
|
2026-08-25 19:56:37 +08:00
|
|
|
|
terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null,
|
2026-08-11 11:15:44 +08:00
|
|
|
|
estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null,
|
|
|
|
|
|
}
|
|
|
|
|
|
vi.mocked(listContainers).mockResolvedValueOnce({
|
|
|
|
|
|
records: [
|
|
|
|
|
|
{ ...base, planStatus: 'EDITING' as const },
|
|
|
|
|
|
{ ...base, id: '9002', planId: '9102', cabinetNo: 'CB-2', planStatus: 'LOADING' as const },
|
|
|
|
|
|
{ ...base, id: '9003', planId: '9103', cabinetNo: 'CB-3', planStatus: 'COMPLETED' as const },
|
|
|
|
|
|
],
|
|
|
|
|
|
page: 1, size: 20, total: 3, pages: 1,
|
|
|
|
|
|
})
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const push = vi.spyOn(router, 'push').mockResolvedValue(undefined as never)
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(wrapper.text()).toContain('排柜')
|
2026-08-13 11:57:44 +08:00
|
|
|
|
expect(wrapper.text()).toContain('预装清单')
|
|
|
|
|
|
expect(wrapper.text()).toContain('装柜清单')
|
2026-08-11 11:15:44 +08:00
|
|
|
|
|
|
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().trim() === '排柜')!.trigger('click')
|
2026-08-13 11:57:44 +08:00
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().includes('预装清单'))!.trigger('click')
|
|
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().includes('装柜清单'))!.trigger('click')
|
2026-08-11 11:15:44 +08:00
|
|
|
|
expect(push).toHaveBeenNthCalledWith(1, { name: 'smart-loading-workbench', params: { containerId: '9001' } })
|
|
|
|
|
|
expect(push).toHaveBeenNthCalledWith(2, { name: 'loading-plans', query: { planId: '9102', warehouseId: '8' } })
|
|
|
|
|
|
expect(push).toHaveBeenNthCalledWith(3, { name: 'loading-lists', query: { planId: '9103', warehouseId: '8' } })
|
|
|
|
|
|
})
|
2026-08-11 18:06:35 +08:00
|
|
|
|
|
2026-08-14 13:03:11 +08:00
|
|
|
|
it('uses active customer names and sends the stable customerId filter', () => {
|
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
|
|
|
|
|
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
|
2026-08-14 14:27:03 +08:00
|
|
|
|
expect(source).toContain("label: customer.tenantName")
|
2026-08-14 13:03:11 +08:00
|
|
|
|
expect(source).toContain('customerId: applied.value.customerId || undefined')
|
|
|
|
|
|
expect(source).not.toContain('customer.tenantCode')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-13 11:57:44 +08:00
|
|
|
|
it('uses the list permission for detail while keeping edit independent', async () => {
|
|
|
|
|
|
accessCan.mockImplementation((permission: string) => permission === 'platform:firstleg:container:list')
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(wrapper.find('button[aria-label="查看 CB-1"]').exists()).toBe(true)
|
|
|
|
|
|
expect(wrapper.find('button[aria-label="更多货柜操作"]').exists()).toBe(true)
|
|
|
|
|
|
expect(wrapper.findAll('button').some((button) => button.text().trim() === '编辑')).toBe(false)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('does not accept the removed standalone view permission', async () => {
|
|
|
|
|
|
accessCan.mockReturnValue(false)
|
2026-08-11 18:06:35 +08:00
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(wrapper.find('button[aria-label="查看 CB-1"]').exists()).toBe(false)
|
2026-08-13 11:57:44 +08:00
|
|
|
|
expect(wrapper.find('button[aria-label="更多货柜操作"]').exists()).toBe(false)
|
2026-08-11 18:06:35 +08:00
|
|
|
|
expect(wrapper.findAll('button').some((button) => button.text().trim() === '编辑')).toBe(false)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('uses business selectors and keeps code/name pairs internal', () => {
|
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
|
|
|
|
|
expect(source).toContain('placeholder="请选择船公司"')
|
|
|
|
|
|
expect(source).toContain('placeholder="请选择起运港"')
|
|
|
|
|
|
expect(source).toContain('placeholder="请选择目的港"')
|
|
|
|
|
|
expect(source).toContain('placeholder="请选择目的仓"')
|
|
|
|
|
|
expect(source).toContain("selectPort('departure'")
|
|
|
|
|
|
expect(source).toContain('selectDestinationWarehouse')
|
|
|
|
|
|
expect(source).toContain('重新加载业务选项')
|
|
|
|
|
|
expect(source).not.toContain('label="起运港代码"')
|
|
|
|
|
|
expect(source).not.toContain('label="起运港名称"')
|
|
|
|
|
|
expect(source).not.toContain('label="目的仓代码"')
|
|
|
|
|
|
expect(source).not.toContain('label="目的仓名称"')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('keeps query and pagination on a single list request path', () => {
|
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
|
|
|
|
|
expect(source).toContain("if (page.value === 1) void loadRows(); else page.value = 1")
|
|
|
|
|
|
expect(source).toContain("if (nextPage !== previousPage || nextSize !== previousSize) void loadRows()")
|
|
|
|
|
|
expect(source).not.toContain('@reset=')
|
|
|
|
|
|
})
|
2026-08-12 16:31:46 +08:00
|
|
|
|
|
2026-08-18 17:40:40 +08:00
|
|
|
|
it('opens container detail as an independent route while keeping create and edit dialogs', () => {
|
2026-08-12 16:31:46 +08:00
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
2026-08-18 17:40:40 +08:00
|
|
|
|
expect(source).toContain("router.push({ name: 'container-detail', params: { containerId: row.id } })")
|
|
|
|
|
|
expect(source).toContain("query: { action: command }")
|
|
|
|
|
|
expect(source).not.toContain('<AppDialog v-model="detailOpen"')
|
2026-08-17 18:32:41 +08:00
|
|
|
|
expect(source).toContain('<AppDialog v-model="editorOpen" class="container-editor-dialog"')
|
2026-08-12 16:31:46 +08:00
|
|
|
|
expect(source).toContain('max-height: 90vh')
|
|
|
|
|
|
expect(source).toContain('overflow-y: auto')
|
|
|
|
|
|
expect(source).toContain('grid-template-columns: minmax(0,1fr)')
|
2026-08-17 18:32:41 +08:00
|
|
|
|
expect(source).not.toContain('AppDrawer')
|
2026-08-12 16:31:46 +08:00
|
|
|
|
expect(source).not.toContain('<AppDrawer v-model="editorOpen"')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-18 17:40:40 +08:00
|
|
|
|
it('registers a list-authorized independent container detail page', () => {
|
|
|
|
|
|
const routes = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/routes.ts'), 'utf8')
|
|
|
|
|
|
const detail = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainerDetailView.vue'), 'utf8')
|
|
|
|
|
|
expect(routes).toContain("path: 'container-loading/:containerId'")
|
|
|
|
|
|
expect(routes).toContain("name: 'container-detail'")
|
|
|
|
|
|
expect(routes).toContain("permission: 'platform:firstleg:container:list'")
|
|
|
|
|
|
expect(detail).toContain('getContainer(containerId.value)')
|
|
|
|
|
|
expect(detail).toContain('getContainerOverview(containerId.value)')
|
|
|
|
|
|
expect(detail).toContain('<ContainerOverviewPanel')
|
|
|
|
|
|
expect(detail).toContain("@upload-document=\"openDocument\"")
|
|
|
|
|
|
expect(detail).toContain("@manage-exception=\"openException\"")
|
2026-08-26 11:10:48 +08:00
|
|
|
|
expect(detail).not.toContain("@preview-photo=\"previewPhoto\"")
|
|
|
|
|
|
expect(detail).toContain('<ContainerOverviewPanel')
|
2026-08-18 17:40:40 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-12 16:31:46 +08:00
|
|
|
|
it('keeps the prototype field sequence ahead of contract-only supplemental fields', () => {
|
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
2026-08-31 12:12:51 +08:00
|
|
|
|
const labels = ['真实集装箱号', '柜次号', '装柜仓库', '提单号', '船名', '航次', '箱型(货柜类型)', '船公司', '起运港', '目的港', '目的仓', '货柜容量(m³)']
|
2026-08-12 16:31:46 +08:00
|
|
|
|
let previous = -1
|
|
|
|
|
|
for (const label of labels) {
|
|
|
|
|
|
const current = source.indexOf(`label="${label}"`, previous + 1)
|
|
|
|
|
|
expect(current, label).toBeGreaterThan(previous)
|
|
|
|
|
|
previous = current
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-08-12 18:00:26 +08:00
|
|
|
|
|
|
|
|
|
|
it('renders container business actions as buttons without changing the transport gate', () => {
|
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
|
|
|
|
|
expect(source).toContain('size="small" type="primary" plain :aria-label="containerViewLabel')
|
2026-08-31 12:12:51 +08:00
|
|
|
|
expect(source).toContain("['WAIT_LOADING', 'EDITING'].includes(row.planStatus)")
|
|
|
|
|
|
expect(source).not.toContain("row.containerNo ? '修改集装箱号' : '录入集装箱号'")
|
|
|
|
|
|
expect(source).toContain('初次录入请使用“关联已放舱订舱”;后续纠错请进入预装清单。')
|
|
|
|
|
|
expect(source).toContain('containerNo: optional(detail.value?.containerNo || null)')
|
|
|
|
|
|
expect(source).not.toContain('containerNo: optional(form.containerNo)')
|
|
|
|
|
|
expect(source).not.toContain('updateContainerTransport')
|
2026-08-12 18:00:26 +08:00
|
|
|
|
expect(source).toContain("row.planStatus === 'EDITING' && canSmartLoading")
|
|
|
|
|
|
expect(source).not.toContain('canSmartLoading && row.containerNo')
|
|
|
|
|
|
expect(source).not.toContain('link type="primary" :aria-label="containerViewLabel')
|
2026-08-12 18:16:44 +08:00
|
|
|
|
expect(source).toContain(':loading="loading" stripe preference-key="container-loading-table"')
|
2026-08-12 18:00:26 +08:00
|
|
|
|
})
|
2026-08-13 11:57:44 +08:00
|
|
|
|
|
2026-08-31 12:40:01 +08:00
|
|
|
|
it('opens the existing released-booking workflow when entered from container detail', () => {
|
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
|
|
|
|
|
expect(source).toContain('route.query.linkContainerId')
|
|
|
|
|
|
expect(source).toContain('const target = await getContainer(linkContainerId)')
|
|
|
|
|
|
expect(source).toContain('await openLink(target)')
|
|
|
|
|
|
expect(source).toContain("void router.replace({ name: 'container-loading' })")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-31 12:12:51 +08:00
|
|
|
|
it('normalizes the mandatory booking-link container number and keeps it as the only initial-entry write', async () => {
|
|
|
|
|
|
vi.mocked(linkContainerOceanBooking).mockResolvedValue({} as never)
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
const vm = wrapper.vm as unknown as {
|
|
|
|
|
|
linkTarget: { id: string; version: number }
|
|
|
|
|
|
linkCandidates: Array<{ key: string; oceanBookingId: string; requirementId: string; disabled: boolean }>
|
|
|
|
|
|
linkCandidateKey: string
|
|
|
|
|
|
linkContainerNo: string
|
|
|
|
|
|
saveLink: () => Promise<void>
|
|
|
|
|
|
}
|
|
|
|
|
|
vm.linkTarget = { id: '9001', version: 7 }
|
|
|
|
|
|
vm.linkCandidates = [{ key: '101:202', oceanBookingId: '101', requirementId: '202', disabled: false }]
|
|
|
|
|
|
vm.linkCandidateKey = '101:202'
|
|
|
|
|
|
vm.linkContainerNo = ' mscu1234567 '
|
|
|
|
|
|
|
|
|
|
|
|
await vm.saveLink()
|
|
|
|
|
|
|
|
|
|
|
|
expect(linkContainerOceanBooking).toHaveBeenCalledWith('9001', {
|
|
|
|
|
|
oceanBookingId: '101', requirementId: '202', containerNo: 'MSCU1234567', expectedVersion: 7,
|
|
|
|
|
|
})
|
|
|
|
|
|
expect(vm.linkContainerNo).toBe('MSCU1234567')
|
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-26 11:10:48 +08:00
|
|
|
|
it('keeps document uploads on the routed detail page without duplicate list implementations', () => {
|
2026-08-13 11:57:44 +08:00
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
2026-08-26 11:10:48 +08:00
|
|
|
|
const detailSource = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainerDetailView.vue'), 'utf8')
|
|
|
|
|
|
expect(source).not.toContain('uploadLoadingDeclaration')
|
|
|
|
|
|
expect(source).not.toContain('uploadContainerClearance')
|
|
|
|
|
|
expect(source).not.toContain('uploadContainerDocument')
|
2026-08-28 16:54:02 +08:00
|
|
|
|
expect(detailSource).not.toContain('uploadLoadingDeclaration')
|
|
|
|
|
|
expect(detailSource).toContain('ContainerCustomsPanel')
|
2026-08-26 11:10:48 +08:00
|
|
|
|
expect(detailSource).toContain('uploadContainerClearance')
|
|
|
|
|
|
expect(detailSource).toContain('uploadContainerDocument')
|
2026-08-13 11:57:44 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-14 20:59:19 +08:00
|
|
|
|
it('creates an internal simulation without asking for real booking transport facts', () => {
|
2026-08-13 11:57:44 +08:00
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
|
|
|
|
|
expect(source).not.toContain('新增货柜')
|
2026-08-14 20:59:19 +08:00
|
|
|
|
expect(source).toContain('新增模拟柜')
|
|
|
|
|
|
expect(source).toContain('createContainerSimulation({ ...simulationForm })')
|
2026-08-17 12:31:27 +08:00
|
|
|
|
expect(source).toContain("channelCode: '', warehouseId: '', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000")
|
|
|
|
|
|
expect(source).toContain('CONTAINER_SPECIFICATIONS')
|
|
|
|
|
|
expect(source).toContain('selectSimulationContainerType')
|
2026-08-15 17:04:00 +08:00
|
|
|
|
expect(source).toContain('服务端会生成稳定的柜次号')
|
2026-08-19 09:42:12 +08:00
|
|
|
|
expect(source).toContain(':model-value="form.cabinetNo" disabled placeholder="系统生成"')
|
|
|
|
|
|
expect(source).not.toContain('v-model="form.cabinetNo"')
|
|
|
|
|
|
expect(source).not.toContain('cabinetNo: form.cabinetNo.trim()')
|
2026-08-13 11:57:44 +08:00
|
|
|
|
expect(source).not.toContain('openCreate')
|
2026-08-17 12:31:27 +08:00
|
|
|
|
expect(source).not.toContain("['20GP','40GP','40HQ','45HQ']")
|
|
|
|
|
|
expect(source).not.toContain('v-model="form.capacityCbm"')
|
|
|
|
|
|
expect(source).not.toContain('v-model="form.maxWeightKg"')
|
2026-08-13 11:57:44 +08:00
|
|
|
|
})
|
2026-08-14 20:59:19 +08:00
|
|
|
|
|
2026-08-25 20:34:52 +08:00
|
|
|
|
it('filters OB-260825-0183 for CAB-260825-0215 because the real warehouses differ', async () => {
|
2026-08-25 20:18:59 +08:00
|
|
|
|
vi.mocked(listContainers).mockResolvedValue({ records: [{
|
|
|
|
|
|
id: '215', planId: null, bookingNo: 'CB-260825-0215', cabinetNo: 'CAB-260825-0215', containerNo: null,
|
2026-08-25 20:34:52 +08:00
|
|
|
|
sourceType: 'SIMULATION', oceanBookingId: null, warehouseId: '193000000000200002', warehouseName: '东莞仓',
|
|
|
|
|
|
channelCode: 'EXX_RAIL_NY', channelName: '纽约铁路', containerType: '45HC', planStatus: 'EDITING',
|
|
|
|
|
|
capacityCbm: 86, maxWeightKg: 29000, version: 7,
|
|
|
|
|
|
}], page: 1, size: 20, total: 1, pages: 1 } as never)
|
|
|
|
|
|
vi.mocked(listOceanBookingLinkCandidates).mockResolvedValueOnce({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { attachTo: document.body, global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
await wrapper.find('button[aria-label="更多货柜操作"]').trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
Array.from(document.body.querySelectorAll<HTMLElement>('.el-dropdown-menu__item'))
|
|
|
|
|
|
.filter((item) => item.textContent?.includes('关联已放舱订舱')).at(-1)!.click()
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(listOceanBookingLinkCandidates).toHaveBeenCalledWith({ warehouseId: '193000000000200002', channelCode: 'EXX_RAIL_NY', containerType: '45HC', page: 1, size: 200 })
|
|
|
|
|
|
expect((wrapper.vm as unknown as { linkCandidates: unknown[] }).linkCandidates).toEqual([])
|
|
|
|
|
|
expect(readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8'))
|
|
|
|
|
|
.toContain('暂无符合当前装柜仓库、物流渠道和柜型的已放舱需求')
|
|
|
|
|
|
expect(getOceanBooking).not.toHaveBeenCalled()
|
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('keeps a three-field match selectable when only capacity snapshots differ and blocks duplicate waves', async () => {
|
|
|
|
|
|
let resolveCandidates!: (value: Awaited<ReturnType<typeof listOceanBookingLinkCandidates>>) => void
|
|
|
|
|
|
vi.mocked(listContainers).mockResolvedValue({ records: [{
|
|
|
|
|
|
id: '315', planId: null, bookingNo: 'CB-CAPACITY-SNAPSHOT', cabinetNo: 'CAB-CAPACITY-SNAPSHOT', containerNo: null,
|
|
|
|
|
|
sourceType: 'SIMULATION', oceanBookingId: null, warehouseId: '193000000000200002', warehouseName: '东莞仓',
|
|
|
|
|
|
channelCode: 'EXX_RAIL_NY', channelName: '纽约铁路', containerType: '45HC', planStatus: 'WAIT_LOADING',
|
2026-08-25 20:18:59 +08:00
|
|
|
|
capacityCbm: 86, maxWeightKg: 29000, version: 7,
|
|
|
|
|
|
}], page: 1, size: 20, total: 1, pages: 1 } as never)
|
2026-08-25 19:56:37 +08:00
|
|
|
|
vi.mocked(listOceanBookingLinkCandidates).mockReturnValueOnce(new Promise((resolve) => { resolveCandidates = resolve }))
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { attachTo: document.body, global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
await wrapper.find('button[aria-label="更多货柜操作"]').trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
const linkAction = Array.from(document.body.querySelectorAll<HTMLElement>('.el-dropdown-menu__item'))
|
2026-08-25 20:18:59 +08:00
|
|
|
|
.filter((item) => item.textContent?.includes('关联已放舱订舱')).at(-1)!
|
2026-08-25 19:56:37 +08:00
|
|
|
|
|
|
|
|
|
|
linkAction.click()
|
|
|
|
|
|
linkAction.click()
|
|
|
|
|
|
|
|
|
|
|
|
expect(listOceanBookingLinkCandidates).toHaveBeenCalledTimes(1)
|
2026-08-25 20:34:52 +08:00
|
|
|
|
expect(listOceanBookingLinkCandidates).toHaveBeenCalledWith({ warehouseId: '193000000000200002', channelCode: 'EXX_RAIL_NY', containerType: '45HC', page: 1, size: 200 })
|
2026-08-25 19:56:37 +08:00
|
|
|
|
expect(getOceanBooking).not.toHaveBeenCalled()
|
2026-08-25 20:34:52 +08:00
|
|
|
|
resolveCandidates({ records: [{ oceanBookingId: '315', bookingNo: 'OB-CAPACITY-SNAPSHOT', requirementId: '45315', containerType: '45HC', capacityCbm: 85, maxWeightKg: 28500, requiredQuantity: 1, allocatedQuantity: 0, remainingQuantity: 1, customerName: '客户', warehouseName: '东莞仓', channelName: '纽约铁路' }], page: 1, size: 200, total: 1, pages: 1 })
|
2026-08-25 19:56:37 +08:00
|
|
|
|
await flushPromises()
|
2026-08-25 20:34:52 +08:00
|
|
|
|
expect(document.body.textContent).toContain('OB-CAPACITY-SNAPSHOT · 45HC × 1')
|
2026-08-25 20:18:59 +08:00
|
|
|
|
expect((wrapper.vm as unknown as { linkCandidates: Array<{ disabled: boolean }> }).linkCandidates[0]?.disabled).toBe(false)
|
2026-08-25 19:56:37 +08:00
|
|
|
|
wrapper.unmount()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('discards a candidate response from a closed link session', async () => {
|
|
|
|
|
|
let resolveFirst!: (value: Awaited<ReturnType<typeof listOceanBookingLinkCandidates>>) => void
|
|
|
|
|
|
let resolveSecond!: (value: Awaited<ReturnType<typeof listOceanBookingLinkCandidates>>) => void
|
|
|
|
|
|
vi.mocked(listOceanBookingLinkCandidates)
|
|
|
|
|
|
.mockReturnValueOnce(new Promise((resolve) => { resolveFirst = resolve }))
|
|
|
|
|
|
.mockReturnValueOnce(new Promise((resolve) => { resolveSecond = resolve }))
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { attachTo: document.body, global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
await wrapper.find('button[aria-label="更多货柜操作"]').trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
Array.from(document.body.querySelectorAll<HTMLElement>('.el-dropdown-menu__item'))
|
2026-08-25 20:18:59 +08:00
|
|
|
|
.filter((item) => item.textContent?.includes('关联已放舱订舱')).at(-1)!.click()
|
2026-08-25 19:56:37 +08:00
|
|
|
|
await flushPromises()
|
2026-08-25 20:18:59 +08:00
|
|
|
|
wrapper.findAllComponents(AppDialog)
|
|
|
|
|
|
.find((dialog) => dialog.props('title') === '关联已放舱订舱')!.vm.$emit('update:modelValue', false)
|
2026-08-25 19:56:37 +08:00
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
await wrapper.find('button[aria-label="更多货柜操作"]').trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
Array.from(document.body.querySelectorAll<HTMLElement>('.el-dropdown-menu__item'))
|
|
|
|
|
|
.filter((item) => item.textContent?.includes('关联已放舱订舱')).at(-1)!.click()
|
|
|
|
|
|
resolveSecond({ records: [{ oceanBookingId: '303', bookingNo: 'OB-LATEST', requirementId: '404', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, requiredQuantity: 1, allocatedQuantity: 0, remainingQuantity: 1, customerName: '客户', warehouseName: '义乌仓', channelName: '美森正班海卡包税' }], page: 1, size: 200, total: 1, pages: 1 })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
resolveFirst({ records: [{ oceanBookingId: '101', bookingNo: 'OB-STALE', requirementId: '202', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, requiredQuantity: 1, allocatedQuantity: 0, remainingQuantity: 1, customerName: '客户', warehouseName: '义乌仓', channelName: '美森正班海卡包税' }], page: 1, size: 200, total: 1, pages: 1 })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(listOceanBookingLinkCandidates).toHaveBeenCalledTimes(2)
|
2026-08-25 20:18:59 +08:00
|
|
|
|
const candidateLabels = (wrapper.vm as unknown as { linkCandidates: Array<{ label: string }> }).linkCandidates.map((candidate) => candidate.label)
|
|
|
|
|
|
expect(candidateLabels.some((label) => label.includes('OB-LATEST'))).toBe(true)
|
|
|
|
|
|
expect(candidateLabels.some((label) => label.includes('OB-STALE'))).toBe(false)
|
2026-08-25 19:56:37 +08:00
|
|
|
|
wrapper.unmount()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('backfills a legacy trucking supplier name and submits the frozen id-name pair', async () => {
|
2026-08-27 10:12:20 +08:00
|
|
|
|
vi.mocked(listSuppliers).mockResolvedValue({ records: [{ id: '9007199254740998', tenantId: '1', name: '测试拖车供应商', category: 'TRUCKING', carrierCodes: [], region: 'CN', legalRepresentative: '张三', contactName: '李四', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-25T10:00:00', updatedBy: null, updatedAt: '2026-08-25T10:00:00', deleted: false, version: 0 }], page: 1, size: 200, total: 1, pages: 1 })
|
2026-08-31 12:12:51 +08:00
|
|
|
|
vi.mocked(getContainer).mockResolvedValueOnce({ id: '9001', planStatus: 'WAIT_LOADING', containerType: '40HC', warehouseId: '8', terminalTruckingCompanyId: null, terminalTruckingCompany: '测试拖车供应商' } as never)
|
2026-08-25 19:56:37 +08:00
|
|
|
|
vi.mocked(updateContainer).mockResolvedValue({} as never)
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().includes('编辑运输信息'))!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().trim() === '确定')!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(updateContainer).toHaveBeenCalledWith('9001', 0, expect.objectContaining({
|
|
|
|
|
|
terminalTruckingCompanyId: '9007199254740998',
|
|
|
|
|
|
terminalTruckingCompany: '测试拖车供应商',
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('clears the trucking supplier id and name together', async () => {
|
2026-08-27 10:12:20 +08:00
|
|
|
|
vi.mocked(listSuppliers).mockResolvedValue({ records: [{ id: '9007199254740998', tenantId: '1', name: '测试拖车供应商', category: 'TRUCKING', carrierCodes: [], region: 'CN', legalRepresentative: '张三', contactName: '李四', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-25T10:00:00', updatedBy: null, updatedAt: '2026-08-25T10:00:00', deleted: false, version: 0 }], page: 1, size: 200, total: 1, pages: 1 })
|
2026-08-31 12:12:51 +08:00
|
|
|
|
vi.mocked(getContainer).mockResolvedValueOnce({ id: '9001', planStatus: 'WAIT_LOADING', containerType: '40HC', warehouseId: '8', terminalTruckingCompanyId: '9007199254740998', terminalTruckingCompany: '测试拖车供应商' } as never)
|
2026-08-25 19:56:37 +08:00
|
|
|
|
vi.mocked(updateContainer).mockResolvedValue({} as never)
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().includes('编辑运输信息'))!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
const truckingSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((select) => select.props('placeholder') === '请选择拖车供应商')
|
|
|
|
|
|
truckingSelect!.vm.$emit('update:modelValue', '')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().trim() === '确定')!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(updateContainer).toHaveBeenCalledWith('9001', 0, expect.objectContaining({
|
|
|
|
|
|
terminalTruckingCompanyId: null,
|
|
|
|
|
|
terminalTruckingCompany: null,
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('blocks saving a legacy trucking supplier name that no longer matches an active option', async () => {
|
2026-08-31 12:12:51 +08:00
|
|
|
|
vi.mocked(getContainer).mockResolvedValueOnce({ id: '9001', planStatus: 'WAIT_LOADING', containerType: '40HC', warehouseId: '8', terminalTruckingCompanyId: null, terminalTruckingCompany: '已停用拖车供应商' } as never)
|
2026-08-25 19:56:37 +08:00
|
|
|
|
vi.mocked(updateContainer).mockClear()
|
|
|
|
|
|
const router = createTestRouter()
|
|
|
|
|
|
await router.push('/'); await router.isReady()
|
|
|
|
|
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().includes('编辑运输信息'))!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
const truckingSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((select) => select.props('placeholder') === '请选择拖车供应商')
|
|
|
|
|
|
expect(truckingSelect?.props('modelValue')).toBe('')
|
|
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().trim() === '确定')!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(updateContainer).not.toHaveBeenCalled()
|
|
|
|
|
|
expect(wrapper.text()).toContain('原拖车供应商已不可用,请重新选择合作中的拖车供应商')
|
2026-08-14 20:59:19 +08:00
|
|
|
|
})
|
2026-08-10 15:36:53 +08:00
|
|
|
|
})
|
2026-08-15 11:12:22 +08:00
|
|
|
|
|
|
|
|
|
|
describe('ContainersView trucking supplier contract', () => {
|
2026-08-17 20:01:51 +08:00
|
|
|
|
it('shows the frozen plan, exception and document aggregates without deriving them client-side', () => {
|
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
2026-08-28 16:54:02 +08:00
|
|
|
|
const fields = ['containerNo', 'planNo', 'planStatus', 'exceptionActive', 'cabinetNo', 'destinationClearanceDocumentStatus', 'deliveryOrderDocumentStatus', 'pickupOrderDocumentStatus']
|
2026-08-17 20:01:51 +08:00
|
|
|
|
let previous = source.indexOf("key: 'actions'")
|
|
|
|
|
|
for (const field of fields) {
|
|
|
|
|
|
const current = source.indexOf(`key: '${field}'`, previous + 1)
|
|
|
|
|
|
expect(current, field).toBeGreaterThan(previous)
|
|
|
|
|
|
previous = current
|
|
|
|
|
|
}
|
|
|
|
|
|
expect(source).toContain("MISSING: '未上传', UPLOADED: '已上传', COMPLETED: '已完成'")
|
2026-08-18 17:40:40 +08:00
|
|
|
|
const detail = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainerDetailView.vue'), 'utf8')
|
|
|
|
|
|
expect(detail).toContain("detail.planNo || '—'")
|
|
|
|
|
|
expect(detail).toContain("detail.exceptionActive ? '异常' : '正常'")
|
2026-08-17 20:01:51 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-15 11:12:22 +08:00
|
|
|
|
it('uses active TRUCKING suppliers without allowing free-text carrier names', () => {
|
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
|
|
|
|
|
expect(source).toContain("listSuppliers({ page: 1, size: 200, category: 'TRUCKING', status: 'ACTIVE' })")
|
|
|
|
|
|
expect(source).toContain("supplier.category === 'TRUCKING' && supplier.status === 'ACTIVE'")
|
|
|
|
|
|
expect(source).toContain('selectTruckingSupplier')
|
|
|
|
|
|
expect(source).toContain('请选择有效的合作中拖车供应商')
|
|
|
|
|
|
expect(source).not.toContain('allow-create')
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|