优化
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
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 { appendCarrierScheduleManualNode, getCarrierScheduleQuery, listCarrierScheduleQueries } from '../api/platformCarrierScheduleQueries'
|
||||
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(),
|
||||
appendCarrierScheduleManualNode: vi.fn(),
|
||||
}))
|
||||
vi.mock('../api/platformPortSchedules', () => ({ getPortSchedule: vi.fn() }))
|
||||
|
||||
@@ -17,7 +21,7 @@ const queryView = {
|
||||
carrierCode: 'COSCO', trackingType: 'CONTAINER_NO', trackingNo: 'CSNU1234567',
|
||||
officialUrl: 'https://elines.coscoshipping.com/ebusiness/', accessMode: 'DYNAMIC_IFRAME',
|
||||
status: 'MANUAL_REQUIRED' as const, triggerSource: 'OUTBOUND_CONFIRM',
|
||||
latestStandardNode: 'DEPARTED_WAREHOUSE' as const, latestNodeObservedAt: '2026-08-20T10:00:00',
|
||||
latestActualNode: 'DEPARTED_WAREHOUSE' as const, latestActualAt: '2026-08-20T10:00:00',
|
||||
lastErrorCode: 'DYNAMIC_IFRAME_REQUIRED', lastErrorMessage: '必须人工查询',
|
||||
resultPortScheduleId: '21', version: 1,
|
||||
},
|
||||
@@ -25,14 +29,15 @@ const queryView = {
|
||||
}
|
||||
|
||||
describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
let pinia: ReturnType<typeof createPinia>
|
||||
beforeEach(() => { vi.clearAllMocks(); pinia = createPinia(); setActivePinia(pinia); useAuthStore().permissions = ['platform:firstleg:port-schedule-query:manual'] })
|
||||
|
||||
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] } })
|
||||
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' })
|
||||
@@ -46,7 +51,7 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
|
||||
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' }, global: { plugins: [ElementPlus] } })
|
||||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2' }, global: { plugins: [ElementPlus, pinia] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('本页面不会创建或模拟任务')
|
||||
@@ -56,8 +61,8 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
|
||||
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: [], histories: [] })
|
||||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3', portScheduleId: '99' }, global: { plugins: [ElementPlus] } })
|
||||
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')
|
||||
@@ -65,21 +70,70 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
expect(getPortSchedule).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders canonical node location, actual time, source and stopped facts from detail DTO', async () => {
|
||||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, status: 'STOPPED', latestStandardNode: 'PICKUP_READY', stoppedAt: '2026-08-20T18:00:00', stopReason: '目的港可提柜后停止', resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||||
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', latestStandardNode: 'PICKUP_READY', stoppedAt: '2026-08-20T18:00:00', stopReason: '目的港可提柜后停止', resultPortScheduleId: null }, expired: false },
|
||||
nodes: [{ id: '31', providerCode: 'COSCO', providerNodeCode: 'PICKUP', standardNode: 'PICKUP_READY', nodeSequence: 60, 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' }],
|
||||
histories: [{ id: '41', action: 'STOP', fromVersion: 4, toVersion: 5, reason: '目的港可提柜后停止', operatorId: null, operatorName: '系统', createdAt: '2026-08-20T18:00:00' }],
|
||||
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] } })
|
||||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('已停止')
|
||||
expect(wrapper.text()).toContain('可提柜')
|
||||
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('目的港可提柜后停止')
|
||||
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')
|
||||
})
|
||||
|
||||
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 = { query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }, nodes: [], exceptions: [], histories: [] }
|
||||
vi.mocked(getCarrierScheduleQuery).mockResolvedValue(currentDetail)
|
||||
vi.mocked(appendCarrierScheduleManualNode)
|
||||
.mockRejectedValueOnce(new ApiResponseError('版本冲突', 'A0409', 409, null, 'req-409', 'trace-409'))
|
||||
.mockResolvedValueOnce(currentDetail)
|
||||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2' }, 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(appendCarrierScheduleManualNode).mock.calls[0]!
|
||||
const second = vi.mocked(appendCarrierScheduleManualNode).mock.calls[1]!
|
||||
expect(first[1]).toBeTruthy(); expect(second[1]).toBe(first[1])
|
||||
expect(second[2]).toEqual(first[2])
|
||||
expect(second[2]).toMatchObject({ expectedVersion: 1, standardNode: 'IN_TRANSIT', estimatedAt: '2026-08-22T10:00:00', actualAt: null, locationCode: 'PACIFIC', locationName: 'Pacific Ocean', reason: '船司官网人工核验', observedAt: expect.any(String) })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user