289 lines
26 KiB
TypeScript
289 lines
26 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { flushPromises, mount } from '@vue/test-utils'
|
||
import ElementPlus from 'element-plus'
|
||
import { createPinia, setActivePinia } from 'pinia'
|
||
import CarrierScheduleQueryPanel from './CarrierScheduleQueryPanel.vue'
|
||
import { getCarrierScheduleQuery, listCarrierScheduleQueries } from '../api/platformCarrierScheduleQueries'
|
||
import { appendContainerShippingNode, getContainerShippingNodes } from '../api/platformContainerShippingNodes'
|
||
import { getPortSchedule } from '../api/platformPortSchedules'
|
||
import { useAuthStore } from '@/core/auth/auth'
|
||
import { ApiResponseError } from '@/shared/api/http'
|
||
|
||
vi.mock('../api/platformCarrierScheduleQueries', () => ({
|
||
listCarrierScheduleQueries: vi.fn(),
|
||
getCarrierScheduleQuery: vi.fn(),
|
||
}))
|
||
vi.mock('../api/platformContainerShippingNodes', () => ({ getContainerShippingNodes: vi.fn(), appendContainerShippingNode: vi.fn() }))
|
||
vi.mock('../api/platformPortSchedules', () => ({ getPortSchedule: vi.fn() }))
|
||
|
||
const queryView = {
|
||
query: {
|
||
id: '11', queryNo: 'CSQ11', customerId: '1', oceanBookingId: '2', containerBookingId: '3',
|
||
carrierCode: 'COSCO', trackingType: 'CONTAINER_NO', trackingNo: 'CSNU1234567',
|
||
officialUrl: 'https://elines.coscoshipping.com/ebusiness/', accessMode: 'DYNAMIC_IFRAME',
|
||
status: 'MANUAL_REQUIRED' as const, triggerSource: 'OUTBOUND_CONFIRM',
|
||
latestActualNode: 'DEPARTED_WAREHOUSE' as const, latestActualAt: '2026-08-20T10:00:00',
|
||
lastErrorCode: 'DYNAMIC_IFRAME_REQUIRED', lastErrorMessage: '必须人工查询',
|
||
resultPortScheduleId: '21', version: 1,
|
||
},
|
||
expired: true,
|
||
}
|
||
|
||
describe('CarrierScheduleQueryPanel loading states', () => {
|
||
let pinia: ReturnType<typeof createPinia>
|
||
beforeEach(() => { vi.clearAllMocks(); pinia = createPinia(); setActivePinia(pinia); useAuthStore().permissions = ['platform:firstleg:port-schedule-query:manual']; vi.mocked(getContainerShippingNodes).mockResolvedValue({ containerBookingId: '3', currentNode: null, currentNodeAt: null, nodes: [] }) })
|
||
|
||
it('keeps list facts visible when history and linked schedule both fail', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [queryView], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockRejectedValue(new Error('detail unavailable'))
|
||
vi.mocked(getPortSchedule).mockRejectedValue(new Error('schedule unavailable'))
|
||
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
expect(listCarrierScheduleQueries).toHaveBeenCalledWith({ page: 1, size: 200, oceanBookingId: '2', containerBookingId: '3' })
|
||
expect(getCarrierScheduleQuery).toHaveBeenCalledWith('11')
|
||
expect(getPortSchedule).toHaveBeenCalledWith('21')
|
||
expect(wrapper.text()).toContain('CSQ11')
|
||
expect(wrapper.text()).toContain('标准节点与变更历史暂不可用')
|
||
expect(wrapper.text()).toContain('关联船期结果暂不可用')
|
||
expect(wrapper.text()).toContain('DYNAMIC_IFRAME_REQUIRED')
|
||
})
|
||
|
||
it('shows an explicit non-fabricated empty state and performs no detail reads', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', warehouseTimeline: [{ eventType: 'PDA_COMPLETE_LOADING', detail: 'complete-key-technical', operatorName: '仓库员', occurredAt: '2026-08-20T08:00:00' }] }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
expect(wrapper.text()).toContain('货柜履约轨迹')
|
||
expect(wrapper.text()).toContain('完成装柜')
|
||
expect(wrapper.text()).toContain('仓内操作')
|
||
expect(wrapper.text()).toContain('当前未开启跟踪任务')
|
||
expect(wrapper.findAll('.tracking-prompt')).toHaveLength(1)
|
||
expect(getCarrierScheduleQuery).not.toHaveBeenCalled()
|
||
expect(getPortSchedule).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('shows manual entry without creating a tracking task', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
const button = wrapper.findAll('button').find((item) => item.text().includes('人工补录节点'))
|
||
expect(button).toBeTruthy()
|
||
await button!.trigger('click')
|
||
await flushPromises()
|
||
|
||
expect(getContainerShippingNodes).toHaveBeenCalledWith('3')
|
||
expect(wrapper.findComponent({ name: 'AppDialog' }).props('modelValue')).toBe(true)
|
||
})
|
||
|
||
it('filters large no-op status batches, keeps real actions, deduplicates only adjacent explicit event keys and sorts newest first', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||
const noops = Array.from({ length: 40 }, (_, index) => ({
|
||
eventType: 'STATUS_UPDATE', action: 'STATUS_UPDATE', fromStatus: 'EDITING', toStatus: 'EDITING',
|
||
detail: index % 2 ? null : '预装柜中→预装柜中', eventId: `2092438843630398${String(index).padStart(3, '0')}`,
|
||
operatorName: '系统', occurredAt: `2026-08-27T09:${String(index).padStart(2, '0')}:00`,
|
||
}))
|
||
const timeline = [
|
||
...noops,
|
||
{ eventType: 'STATUS_UPDATE', action: 'STATUS_UPDATE', fromStatus: 'EDITING', toStatus: 'LOADING', detail: null, eventId: '2092438843630399001', operatorName: '调度员', occurredAt: '2026-08-27T10:00:00' },
|
||
{ eventType: 'STATUS_UPDATE', action: 'STATUS_UPDATE', fromStatus: 'LOADING', toStatus: 'LOADING', detail: null, changedFields: ['containerNo'], eventId: '2092438843630399002', operatorName: '调度员', occurredAt: '2026-08-27T10:01:00' },
|
||
{ eventType: 'STATUS_UPDATE', action: 'PUSH_PRELOAD', fromStatus: 'EDITING', toStatus: 'EDITING', detail: null, eventId: '2092438843630399003', operatorName: '调度员', occurredAt: '2026-08-27T10:02:00' },
|
||
{ eventType: 'PDA_BATCH', action: 'PDA_BATCH', fromStatus: 'LOADING', toStatus: 'LOADING', detail: '提交拣货结果', eventId: '2092438843630399004', operatorName: '仓库员', occurredAt: '2026-08-27T10:03:00' },
|
||
{ eventType: 'PDA_BATCH', action: 'PDA_BATCH', fromStatus: 'LOADING', toStatus: 'LOADING', detail: '提交拣货结果', eventId: '2092438843630399004', operatorName: '仓库员', occurredAt: '2026-08-27T10:04:00' },
|
||
{ eventType: 'STATUS_UPDATE', action: 'CONFIRM_PRELOAD', fromStatus: 'EDITING', toStatus: 'EDITING', detail: null, idempotencyKey: '2092438843630399005', operatorName: '复核员', occurredAt: '2026-08-27T10:05:00' },
|
||
{ eventType: 'TRANSPORT_UPDATE', action: 'TRANSPORT_UPDATE', detail: '更新集装箱号', eventId: '2092438843630399006', operatorName: '操作员甲', occurredAt: '2026-08-27T10:06:00' },
|
||
{ eventType: 'TRANSPORT_UPDATE', action: 'TRANSPORT_UPDATE', detail: '更新集装箱号', eventId: '2092438843630399007', operatorName: '操作员乙', occurredAt: '2026-08-27T10:07:00' },
|
||
]
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', warehouseTimeline: timeline }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
expect(wrapper.find('.history-count').text()).toBe('仓内 7 条 · 共 7 条')
|
||
const rows = wrapper.findAll('.fulfillment-timeline li')
|
||
expect(rows).toHaveLength(7)
|
||
expect(rows[0]?.text()).toContain('操作员乙')
|
||
expect(rows[6]?.text()).toContain('预装柜中 → 拣货中')
|
||
expect(rows.filter((row) => row.text().includes('提交拣货结果'))).toHaveLength(1)
|
||
expect(wrapper.text()).toContain('推送预装清单')
|
||
expect(wrapper.text()).toContain('确认预装清单')
|
||
expect(wrapper.text()).toContain('业务字段已更新')
|
||
expect(wrapper.text()).not.toContain('2092438843630398000')
|
||
expect(rows.filter((row) => row.text().includes('更新集装箱号'))).toHaveLength(2)
|
||
})
|
||
|
||
it('falls back to the booking/container task when an automatic task has no linked port schedule', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }, nodes: [], exceptions: [], histories: [] })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3', portScheduleId: '99' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
expect(wrapper.text()).toContain('CSQ11')
|
||
expect(getCarrierScheduleQuery).toHaveBeenCalledWith('11')
|
||
expect(getPortSchedule).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('does not expose a circuit-breaker exception name stored by an earlier scheduler attempt', async () => {
|
||
const failed = { ...queryView.query, lastErrorCode: 'PROVIDER_CALL_FAILED', lastErrorMessage: 'NoFallbackAvailableException', resultPortScheduleId: null }
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: failed }], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: failed }, nodes: [], exceptions: [], histories: [] })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
expect(wrapper.text()).toContain('轨迹供应商暂时不可用,请稍后重试或人工补录节点。')
|
||
expect(wrapper.text()).not.toContain('NoFallbackAvailableException')
|
||
})
|
||
|
||
it('renders canonical node location, actual time, source and empty-return stop facts from detail DTO', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, status: 'STOPPED', latestActualNode: 'EMPTY_RETURNED', latestActualAt: '2026-08-20T17:45:00', stoppedAt: '2026-08-20T18:00:00', stopReason: '已还柜', resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({
|
||
query: { query: { ...queryView.query, status: 'STOPPED', latestActualNode: 'EMPTY_RETURNED', latestActualAt: '2026-08-20T17:45:00', stoppedAt: '2026-08-20T18:00:00', stopReason: '已还柜', resultPortScheduleId: null }, expired: false },
|
||
nodes: [{ id: '31', providerCode: 'COSCO', providerEventId: 'provider-event-31', providerNodeCode: 'EMPTY_RETURN', standardNode: 'EMPTY_RETURNED', nodeSequence: 100, observedAt: '2026-08-20T17:50:00', estimatedAt: null, actualAt: '2026-08-20T17:45:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'provider-event-31', rawPayloadHash: 'a'.repeat(64), createdAt: '2026-08-20T17:51:00' }],
|
||
exceptions: [],
|
||
histories: [{ id: '41', action: 'STOP', fromVersion: 4, toVersion: 5, reason: '已还柜', operatorId: null, operatorName: '系统', createdAt: '2026-08-20T18:00:00' }],
|
||
})
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
expect(wrapper.text()).toContain('已停止 · 已还柜')
|
||
expect(wrapper.text()).toContain('USLAX · Los Angeles')
|
||
expect(wrapper.text()).toContain('2026-08-20 17:45')
|
||
expect(wrapper.text()).toContain('COSCO')
|
||
expect(wrapper.text()).toContain('已还柜')
|
||
})
|
||
|
||
it('labels forecast-only facts as estimated without promoting latest actual progress', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, latestActualNode: 'DEPARTED_WAREHOUSE', latestActualAt: '2026-08-20T08:55:00', resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({
|
||
query: { query: { ...queryView.query, latestActualNode: 'DEPARTED_WAREHOUSE', latestActualAt: '2026-08-20T08:55:00', resultPortScheduleId: null }, expired: false },
|
||
nodes: [
|
||
{ id: '11', providerCode: 'FIRSTLEG', providerEventId: 'outbound-11', providerNodeCode: 'OUTBOUND_CONFIRM', standardNode: 'DEPARTED_WAREHOUSE', nodeSequence: 10, observedAt: '2026-08-20T09:00:00', estimatedAt: null, actualAt: '2026-08-20T08:55:00', locationCode: null, locationName: null, sourceReference: null, rawPayloadHash: 'b'.repeat(64), createdAt: '2026-08-20T09:00:01' },
|
||
{ id: '12', providerCode: 'HEDE', providerEventId: 'hede-estimate-12', providerNodeCode: '预抵', standardNode: 'ARRIVED_DESTINATION_PORT', nodeSequence: 60, observedAt: '2026-08-20T10:00:00', estimatedAt: '2026-08-28T12:00:00', actualAt: null, locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'hede-estimate', rawPayloadHash: 'c'.repeat(64), createdAt: '2026-08-20T10:00:01' },
|
||
], exceptions: [{ id: '51', providerCode: 'HEDE', providerEventId: 'hold-51', providerStatus: 'Customs hold', exceptionCode: 'CUSTOMS_HOLD', active: true, observedAt: '2026-08-20T11:00:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'hede-hold', rawPayloadHash: 'd'.repeat(64), createdAt: '2026-08-20T11:00:01' }], histories: [],
|
||
})
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
expect(wrapper.text()).toContain('最新实际节点已离开国内仓库')
|
||
expect(wrapper.text()).toContain('预计 2026-08-28 12:00')
|
||
expect(wrapper.text()).not.toContain('最新实际节点船舶已到目的港')
|
||
expect(wrapper.text()).toContain('异常')
|
||
expect(wrapper.text()).toContain('海关扣留 · 发生')
|
||
expect(wrapper.text()).toContain('Customs hold')
|
||
expect(wrapper.text()).toContain('船运跟踪 · HEDE · hede-hold')
|
||
})
|
||
|
||
it('merges warehouse, actual, estimated, manual and exception facts with stable ordering and deduplication', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||
const actual = { id: '61', providerCode: 'COSCO', providerEventId: 'actual-61', providerNodeCode: 'LOAD', standardNode: 'LOADED_ON_VESSEL' as const, nodeSequence: 30, observedAt: '2026-08-21T10:05:00', estimatedAt: null, actualAt: '2026-08-21T10:00:00', locationCode: 'CNNGB', locationName: '宁波港', sourceReference: 'actual-source', rawPayloadHash: 'e'.repeat(64), createdAt: '2026-08-21T10:06:00' }
|
||
const estimated = { id: '62', providerCode: 'COSCO', providerEventId: 'estimate-62', providerNodeCode: 'ETA', standardNode: 'ARRIVED_DESTINATION_PORT' as const, nodeSequence: 60, observedAt: '2026-08-20T10:00:00', estimatedAt: '2026-08-21T10:00:00', actualAt: null, locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'estimate-source', rawPayloadHash: 'f'.repeat(64), createdAt: '2026-08-20T10:01:00' }
|
||
const manual = { id: '63', providerCode: 'MANUAL', providerEventId: 'manual-63', providerNodeCode: 'TRANSIT', standardNode: 'IN_TRANSIT' as const, nodeSequence: 50, observedAt: '2026-08-21T09:00:00', estimatedAt: null, actualAt: '2026-08-21T09:00:00', locationCode: null, locationName: '太平洋', sourceReference: '人工核验', rawPayloadHash: 'g'.repeat(64), createdAt: '2026-08-21T09:01:00' }
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }, nodes: [estimated, actual, { ...actual, id: '64' }, manual], exceptions: [{ id: '65', providerCode: 'COSCO', providerEventId: 'hold-65', providerStatus: 'Terminal hold', exceptionCode: 'TERMINAL_HOLD', active: true, observedAt: '2026-08-21T08:30:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'hold-source', rawPayloadHash: 'h'.repeat(64), createdAt: '2026-08-21T08:31:00' }], histories: [{ id: '66', action: 'MANUAL_NODE', fromVersion: 1, toVersion: 2, reason: '人工核验', operatorId: '1', operatorName: '管理员', createdAt: '2026-08-21T09:01:00' }] })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', warehouseTimeline: [{ eventType: 'PDA_COMPLETE_LOADING', detail: '装柜完成', operatorName: '仓库员', occurredAt: '2026-08-20T08:00:00' }] }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
const rows = wrapper.findAll('.fulfillment-timeline li')
|
||
expect(rows.filter((row) => row.text().includes('已装船'))).toHaveLength(1)
|
||
expect(rows[0]?.text()).toContain('已装船')
|
||
expect(rows[1]?.text()).toContain('预计 2026-08-21 10:00')
|
||
expect(rows[1]?.text()).toContain('船运节点')
|
||
expect(rows.map((row) => row.text()).join('|')).toContain('人工补录')
|
||
expect(rows.map((row) => row.text()).join('|')).toContain('码头扣留 · 发生')
|
||
expect(wrapper.text()).toContain('查询任务摘要、异常与变更历史')
|
||
expect(wrapper.text()).toContain('人工补录标准节点')
|
||
})
|
||
|
||
it('shows only the latest duplicate actual node in the primary trajectory and keeps the older fact expandable', async () => {
|
||
const task = { ...queryView.query, latestActualNode: 'PICKED_UP' as const, latestActualAt: '2026-09-24T10:00:00', resultPortScheduleId: null }
|
||
const older = { id: 'pickup-1', providerCode: 'MANUAL', providerEventId: 'pickup-old', providerNodeCode: 'PICKED_UP', standardNode: 'PICKED_UP' as const, nodeSequence: 90, observedAt: '2026-09-09T10:05:00', estimatedAt: null, actualAt: '2026-09-09T10:00:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: '人工提柜-旧', rawPayloadHash: '1'.repeat(64), createdAt: '2026-09-09T10:06:00' }
|
||
const latest = { ...older, id: 'pickup-2', providerEventId: 'pickup-latest', observedAt: '2026-09-24T10:05:00', actualAt: '2026-09-24T10:00:00', sourceReference: '人工提柜-最新', rawPayloadHash: '2'.repeat(64), createdAt: '2026-09-24T10:06:00' }
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: task, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: task, expired: false }, nodes: [older, latest], exceptions: [], histories: [] })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
const primaryPickupRows = wrapper.findAll('.fulfillment-timeline li').filter((row) => row.text().includes('已提柜'))
|
||
expect(primaryPickupRows).toHaveLength(1)
|
||
expect(primaryPickupRows[0]!.text()).toContain('2026-09-24 10:00')
|
||
expect(primaryPickupRows[0]!.text()).not.toContain('2026-09-09 10:00')
|
||
expect(wrapper.find('.duplicate-node-history').text()).toContain('历史重复实际节点(1)')
|
||
expect(wrapper.find('.duplicate-node-history').text()).toContain('2026-09-09 10:00')
|
||
expect(wrapper.find('.duplicate-node-history').text()).toContain('人工提柜-旧')
|
||
})
|
||
|
||
it('allows correcting the current node without sequence restrictions', async () => {
|
||
const task = { ...queryView.query, latestActualNode: 'PICKED_UP' as const, latestActualAt: '2026-09-24T10:00:00', resultPortScheduleId: null }
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: task, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: task, expired: false }, nodes: [], exceptions: [], histories: [] })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
await wrapper.findAll('button').find((button) => button.text().includes('人工补录节点'))!.trigger('click')
|
||
const pickers = wrapper.findAllComponents({ name: 'ElDatePicker' })
|
||
pickers[1]!.vm.$emit('update:modelValue', '2026-09-25T10:00:00')
|
||
await flushPromises()
|
||
const options = wrapper.findAllComponents({ name: 'ElOption' })
|
||
expect(options.find((option) => option.props('value') === 'DEPARTED_WAREHOUSE')!.props('disabled')).toBeFalsy()
|
||
expect(options.find((option) => option.props('value') === 'PICKED_UP')!.props('disabled')).toBeFalsy()
|
||
expect(options.find((option) => option.props('value') === 'EMPTY_RETURNED')!.props('disabled')).toBe(false)
|
||
|
||
wrapper.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'PICKED_UP')
|
||
wrapper.findAllComponents({ name: 'ElInput' }).find((input) => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', '重复提柜事实')
|
||
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
|
||
expect(appendContainerShippingNode).toHaveBeenCalled()
|
||
})
|
||
|
||
it('hides the manual action without the frozen permission while retaining fulfillment facts', async () => {
|
||
useAuthStore().permissions = []
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }, nodes: [], exceptions: [], histories: [] })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', warehouseTimeline: [{ eventType: 'PDA_BATCH', detail: '拣货结果已提交', operatorName: '仓库员', occurredAt: '2026-08-20T07:00:00' }] }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
expect(wrapper.text()).toContain('提交拣货结果')
|
||
expect(wrapper.findAll('button').some((button) => button.text().includes('人工补录节点'))).toBe(false)
|
||
})
|
||
|
||
it('keeps the form context and dialog-scoped idempotency key when a conflict is retried', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||
const currentDetail = { containerBookingId: '3', currentNode: null, currentNodeAt: null, nodes: [] }
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }, nodes: [], exceptions: [], histories: [] })
|
||
vi.mocked(appendContainerShippingNode)
|
||
.mockRejectedValueOnce(new ApiResponseError('版本冲突', 'A0409', 409, null, 'req-409', 'trace-409'))
|
||
.mockResolvedValueOnce(currentDetail)
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
await wrapper.findAll('button').find((button) => button.text().includes('人工补录节点'))!.trigger('click')
|
||
const select = wrapper.findComponent({ name: 'ElSelect' }); select.vm.$emit('update:modelValue', 'IN_TRANSIT')
|
||
const pickers = wrapper.findAllComponents({ name: 'ElDatePicker' }); pickers[0]!.vm.$emit('update:modelValue', '2026-08-22T10:00:00')
|
||
const inputs = wrapper.findAllComponents({ name: 'ElInput' })
|
||
inputs.find((input) => input.props('placeholder') === '可选,如 USLAX')!.vm.$emit('update:modelValue', 'PACIFIC')
|
||
inputs.find((input) => String(input.props('maxlength')) === '128')!.vm.$emit('update:modelValue', 'Pacific Ocean')
|
||
inputs.find((input) => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', '船司官网人工核验')
|
||
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
|
||
expect(wrapper.text()).toContain('补录未成功,已保留当前填写内容')
|
||
expect(wrapper.text()).toContain('req-409'); expect(wrapper.text()).toContain('trace-409')
|
||
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
|
||
|
||
const first = vi.mocked(appendContainerShippingNode).mock.calls[0]!
|
||
const second = vi.mocked(appendContainerShippingNode).mock.calls[1]!
|
||
expect(first[1]).toBeTruthy(); expect(second[1]).toBe(first[1])
|
||
expect(second[2]).toEqual(first[2])
|
||
expect(second[2]).toMatchObject({ standardNode: 'IN_TRANSIT', estimatedAt: '2026-08-22T10:00:00', actualAt: null, locationCode: 'PACIFIC', locationName: 'Pacific Ocean', reason: '船司官网人工核验', observedAt: expect.any(String) })
|
||
})
|
||
|
||
it('requires an estimated or actual time before calling the manual-node adapter', async () => {
|
||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }, nodes: [], exceptions: [], histories: [] })
|
||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||
await flushPromises()
|
||
|
||
await wrapper.findAll('button').find((button) => button.text().includes('人工补录节点'))!.trigger('click')
|
||
wrapper.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'PICKUP_READY')
|
||
wrapper.findAllComponents({ name: 'ElInput' }).find((input) => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', '人工核验可提柜')
|
||
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
|
||
|
||
expect(wrapper.text()).toContain('预计时间和实际时间至少填写一项')
|
||
expect(appendContainerShippingNode).not.toHaveBeenCalled()
|
||
})
|
||
})
|