This commit is contained in:
2026-08-27 15:02:41 +08:00
parent 24575223a0
commit 00045a9764
2 changed files with 98 additions and 12 deletions
@@ -2,7 +2,7 @@ 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 { completeContainerClearance, 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'
@@ -47,6 +47,13 @@ function mountView(customs = true, outbound = true) {
}
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))! }
function deferred<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((resolvePromise) => { resolve = resolvePromise })
return { promise, resolve }
}
type ClearanceVm = { clearanceOpen: boolean; clearanceFile: File | null; clearanceError: { code?: string; requestId?: string } | null; documentRefreshWarning: unknown; selectClearanceFile: (file: { raw: File }) => void; uploadClearance: () => Promise<void>; completeClearance: () => Promise<void> }
function clearanceVm(wrapper: VueWrapper) { return wrapper.vm as unknown as ClearanceVm }
describe('ContainerDetailView', () => {
beforeEach(() => {
@@ -163,18 +170,26 @@ describe('ContainerDetailView', () => {
wrapper.unmount()
})
it('uses the independent destination document version and immediately replaces the table row after update', async () => {
it('closes immediately after a valid destination upload response, clears dialog state and refreshes after optimistic table 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 }
const refresh = deferred<typeof overview>()
vi.mocked(getContainerOverview).mockResolvedValueOnce(overview as never).mockReturnValueOnce(refresh.promise as never)
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()
clearanceVm(wrapper).selectClearanceFile({ raw: file })
const request = clearanceVm(wrapper).uploadClearance(); await flushPromises()
expect(uploadContainerClearance).toHaveBeenCalledWith('2092438843630391297', file, 1, '上传目的港清关资料')
expect(clearanceVm(wrapper).clearanceOpen).toBe(false)
expect(clearanceVm(wrapper).clearanceFile).toBeNull()
expect(clearanceVm(wrapper).clearanceError).toBeNull()
expect(documentArticle(wrapper, '目的港清关资料').text()).toContain('clearance-v2.pdf')
refresh.resolve(overview)
await request; await flushPromises()
expect(clearanceVm(wrapper).documentRefreshWarning).toBeNull()
wrapper.unmount()
})
@@ -196,17 +211,75 @@ describe('ContainerDetailView', () => {
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(getContainerClearance).mockResolvedValueOnce(existing as never).mockResolvedValueOnce(server 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()
clearanceVm(wrapper).selectClearanceFile({ raw: file })
await clearanceVm(wrapper).uploadClearance(); await flushPromises()
expect(uploadContainerClearance).toHaveBeenCalledOnce()
expect(clearanceVm(wrapper).clearanceOpen).toBe(true)
expect(clearanceVm(wrapper).clearanceFile).toBe(file)
expect(clearanceVm(wrapper).clearanceError).toMatchObject({ code: 'A0409', requestId: 'request-clearance-409' })
expect(documentArticle(wrapper, '目的港清关资料').text()).toContain('server-clearance-v2.pdf')
expect(wrapper.text()).toContain('A0409')
expect(wrapper.text()).toContain('request-clearance-409')
clearanceVm(wrapper).clearanceOpen = false; await flushPromises()
await documentButton(wrapper, '目的港清关资料', '更新附件').trigger('click'); await flushPromises()
expect(clearanceVm(wrapper).clearanceOpen).toBe(true)
expect(clearanceVm(wrapper).clearanceFile).toBeNull()
expect(clearanceVm(wrapper).clearanceError).toBeNull()
expect(wrapper.text()).not.toContain('request-clearance-409')
wrapper.unmount()
})
it('keeps the destination dialog and selected file for network and empty-response upload failures', async () => {
const existing = { id: '501', documentType: 'DESTINATION_CLEARANCE', fileId: '502', fileName: 'clearance-v1.pdf', fileHash: null, uploadedBy: '3', uploadedByName: '清关员', uploadedAt: '2026-08-26T11:00:00', completedBy: null, completedByName: null, completedAt: null, version: 1 }
vi.mocked(getContainerClearance).mockResolvedValueOnce(existing as never)
vi.mocked(uploadContainerClearance)
.mockRejectedValueOnce(new ApiResponseError('网络请求失败', 'NETWORK_ERROR', undefined, undefined, 'request-network', 'trace-network'))
.mockResolvedValueOnce(null)
const wrapper = mountView(); await flushPromises()
await documentButton(wrapper, '目的港清关资料', '上传附件').trigger('click'); await flushPromises()
const file = new File(['clearance'], 'clearance-next.pdf', { type: 'application/pdf' })
clearanceVm(wrapper).selectClearanceFile({ raw: file })
await clearanceVm(wrapper).uploadClearance(); await flushPromises()
expect(clearanceVm(wrapper).clearanceOpen).toBe(true)
expect(clearanceVm(wrapper).clearanceFile).toBe(file)
expect(wrapper.text()).toContain('request-network')
await clearanceVm(wrapper).uploadClearance(); await flushPromises()
expect(clearanceVm(wrapper).clearanceOpen).toBe(true)
expect(clearanceVm(wrapper).clearanceFile).toBe(file)
expect(uploadContainerClearance).toHaveBeenCalledTimes(2)
wrapper.unmount()
})
it('closes after successful destination clearance completion but keeps the dialog open on completion failure', async () => {
const existing = { id: '601', documentType: 'DESTINATION_CLEARANCE', fileId: '602', fileName: 'clearance.pdf', fileHash: null, uploadedBy: '3', uploadedByName: '清关员', uploadedAt: '2026-08-26T11:00:00', completedBy: null, completedByName: null, completedAt: null, version: 1 }
const completed = { ...existing, completedBy: '4', completedByName: '复核员', completedAt: '2026-08-27T12:00:00', version: 2 }
const refresh = deferred<typeof overview>()
vi.mocked(getContainerOverview).mockResolvedValueOnce(overview as never).mockReturnValueOnce(refresh.promise as never)
vi.mocked(getContainerClearance).mockResolvedValueOnce(existing as never)
vi.mocked(completeContainerClearance).mockResolvedValueOnce(completed as never)
let wrapper = mountView(); await flushPromises()
await documentButton(wrapper, '目的港清关资料', '上传附件').trigger('click'); await flushPromises()
const request = clearanceVm(wrapper).completeClearance(); await flushPromises()
expect(clearanceVm(wrapper).clearanceOpen).toBe(false)
expect(clearanceVm(wrapper).clearanceError).toBeNull()
expect(documentArticle(wrapper, '目的港清关资料').text()).toContain('已完成')
refresh.resolve(overview); await request; await flushPromises()
wrapper.unmount()
vi.mocked(getContainerOverview).mockResolvedValue(overview as never)
vi.mocked(getContainerClearance).mockResolvedValueOnce(existing as never)
vi.mocked(completeContainerClearance).mockRejectedValueOnce(new ApiResponseError('完成失败', 'A0409', undefined, undefined, 'request-complete-409', 'trace-complete-409'))
wrapper = mountView(); await flushPromises()
await documentButton(wrapper, '目的港清关资料', '上传附件').trigger('click'); await flushPromises()
await clearanceVm(wrapper).completeClearance(); await flushPromises()
expect(clearanceVm(wrapper).clearanceOpen).toBe(true)
expect(clearanceVm(wrapper).clearanceError).toMatchObject({ code: 'A0409', requestId: 'request-complete-409' })
expect(wrapper.text()).toContain('trace-complete-409')
wrapper.unmount()
})