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 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() }) })