2026-08-26 11:10:48 +08:00
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
2026-08-13 11:57:44 +08:00
|
|
|
import ElementPlus from 'element-plus'
|
|
|
|
|
import ContainerOverviewPanel from './ContainerOverviewPanel.vue'
|
|
|
|
|
import type { ContainerOverview } from '../types'
|
2026-08-26 11:10:48 +08:00
|
|
|
import { downloadContainerPdaPhoto } from '../api/platformContainerLoading'
|
|
|
|
|
|
|
|
|
|
vi.mock('../api/platformContainerLoading', () => ({ downloadContainerPdaPhoto: vi.fn() }))
|
2026-08-13 11:57:44 +08:00
|
|
|
|
|
|
|
|
const overview: ContainerOverview = {
|
|
|
|
|
container: { id: '7', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'LOADING', warehouseId: '8', warehouseName: '义乌仓', voyageNo: 'V1', vesselName: '船一', version: 2 },
|
|
|
|
|
loadingPlanId: '9',
|
2026-08-14 14:27:03 +08:00
|
|
|
pda: { taskId: '10', loadingPlanId: '9', containerBookingId: '7', taskStatus: 'IN_PROGRESS', workflowStatus: 'AWAITING_REVIEW', taskVersion: 2, itemCount: 4, completedItemCount: 4, unresolvedItemCount: 0, photoCounts: [{ evidenceType: 'PHOTO_STEP_EMPTY_CONTAINER', count: 2 }], photos: [{ evidenceId: '20', tenantId: '21', inboundId: '22', evidenceType: 'PHOTO_STEP_EMPTY_CONTAINER', fileId: '23', fileName: 'empty.jpg', status: 'ACTIVE', downloadPath: '/api/platform/v1/containers/7/pda/photos/23/download', uploadedAt: '2026-08-13T12:30:00' }], exceptionReasons: ['一项待复核'] },
|
2026-08-13 11:57:44 +08:00
|
|
|
documents: [{ id: '11', documentType: 'DELIVERY_ORDER', fileId: '9007199254740999', fileName: 'do.pdf', fileHash: null, uploadedBy: '13', uploadedByName: '操作员', uploadedAt: '2026-08-13T10:00:00', version: 1, completed: false }],
|
|
|
|
|
exception: { active: true, reason: '码头查验', markedBy: '13', markedByName: '操作员', markedAt: '2026-08-13T11:00:00' },
|
|
|
|
|
exceptionHistory: [{ id: '14', action: 'MARK', reason: '码头查验', operatorId: '13', operatorName: '操作员', occurredAt: '2026-08-13T11:00:00' }],
|
|
|
|
|
timeline: [{ eventType: 'TRANSPORT_UPDATE', detail: '更新预计到港时间', operatorId: '13', operatorName: '操作员', occurredAt: '2026-08-13T12:00:00' }],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('ContainerOverviewPanel', () => {
|
2026-08-26 11:10:48 +08:00
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks()
|
|
|
|
|
vi.mocked(downloadContainerPdaPhoto).mockResolvedValue(new Blob(['photo']))
|
|
|
|
|
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:container-photo') })
|
|
|
|
|
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders documents, PDA progress, exception history and timeline from the aggregate DTO', async () => {
|
|
|
|
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
2026-08-13 11:57:44 +08:00
|
|
|
expect(wrapper.text()).toContain('DO 资料')
|
|
|
|
|
expect(wrapper.text()).toContain('do.pdf')
|
|
|
|
|
expect(wrapper.text()).toContain('PDA 作业进度')
|
2026-08-14 14:27:03 +08:00
|
|
|
expect(wrapper.text()).toContain('待人工复核')
|
|
|
|
|
expect(wrapper.text()).toContain('空柜照片')
|
|
|
|
|
expect(wrapper.text()).not.toContain('PHOTO_STEP_EMPTY_CONTAINER')
|
2026-08-13 11:57:44 +08:00
|
|
|
expect(wrapper.text()).toContain('一项待复核')
|
|
|
|
|
expect(wrapper.text()).toContain('码头查验')
|
|
|
|
|
expect(wrapper.text()).toContain('更新预计到港时间')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('emits stable document types and does not expose file ids in visible text', async () => {
|
2026-08-26 11:10:48 +08:00
|
|
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
2026-08-13 11:57:44 +08:00
|
|
|
const updateButton = wrapper.findAll('button').find((button) => button.text().includes('更新'))!
|
|
|
|
|
await updateButton.trigger('click')
|
|
|
|
|
expect(wrapper.emitted('uploadDocument')?.[0]).toEqual(['DELIVERY_ORDER'])
|
|
|
|
|
expect(wrapper.text()).not.toContain('9007199254740999')
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-26 11:10:48 +08:00
|
|
|
it('loads protected photos into direct thumbnails, opens them and revokes object URLs', async () => {
|
|
|
|
|
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
|
|
|
|
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(downloadContainerPdaPhoto).toHaveBeenCalledWith('/api/platform/v1/containers/7/pda/photos/23/download')
|
|
|
|
|
expect(wrapper.find('img[src="blob:container-photo"]').exists()).toBe(true)
|
|
|
|
|
await wrapper.find('button[aria-label="预览装柜照片 empty.jpg"]').trigger('click')
|
|
|
|
|
expect(open).toHaveBeenCalledWith('blob:container-photo', '_blank', 'noopener,noreferrer')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:container-photo')
|
2026-08-13 11:57:44 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-26 11:10:48 +08:00
|
|
|
it('revokes old thumbnail URLs when the overview photos change', async () => {
|
|
|
|
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const changed = { ...overview.pda!.photos[0], evidenceId: '22', fileId: '25', fileName: 'next.jpg', downloadPath: '/api/platform/v1/containers/7/pda/photos/25/download' }
|
|
|
|
|
await wrapper.setProps({ overview: { ...overview, pda: { ...overview.pda!, photos: [changed] } } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:container-photo')
|
|
|
|
|
expect(downloadContainerPdaPhoto).toHaveBeenCalledWith('/api/platform/v1/containers/7/pda/photos/25/download')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('isolates one thumbnail failure without blocking other photos', async () => {
|
|
|
|
|
const second = { ...overview.pda!.photos[0], evidenceId: '21', fileId: '24', fileName: 'seal.jpg', evidenceType: 'SEAL', downloadPath: '/api/platform/v1/containers/7/pda/photos/24/download' }
|
|
|
|
|
vi.mocked(downloadContainerPdaPhoto).mockResolvedValueOnce(new Blob(['photo'])).mockRejectedValueOnce(new Error('photo unavailable'))
|
|
|
|
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, pda: { ...overview.pda!, photos: [...overview.pda!.photos, second] } }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(wrapper.find('img[src="blob:container-photo"]').exists()).toBe(true)
|
|
|
|
|
expect(wrapper.text()).toContain('照片加载失败')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('maps PDA events to Chinese and hides long technical keys from the main timeline', () => {
|
|
|
|
|
const longKey = 'pda-submit-1000001-2092438843630391297-2092437496092790785--0c642e80891d76a4-m9428-76'
|
|
|
|
|
const timeline = [
|
|
|
|
|
{ eventType: 'PDA_CLAIM', detail: `claim-${longKey}`, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:28:00' },
|
|
|
|
|
{ eventType: 'PDA_BATCH', detail: longKey, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:28:00' },
|
|
|
|
|
{ eventType: 'PDA_COMPLETE_LOADING', detail: `complete-${longKey}`, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:29:00' },
|
|
|
|
|
]
|
|
|
|
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, pda: null, timeline }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
|
|
|
|
expect(wrapper.text()).toContain('领取 PDA 任务')
|
|
|
|
|
expect(wrapper.text()).toContain('提交拣货结果')
|
|
|
|
|
expect(wrapper.text()).toContain('完成装柜')
|
|
|
|
|
expect(wrapper.text()).not.toContain(longKey)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders a compact empty photo state without exposing credentials', () => {
|
2026-08-13 11:57:44 +08:00
|
|
|
const withoutPhotos = { ...overview, pda: overview.pda ? { ...overview.pda, photos: [] } : null }
|
2026-08-26 11:10:48 +08:00
|
|
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview: withoutPhotos, canManageDocuments: false, canManageCustoms: false, canManageException: false }, global: { plugins: [ElementPlus] } })
|
2026-08-13 11:57:44 +08:00
|
|
|
expect(wrapper.text()).toContain('暂无可查看的有效照片')
|
|
|
|
|
expect(wrapper.text()).not.toContain('Authorization')
|
|
|
|
|
})
|
2026-08-26 11:10:48 +08:00
|
|
|
|
|
|
|
|
it('keeps a completed customs document labelled as an attachment, not formal customs release', () => {
|
|
|
|
|
const customs = { ...overview.documents[0], documentType: 'CUSTOMS_DECLARATION', completed: true, fileName: 'customs.pdf' }
|
|
|
|
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, pda: null, documents: [customs] }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
|
|
|
|
expect(wrapper.text()).toContain('国内出口报关附件')
|
|
|
|
|
expect(wrapper.text()).not.toContain('已完成报关')
|
|
|
|
|
expect(wrapper.text()).not.toContain('已放行')
|
|
|
|
|
})
|
2026-08-13 11:57:44 +08:00
|
|
|
})
|