fix(admin): allow manual container nodes without tracking
This commit is contained in:
@@ -3,7 +3,8 @@ import { flushPromises, mount } from '@vue/test-utils'
|
||||
import ElementPlus from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import CarrierScheduleQueryPanel from './CarrierScheduleQueryPanel.vue'
|
||||
import { appendCarrierScheduleManualNode, ensureCarrierScheduleQueryForContainer, getCarrierScheduleQuery, listCarrierScheduleQueries } from '../api/platformCarrierScheduleQueries'
|
||||
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'
|
||||
@@ -11,9 +12,8 @@ import { ApiResponseError } from '@/shared/api/http'
|
||||
vi.mock('../api/platformCarrierScheduleQueries', () => ({
|
||||
listCarrierScheduleQueries: vi.fn(),
|
||||
getCarrierScheduleQuery: vi.fn(),
|
||||
ensureCarrierScheduleQueryForContainer: vi.fn(),
|
||||
appendCarrierScheduleManualNode: vi.fn(),
|
||||
}))
|
||||
vi.mock('../api/platformContainerShippingNodes', () => ({ getContainerShippingNodes: vi.fn(), appendContainerShippingNode: vi.fn() }))
|
||||
vi.mock('../api/platformPortSchedules', () => ({ getPortSchedule: vi.fn() }))
|
||||
|
||||
const queryView = {
|
||||
@@ -31,7 +31,7 @@ const queryView = {
|
||||
|
||||
describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
let pinia: ReturnType<typeof createPinia>
|
||||
beforeEach(() => { vi.clearAllMocks(); pinia = createPinia(); setActivePinia(pinia); useAuthStore().permissions = ['platform:firstleg:port-schedule-query:manual'] })
|
||||
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 })
|
||||
@@ -58,15 +58,14 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
expect(wrapper.text()).toContain('货柜履约轨迹')
|
||||
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 an existing task and ensures the task on click', async () => {
|
||||
it('shows manual entry without creating a tracking task', async () => {
|
||||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||
vi.mocked(ensureCarrierScheduleQueryForContainer).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, customerId: '0', resultPortScheduleId: null } }, nodes: [], exceptions: [], histories: [] })
|
||||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||||
await flushPromises()
|
||||
|
||||
@@ -75,7 +74,7 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
await button!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(ensureCarrierScheduleQueryForContainer).toHaveBeenCalledWith('3', expect.any(String))
|
||||
expect(getContainerShippingNodes).toHaveBeenCalledWith('3')
|
||||
expect(wrapper.findComponent({ name: 'AppDialog' }).props('modelValue')).toBe(true)
|
||||
})
|
||||
|
||||
@@ -199,7 +198,7 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
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' }, global: { plugins: [ElementPlus, pinia] } })
|
||||
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('已提柜'))
|
||||
@@ -211,11 +210,11 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
expect(wrapper.find('.duplicate-node-history').text()).toContain('人工提柜-旧')
|
||||
})
|
||||
|
||||
it('disables same and earlier actual nodes and rejects an invalid actual submission defensively', async () => {
|
||||
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' }, global: { plugins: [ElementPlus, pinia] } })
|
||||
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')
|
||||
@@ -223,15 +222,14 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
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')).toBe(true)
|
||||
expect(options.find((option) => option.props('value') === 'PICKED_UP')!.props('disabled')).toBe(true)
|
||||
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(wrapper.text()).toContain('实际补录只能选择严格晚于当前实际进度的节点')
|
||||
expect(appendCarrierScheduleManualNode).not.toHaveBeenCalled()
|
||||
expect(appendContainerShippingNode).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides the manual action without the frozen permission while retaining fulfillment facts', async () => {
|
||||
@@ -246,12 +244,12 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
|
||||
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)
|
||||
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' }, global: { plugins: [ElementPlus, pinia] } })
|
||||
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')
|
||||
@@ -266,17 +264,17 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
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]!
|
||||
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({ expectedVersion: 1, standardNode: 'IN_TRANSIT', estimatedAt: '2026-08-22T10:00:00', actualAt: null, locationCode: 'PACIFIC', locationName: 'Pacific Ocean', reason: '船司官网人工核验', observedAt: expect.any(String) })
|
||||
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' }, global: { plugins: [ElementPlus, pinia] } })
|
||||
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')
|
||||
@@ -285,6 +283,6 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('预计时间和实际时间至少填写一项')
|
||||
expect(appendCarrierScheduleManualNode).not.toHaveBeenCalled()
|
||||
expect(appendContainerShippingNode).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user