428 lines
30 KiB
TypeScript
428 lines
30 KiB
TypeScript
import ElementPlus, { ElMessageBox } from 'element-plus'
|
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import ContainerDetailView from './ContainerDetailView.vue'
|
|
import { downloadContainerClearance, downloadContainerDocument, getContainer, getContainerClearance, getContainerOverview, uploadContainerClearance, uploadContainerDocument } from '../api/platformContainerLoading'
|
|
import { completeLoadingDeclaration, downloadLoadingDeclaration, getLoadingDeclaration, uploadLoadingDeclaration } from '@/modules/loading-lists'
|
|
import { confirmContainerOutboundDispatch, getContainerOutboundDispatch } from '@/modules/outbound-dispatch/api/platformOutboundDispatch'
|
|
import type { ContainerOutboundDispatchDetail } from '@/modules/outbound-dispatch/types'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
|
|
const { accessCan, routerPush, carrierMounts } = vi.hoisted(() => ({ accessCan: vi.fn(), routerPush: vi.fn(), carrierMounts: { value: 0 } }))
|
|
vi.mock('vue-router', () => ({ useRoute: () => ({ params: { containerId: '2092438843630391297' }, query: {} }), useRouter: () => ({ push: routerPush }) }))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: accessCan }) }))
|
|
vi.mock('@/modules/port-schedules', () => ({ CarrierScheduleQueryPanel: { template: '<div data-test="carrier-panel" />', mounted: () => { carrierMounts.value += 1 } } }))
|
|
vi.mock('@/modules/loading-lists', () => ({ uploadLoadingDeclaration: vi.fn(), completeLoadingDeclaration: vi.fn(), downloadLoadingDeclaration: vi.fn(), getLoadingDeclaration: vi.fn() }))
|
|
vi.mock('@/modules/outbound-dispatch/api/platformOutboundDispatch', () => ({ getContainerOutboundDispatch: vi.fn(), confirmContainerOutboundDispatch: vi.fn() }))
|
|
vi.mock('../api/platformContainerLoading', () => ({
|
|
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(),
|
|
downloadContainerClearance: vi.fn(),
|
|
}))
|
|
|
|
const detail = { id: '2092438843630391297', planId: '2092438843630391401', planNo: 'LP-260825-0181', bookingNo: 'CB-1', cabinetNo: 'CAB-1', containerNo: 'C11111111', warehouseId: '193000000000200002', warehouseName: '东莞仓', containerType: '45HC', planStatus: 'COMPLETED', version: 8 } as never
|
|
const overview = {
|
|
container: { id: '2092438843630391297', bookingNo: 'CB-1', cabinetNo: 'CAB-1', containerNo: 'C11111111', status: 'COMPLETED', warehouseId: '193000000000200002', warehouseName: '东莞仓', voyageNo: null, vesselName: null, version: 8 },
|
|
loadingPlanId: '2092438843630391401', pda: null,
|
|
documents: [{ id: '2092438843630391501', documentType: 'CUSTOMS_DECLARATION', fileId: '2092438843630391502', fileName: 'old.pdf', fileHash: null, uploadedBy: '1', uploadedByName: '操作员', uploadedAt: '2026-08-26T10:00:00', version: 5, completed: false }],
|
|
exception: { active: false, reason: null, markedBy: null, markedByName: null, markedAt: null }, exceptionHistory: [], timeline: [],
|
|
}
|
|
const outboundDetail = {
|
|
aggregate: {
|
|
containerBookingId: '2092438843630391297', cabinetNo: 'CAB-1', containerNo: 'C11111111',
|
|
loadingPlanId: '2092438843630391401', loadingPlanStatus: 'COMPLETED', truckAppointmentStatus: 'ARRIVED', dispatchStatus: 'READY_FOR_DISPATCH', version: 4,
|
|
inbounds: [{ orderId: '11', orderNo: 'O-1', inboundId: '2092438843630391501', inboundCode: 'IN-1', tenantId: '22', inboundStatus: 'LOADED' }],
|
|
gate: { passed: true, checkedAt: '2026-08-26T12:00:00', inbounds: [{ inboundId: '2092438843630391501', inboundCode: 'IN-1', gate: { passed: true, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: true, actual: 'COMPLETED', requirement: 'COMPLETED' }] } }] },
|
|
},
|
|
timeline: [],
|
|
} as ContainerOutboundDispatchDetail
|
|
|
|
function mountView(customs = true, outbound = true) {
|
|
accessCan.mockImplementation((permission: string) => permission === 'platform:firstleg:container:edit'
|
|
|| (customs && permission === 'platform:firstleg:customs:manage')
|
|
|| permission === 'platform:firstleg:loading-list:list'
|
|
|| (outbound && ['platform:firstleg:outbound-dispatch:view', 'platform:firstleg:outbound-dispatch:confirm'].includes(permission)))
|
|
return mount(ContainerDetailView, { global: { plugins: [ElementPlus] } })
|
|
}
|
|
function documentArticle(wrapper: VueWrapper, label: string) { return wrapper.findAll('.document-table tbody tr').find((item) => item.text().includes(label))! }
|
|
function documentButton(wrapper: VueWrapper, label: string, buttonText: string) { return documentArticle(wrapper, label).findAll('button').find((button) => button.text().includes(buttonText))! }
|
|
|
|
describe('ContainerDetailView', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:document') })
|
|
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
|
|
carrierMounts.value = 0
|
|
vi.mocked(getContainer).mockResolvedValue(detail)
|
|
vi.mocked(getContainerOverview).mockResolvedValue(overview as never)
|
|
vi.mocked(getContainerClearance).mockResolvedValue(null)
|
|
vi.mocked(uploadLoadingDeclaration).mockResolvedValue({ id: '2092438843630391501', documentType: 'CUSTOMS_DECLARATION', fileId: '2092438843630391502', fileName: 'customs-new.pdf', fileHash: 'hash', uploadedBy: '1', uploadedByName: '操作员', uploadedAt: '2026-08-26T12:00:00', completedBy: null, completedByName: null, completedAt: null, version: 6 } as never)
|
|
vi.mocked(completeLoadingDeclaration).mockResolvedValue({} as never)
|
|
vi.mocked(downloadLoadingDeclaration).mockResolvedValue(new Blob(['customs']))
|
|
vi.mocked(downloadContainerClearance).mockResolvedValue(new Blob(['clearance']))
|
|
vi.mocked(getLoadingDeclaration).mockResolvedValue(null)
|
|
vi.mocked(uploadContainerDocument).mockResolvedValue({} as never)
|
|
vi.mocked(uploadContainerClearance).mockResolvedValue({} as never)
|
|
vi.mocked(getContainerOutboundDispatch).mockResolvedValue(outboundDetail)
|
|
vi.mocked(confirmContainerOutboundDispatch).mockResolvedValue({ ...outboundDetail, aggregate: { ...outboundDetail.aggregate, dispatchStatus: 'WAITING_FOR_LOADING', version: 5 } } as never)
|
|
})
|
|
|
|
it('opens domestic export customs upload and sends stable plan, warehouse, version and reason', async () => {
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentButton(wrapper, '国内出口报关附件', '更新附件').trigger('click'); await flushPromises()
|
|
expect(wrapper.text()).toContain('国内出口报关附件')
|
|
const file = new File(['customs'], 'customs.pdf', { type: 'application/pdf' })
|
|
;(wrapper.vm as unknown as { selectDocumentFile: (file: { raw: File }) => void }).selectDocumentFile({ raw: file })
|
|
await (wrapper.vm as unknown as { saveDocument: () => Promise<void> }).saveDocument()
|
|
expect(uploadLoadingDeclaration).toHaveBeenCalledWith('2092438843630391401', '193000000000200002', file, 5, '上传国内出口报关附件')
|
|
expect(uploadContainerDocument).not.toHaveBeenCalled(); expect(uploadContainerClearance).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('shows a readable error when the container has no loading plan', async () => {
|
|
vi.mocked(getContainerOverview).mockResolvedValueOnce({ ...overview, loadingPlanId: null } as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentButton(wrapper, '国内出口报关附件', '更新附件').trigger('click'); await flushPromises()
|
|
expect(wrapper.text()).toContain('当前货柜没有装柜清单,无法上传国内出口报关附件')
|
|
expect(uploadLoadingDeclaration).not.toHaveBeenCalled(); wrapper.unmount()
|
|
})
|
|
|
|
it('hides customs and clearance upload without customs manage permission while keeping DO and PU', async () => {
|
|
const wrapper = mountView(false); await flushPromises()
|
|
expect(documentArticle(wrapper, '国内出口报关附件').findAll('button').map((button) => button.text())).toEqual(['下载'])
|
|
expect(documentArticle(wrapper, '目的港清关资料').find('button').exists()).toBe(false)
|
|
expect(documentArticle(wrapper, 'DO 资料').find('button').exists()).toBe(true)
|
|
expect(documentArticle(wrapper, 'PU 资料').find('button').exists()).toBe(true)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps destination clearance on its independent container clearance API', async () => {
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentArticle(wrapper, '目的港清关资料').find('button').trigger('click'); await flushPromises()
|
|
expect(getContainerClearance).toHaveBeenCalledWith('2092438843630391297')
|
|
expect(wrapper.text()).toContain('目的港清关资料'); expect(uploadLoadingDeclaration).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('downloads customs through the dedicated loading-list API and keeps DO PU isolated', async () => {
|
|
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentButton(wrapper, '国内出口报关附件', '下载').trigger('click'); await flushPromises()
|
|
expect(downloadLoadingDeclaration).toHaveBeenCalledWith('2092438843630391401', '193000000000200002', '2092438843630391502')
|
|
expect(downloadContainerDocument).not.toHaveBeenCalled()
|
|
expect(URL.createObjectURL).toHaveBeenCalledWith(expect.any(Blob))
|
|
expect(click).toHaveBeenCalledOnce()
|
|
expect(URL.revokeObjectURL).toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps customs download after completion while hiding update and completion actions', async () => {
|
|
vi.mocked(getContainerOverview).mockResolvedValueOnce({ ...overview, documents: overview.documents.map((item) => ({ ...item, completed: true })) } as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
const row = documentArticle(wrapper, '国内出口报关附件')
|
|
expect(row.findAll('button').map((button) => button.text())).toEqual(['下载'])
|
|
expect(row.text()).toContain('已完成')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps the optimistic customs upload when overview refresh fails and shows a readable warning', async () => {
|
|
vi.mocked(getContainerOverview).mockResolvedValueOnce(overview as never).mockRejectedValueOnce(new Error('overview unavailable'))
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentButton(wrapper, '国内出口报关附件', '更新附件').trigger('click'); await flushPromises()
|
|
const file = new File(['customs-new'], 'customs-new.pdf', { type: 'application/pdf' })
|
|
;(wrapper.vm as unknown as { selectDocumentFile: (file: { raw: File }) => void }).selectDocumentFile({ raw: file })
|
|
await (wrapper.vm as unknown as { saveDocument: () => Promise<void> }).saveDocument(); await flushPromises()
|
|
const row = documentArticle(wrapper, '国内出口报关附件')
|
|
expect(row.text()).toContain('customs-new.pdf')
|
|
expect(row.text()).toContain('操作员')
|
|
expect(row.text()).toContain('2026-08-26 12:00')
|
|
expect(wrapper.text()).toContain('附件已上传,但最新资料刷新失败;当前已保留本次上传结果')
|
|
expect(uploadLoadingDeclaration).toHaveBeenCalledWith('2092438843630391401', '193000000000200002', file, 5, '上传国内出口报关附件')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('refreshes a hidden customs version after A0409 and preserves error evidence without auto retry', async () => {
|
|
const hidden = { id: '2092438843630392501', documentType: 'CUSTOMS_DECLARATION', fileId: '2092438843630392502', fileName: 'server-customs.pdf', fileHash: 'server-hash', uploadedBy: '2', uploadedByName: '服务端操作员', uploadedAt: '2026-08-26T12:30:00', completedBy: null, completedByName: null, completedAt: null, version: 7 }
|
|
const withoutCustoms = { ...overview, documents: [] }
|
|
vi.mocked(getContainerOverview).mockResolvedValue(withoutCustoms as never)
|
|
vi.mocked(getLoadingDeclaration).mockResolvedValueOnce(hidden as never)
|
|
vi.mocked(uploadLoadingDeclaration).mockRejectedValueOnce(new ApiResponseError('版本冲突', 'A0409', undefined, undefined, 'request-upload-409', 'trace-upload-409'))
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentButton(wrapper, '国内出口报关附件', '上传附件').trigger('click'); await flushPromises()
|
|
const file = new File(['next'], 'next.pdf', { type: 'application/pdf' })
|
|
;(wrapper.vm as unknown as { selectDocumentFile: (file: { raw: File }) => void }).selectDocumentFile({ raw: file })
|
|
await (wrapper.vm as unknown as { saveDocument: () => Promise<void> }).saveDocument(); await flushPromises()
|
|
expect(uploadLoadingDeclaration).toHaveBeenCalledOnce()
|
|
expect(uploadLoadingDeclaration).toHaveBeenCalledWith('2092438843630391401', '193000000000200002', file, 0, '上传国内出口报关附件')
|
|
expect(getLoadingDeclaration).toHaveBeenCalledWith('2092438843630391401', '193000000000200002')
|
|
expect(documentArticle(wrapper, '国内出口报关附件').text()).toContain('server-customs.pdf')
|
|
expect(documentArticle(wrapper, '国内出口报关附件').text()).toContain('服务端操作员')
|
|
expect(wrapper.text()).toContain('A0409')
|
|
expect(wrapper.text()).toContain('request-upload-409')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('uses the independent destination document version and immediately replaces the table row after update', async () => {
|
|
const existing = { id: '301', documentType: 'DESTINATION_CLEARANCE', fileId: '302', fileName: 'clearance-v1.pdf', fileHash: null, uploadedBy: '3', uploadedByName: '清关员', uploadedAt: '2026-08-26T11:00:00', completedBy: null, completedByName: null, completedAt: null, version: 1 }
|
|
const updated = { ...existing, fileId: '303', fileName: 'clearance-v2.pdf', uploadedAt: '2026-08-26T13:00:00', version: 2 }
|
|
vi.mocked(getContainerClearance).mockResolvedValueOnce(existing as never)
|
|
vi.mocked(uploadContainerClearance).mockResolvedValueOnce(updated as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentButton(wrapper, '目的港清关资料', '上传附件').trigger('click'); await flushPromises()
|
|
const file = new File(['clearance'], 'clearance-v2.pdf', { type: 'application/pdf' })
|
|
;(wrapper.vm as unknown as { selectClearanceFile: (file: { raw: File }) => void }).selectClearanceFile({ raw: file })
|
|
await (wrapper.vm as unknown as { uploadClearance: () => Promise<void> }).uploadClearance(); await flushPromises()
|
|
expect(uploadContainerClearance).toHaveBeenCalledWith('2092438843630391297', file, 1, '上传目的港清关资料')
|
|
expect(documentArticle(wrapper, '目的港清关资料').text()).toContain('clearance-v2.pdf')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('downloads destination clearance through its dedicated endpoint and keeps completed rows download-only', async () => {
|
|
const completed = { id: '351', documentType: 'DESTINATION_CLEARANCE', fileId: '2092438843630393502', fileName: 'destination-clearance.pdf', fileHash: null, uploadedBy: '3', uploadedByName: '清关员', uploadedAt: '2026-08-26T11:00:00', version: 2, completed: true }
|
|
vi.mocked(getContainerOverview).mockResolvedValueOnce({ ...overview, documents: [...overview.documents, completed] } as never)
|
|
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
|
const wrapper = mountView(); await flushPromises()
|
|
const row = documentArticle(wrapper, '目的港清关资料')
|
|
expect(row.findAll('button').map((button) => button.text())).toEqual(['下载'])
|
|
await documentButton(wrapper, '目的港清关资料', '下载').trigger('click'); await flushPromises()
|
|
expect(downloadContainerClearance).toHaveBeenCalledWith('2092438843630391297', '2092438843630393502')
|
|
expect(downloadLoadingDeclaration).not.toHaveBeenCalled()
|
|
expect(downloadContainerDocument).not.toHaveBeenCalled()
|
|
expect(click).toHaveBeenCalledOnce()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('refreshes destination clearance after A0409 and preserves the server file and error metadata', async () => {
|
|
const existing = { id: '401', documentType: 'DESTINATION_CLEARANCE', fileId: '402', fileName: 'clearance-v1.pdf', fileHash: null, uploadedBy: '3', uploadedByName: '清关员', uploadedAt: '2026-08-26T11:00:00', completedBy: null, completedByName: null, completedAt: null, version: 1 }
|
|
const server = { ...existing, fileId: '403', fileName: 'server-clearance-v2.pdf', uploadedByName: '服务端清关员', version: 2 }
|
|
vi.mocked(getContainerClearance).mockResolvedValueOnce(existing as never).mockResolvedValueOnce(server as never)
|
|
vi.mocked(uploadContainerClearance).mockRejectedValueOnce(new ApiResponseError('版本冲突', 'A0409', undefined, undefined, 'request-clearance-409', 'trace-clearance-409'))
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentButton(wrapper, '目的港清关资料', '上传附件').trigger('click'); await flushPromises()
|
|
const file = new File(['clearance'], 'clearance-next.pdf', { type: 'application/pdf' })
|
|
;(wrapper.vm as unknown as { selectClearanceFile: (file: { raw: File }) => void }).selectClearanceFile({ raw: file })
|
|
await (wrapper.vm as unknown as { uploadClearance: () => Promise<void> }).uploadClearance(); await flushPromises()
|
|
expect(uploadContainerClearance).toHaveBeenCalledOnce()
|
|
expect(documentArticle(wrapper, '目的港清关资料').text()).toContain('server-clearance-v2.pdf')
|
|
expect(wrapper.text()).toContain('A0409')
|
|
expect(wrapper.text()).toContain('request-clearance-409')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps customs attachment upload and completion as two explicit steps with exact payload', async () => {
|
|
const completedOverview = { ...overview, documents: overview.documents.map((item) => ({ ...item, completed: true })) }
|
|
vi.mocked(getContainerOverview).mockResolvedValueOnce(overview as never).mockResolvedValue(completedOverview as never)
|
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValueOnce({ action: 'confirm' } as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
const customs = documentArticle(wrapper, '国内出口报关附件')
|
|
expect(customs.text()).toContain('附件已上传,待确认报关完成')
|
|
expect(customs.text()).toContain('更新附件')
|
|
await documentButton(wrapper, '国内出口报关附件', '确认报关完成').trigger('click'); await flushPromises()
|
|
expect(completeLoadingDeclaration).toHaveBeenCalledOnce()
|
|
expect(completeLoadingDeclaration).toHaveBeenCalledWith('2092438843630391401', '193000000000200002', 5, '货柜详情确认国内出口报关完成')
|
|
expect(uploadLoadingDeclaration).not.toHaveBeenCalled()
|
|
expect(getContainerOverview).toHaveBeenCalledTimes(2)
|
|
expect(getContainerOutboundDispatch).toHaveBeenCalledTimes(2)
|
|
expect(documentArticle(wrapper, '国内出口报关附件').text()).toContain('已确认报关完成')
|
|
expect(documentButton(wrapper, '国内出口报关附件', '确认报关完成')).toBeUndefined()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('shows customs completion only for an existing unfinished attachment with manage permission', async () => {
|
|
vi.mocked(getContainerOverview).mockResolvedValueOnce({ ...overview, documents: [] } as never)
|
|
let wrapper = mountView(); await flushPromises()
|
|
expect(documentArticle(wrapper, '国内出口报关附件').text()).toContain('第一步上传附件,第二步确认报关完成')
|
|
expect(documentButton(wrapper, '国内出口报关附件', '确认报关完成')).toBeUndefined()
|
|
wrapper.unmount()
|
|
|
|
vi.mocked(getContainerOverview).mockResolvedValueOnce({ ...overview, documents: overview.documents.map((item) => ({ ...item, completed: true })) } as never)
|
|
wrapper = mountView(); await flushPromises()
|
|
expect(documentButton(wrapper, '国内出口报关附件', '确认报关完成')).toBeUndefined()
|
|
wrapper.unmount()
|
|
|
|
wrapper = mountView(false); await flushPromises()
|
|
expect(documentButton(wrapper, '国内出口报关附件', '确认报关完成')).toBeUndefined()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('shows a readable completion error without a loading plan and does not call the API', async () => {
|
|
vi.mocked(getContainerOverview).mockResolvedValueOnce({ ...overview, loadingPlanId: null } as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
await documentButton(wrapper, '国内出口报关附件', '确认报关完成').trigger('click'); await flushPromises()
|
|
expect(wrapper.text()).toContain('当前货柜没有装柜清单,无法确认报关完成')
|
|
expect(completeLoadingDeclaration).not.toHaveBeenCalled()
|
|
expect(ElMessageBox.confirm).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('prevents repeated customs completion and preserves A0409 evidence after refreshing facts', async () => {
|
|
let releasePrompt!: () => void
|
|
vi.spyOn(ElMessageBox, 'confirm').mockReturnValueOnce(new Promise<void>((resolve) => { releasePrompt = resolve }) as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
const button = documentButton(wrapper, '国内出口报关附件', '确认报关完成')
|
|
await button.trigger('click'); await button.trigger('click')
|
|
expect(ElMessageBox.confirm).toHaveBeenCalledOnce()
|
|
releasePrompt(); await flushPromises()
|
|
vi.mocked(completeLoadingDeclaration).mockRejectedValueOnce(new ApiResponseError('数据已变更', 'A0409', undefined, undefined, 'request-customs-409', 'trace-customs-409'))
|
|
vi.mocked(ElMessageBox.confirm).mockResolvedValueOnce({ action: 'confirm' } as never)
|
|
await documentButton(wrapper, '国内出口报关附件', '确认报关完成').trigger('click'); await flushPromises()
|
|
expect(completeLoadingDeclaration).toHaveBeenCalledTimes(2)
|
|
expect(wrapper.text()).toContain('A0409')
|
|
expect(wrapper.text()).toContain('request-customs-409')
|
|
expect(getContainerOverview).toHaveBeenCalledTimes(3)
|
|
expect(getContainerOutboundDispatch).toHaveBeenCalledTimes(3)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('loads and displays the whole-container gate without querying an inbound id', async () => {
|
|
const wrapper = mountView(); await flushPromises()
|
|
expect(getContainerOutboundDispatch).toHaveBeenCalledOnce()
|
|
expect(getContainerOutboundDispatch).toHaveBeenCalledWith('2092438843630391297')
|
|
expect(wrapper.text()).toContain('国内出仓门禁')
|
|
expect(wrapper.find('.gate-summary').text()).toContain('总门禁 1')
|
|
expect(wrapper.find('.gate-summary').text()).toContain('已通过 1')
|
|
expect(wrapper.find('.gate-summary').text()).toContain('阻断 0')
|
|
expect(wrapper.find('.gate-list').text()).not.toContain('报关协同完成')
|
|
await wrapper.find('.gate-summary button').trigger('click')
|
|
expect(wrapper.find('.gate-list').text()).toContain('报关协同完成')
|
|
expect(wrapper.find('.gate-list').text()).toContain('覆盖 1 个入库码')
|
|
expect(wrapper.find('.gate-list').text()).toContain('IN-1')
|
|
expect(wrapper.text()).toContain('确认离仓')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('confirms the physical container once with version and no inboundId then refreshes the carrier panel', async () => {
|
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValueOnce({ action: 'confirm' } as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
await wrapper.find('.outbound-panel__actions button').trigger('click'); await flushPromises()
|
|
expect(confirmContainerOutboundDispatch).toHaveBeenCalledOnce()
|
|
expect(confirmContainerOutboundDispatch).toHaveBeenCalledWith('2092438843630391297', { expectedVersion: 4, remark: '货柜详情确认国内出仓' })
|
|
expect(vi.mocked(confirmContainerOutboundDispatch).mock.calls[0]?.[1]).not.toHaveProperty('inboundId')
|
|
expect(getContainerOverview).toHaveBeenCalledTimes(2)
|
|
expect(carrierMounts.value).toBe(2)
|
|
expect(wrapper.text()).toContain('已确认离仓')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('prevents a repeated confirmation wave while the first prompt is open', async () => {
|
|
vi.spyOn(ElMessageBox, 'confirm').mockReturnValueOnce(new Promise(() => {}) as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
const button = wrapper.find('.outbound-panel__actions button')
|
|
await button.trigger('click')
|
|
await button.trigger('click')
|
|
expect(ElMessageBox.confirm).toHaveBeenCalledOnce()
|
|
expect(confirmContainerOutboundDispatch).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('hides the outbound gate and never calls its API without view permission', async () => {
|
|
const wrapper = mountView(true, false); await flushPromises()
|
|
expect(wrapper.text()).not.toContain('国内出仓门禁')
|
|
expect(getContainerOutboundDispatch).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('shows gate failure reasons and does not expose confirm when any gate is blocked', async () => {
|
|
vi.mocked(getContainerOutboundDispatch).mockResolvedValueOnce({
|
|
...outboundDetail,
|
|
aggregate: {
|
|
...outboundDetail.aggregate,
|
|
gate: { passed: false, checkedAt: '2026-08-26T12:00:00', inbounds: [{ inboundId: '2092438843630391501', inboundCode: 'IN-1', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: false, actual: 'MISSING', requirement: 'COMPLETED' }] } }] },
|
|
},
|
|
} as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
expect(wrapper.text()).toContain('报关协同完成')
|
|
expect(wrapper.text()).toContain('MISSING')
|
|
expect(wrapper.text()).toContain('COMPLETED')
|
|
expect(wrapper.find('.outbound-panel__actions button').exists()).toBe(false)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('deduplicates identical gates across inbounds and shows compact coverage counts', async () => {
|
|
const blocked = { code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: false, actual: 'MISSING', requirement: 'COMPLETED' }
|
|
const passed = { code: 'LOADING_PLAN_COMPLETED', name: '柜次装柜计划完成', passed: true, actual: 'COMPLETED', requirement: 'COMPLETED' }
|
|
vi.mocked(getContainerOutboundDispatch).mockResolvedValueOnce({
|
|
...outboundDetail,
|
|
aggregate: {
|
|
...outboundDetail.aggregate,
|
|
gate: {
|
|
passed: false,
|
|
checkedAt: '2026-08-26T12:00:00',
|
|
inbounds: [
|
|
{ inboundId: '101', inboundCode: 'IN-101', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [blocked, passed] } },
|
|
{ inboundId: '102', inboundCode: 'IN-102', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [blocked, passed] } },
|
|
],
|
|
},
|
|
},
|
|
} as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
expect(wrapper.find('.gate-summary').text()).toContain('总门禁 2')
|
|
expect(wrapper.find('.gate-summary').text()).toContain('已通过 1')
|
|
expect(wrapper.find('.gate-summary').text()).toContain('阻断 1')
|
|
expect(wrapper.findAll('.gate-row')).toHaveLength(1)
|
|
expect(wrapper.find('.gate-row').text()).toContain('影响 2 个入库码')
|
|
expect(wrapper.find('.gate-row').text()).toContain('IN-101')
|
|
expect(wrapper.find('.gate-row').text()).toContain('IN-102')
|
|
await wrapper.find('.gate-summary button').trigger('click')
|
|
expect(wrapper.findAll('.gate-row')).toHaveLength(2)
|
|
expect(wrapper.findAll('.gate-row')[1]?.text()).toContain('覆盖 2 个入库码')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps different facts for the same gate code separate and labels the affected inbound', async () => {
|
|
vi.mocked(getContainerOutboundDispatch).mockResolvedValueOnce({
|
|
...outboundDetail,
|
|
aggregate: {
|
|
...outboundDetail.aggregate,
|
|
gate: {
|
|
passed: false,
|
|
checkedAt: '2026-08-26T12:00:00',
|
|
inbounds: [
|
|
{ inboundId: '201', inboundCode: 'IN-MISSING', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: false, actual: 'MISSING', requirement: 'COMPLETED' }] } },
|
|
{ inboundId: '202', inboundCode: 'IN-PENDING', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: false, actual: 'PENDING', requirement: 'COMPLETED' }] } },
|
|
{ inboundId: '203', inboundCode: 'IN-DONE', gate: { passed: true, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: true, actual: 'COMPLETED', requirement: 'COMPLETED' }] } },
|
|
],
|
|
},
|
|
},
|
|
} as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
expect(wrapper.findAll('.gate-row')).toHaveLength(2)
|
|
expect(wrapper.find('.gate-list').text()).toContain('MISSING')
|
|
expect(wrapper.find('.gate-list').text()).toContain('IN-MISSING')
|
|
expect(wrapper.find('.gate-list').text()).toContain('PENDING')
|
|
expect(wrapper.find('.gate-list').text()).toContain('IN-PENDING')
|
|
await wrapper.find('.gate-summary button').trigger('click')
|
|
expect(wrapper.findAll('.gate-row')).toHaveLength(3)
|
|
expect(wrapper.find('.gate-list').text()).toContain('IN-DONE')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps truck and sailing schedule as reference only and never counts legacy reference gates as blocking', async () => {
|
|
vi.mocked(getContainerOutboundDispatch).mockResolvedValueOnce({
|
|
...outboundDetail,
|
|
aggregate: {
|
|
...outboundDetail.aggregate,
|
|
truckAppointmentStatus: 'ARRIVED',
|
|
gate: {
|
|
passed: false,
|
|
checkedAt: '2026-08-26T12:00:00',
|
|
inbounds: [{ inboundId: '301', inboundCode: 'IN-301', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [
|
|
{ code: 'TRUCK_DEPARTED', name: '关联拖车已离仓', passed: false, actual: 'ARRIVED', requirement: 'DEPARTED或COMPLETED' },
|
|
{ code: 'SAILING_SCHEDULE', name: '船期有效且未过期', passed: false, actual: 'EXPIRED', requirement: 'VALID' },
|
|
{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: true, actual: 'COMPLETED', requirement: 'COMPLETED' },
|
|
] } }],
|
|
},
|
|
},
|
|
} as never)
|
|
const wrapper = mountView(); await flushPromises()
|
|
expect(wrapper.find('.gate-summary').text()).toContain('总门禁 1')
|
|
expect(wrapper.find('.gate-summary').text()).toContain('阻断 0')
|
|
expect(wrapper.find('.gate-summary').text()).toContain('拖车状态(参考):已到仓')
|
|
expect(wrapper.text()).not.toContain('关联拖车已离仓')
|
|
expect(wrapper.text()).not.toContain('船期有效且未过期')
|
|
expect(wrapper.find('.outbound-panel__actions button').text()).toContain('确认离仓')
|
|
wrapper.unmount()
|
|
})
|
|
})
|