import { beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import ElementPlus from 'element-plus' import ContainerOverviewPanel from './ContainerOverviewPanel.vue' import type { ContainerOverview } from '../types' import { downloadContainerPdaPhoto } from '../api/platformContainerLoading' vi.mock('../api/platformContainerLoading', () => ({ downloadContainerPdaPhoto: vi.fn() })) const overview: ContainerOverview = { container: { id: '7', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'LOADING', warehouseId: '8', warehouseName: '义乌仓', voyageNo: 'V1', vesselName: '船一', pickupCode: '00123456', pickupCodeUpdatedBy: '13', pickupCodeUpdatedByName: '操作员', pickupCodeUpdatedAt: '2026-08-13T10:30:00', version: 2 }, loadingPlanId: '9', 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: ['一项待复核'] }, 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', () => { 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() expect(wrapper.text()).toContain('DO 资料') expect(wrapper.text()).toContain('do.pdf') expect(wrapper.text()).toContain('PDA 作业进度') expect(wrapper.text()).toContain('待人工复核') expect(wrapper.text()).toContain('空柜照片') expect(wrapper.text()).not.toContain('PHOTO_STEP_EMPTY_CONTAINER') expect(wrapper.text()).toContain('一项待复核') expect(wrapper.text()).toContain('码头查验') expect(wrapper.text()).not.toContain('更新预计到港时间') }) it('emits stable document types and does not expose file ids in visible text', async () => { const wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } }) 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') }) it('shows the channel restriction and suppresses DO uploads when the document is not applicable', () => { const withoutDo = { ...overview, documents: overview.documents.filter((item) => item.documentType !== 'DELIVERY_ORDER') } const wrapper = mount(ContainerOverviewPanel, { props: { overview: withoutDo, canManageDocuments: true, canManageCustoms: true, canManageException: true, deliveryOrderBlockedReason: '海铁渠道不适用 DO 资料' }, global: { plugins: [ElementPlus] } }) const deliveryRow = wrapper.findAll('.document-table tbody tr').find((row) => row.text().includes('DO 资料'))! expect(deliveryRow.text()).toContain('海铁渠道不适用 DO 资料') expect(deliveryRow.find('button').exists()).toBe(false) }) it('renders two file rows and one inline PU code row without a nested heading', async () => { const wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } }) const headers = wrapper.findAll('.document-table thead th').map((cell) => cell.text()) expect(headers).toEqual(['操作', '资料类型', '资料内容', '更新人', '更新时间', '状态']) expect(wrapper.findAll('.document-table tbody tr')).toHaveLength(3) expect(wrapper.findAll('.document-item')).toHaveLength(0) expect(wrapper.find('.document-table-wrap').exists()).toBe(true) expect(wrapper.text()).not.toContain('业务资料') const pickupInput = wrapper.find('input[aria-label="PU码"]') expect((pickupInput.element as HTMLInputElement).value).toBe('00123456') await pickupInput.setValue('998877') await wrapper.find('.pickup-code-row button').trigger('click') expect(wrapper.emitted('savePickupCode')?.[0]).toEqual(['998877']) const deliveryRow = wrapper.findAll('.document-table tbody tr').find((row) => row.text().includes('DO 资料'))! expect(deliveryRow.text()).toContain('do.pdf') expect(deliveryRow.text()).toContain('操作员') expect(deliveryRow.text()).toContain('2026-08-13 10:00') expect(deliveryRow.text()).toContain('已上传') expect(deliveryRow.find('.document-file-name').attributes('title')).toBe('do.pdf') }) it('renders one compact normal exception section with a single-line empty history', () => { const normal = { ...overview, exception: { active: false, reason: null, markedBy: null, markedByName: null, markedAt: null }, exceptionHistory: [] } const wrapper = mount(ContainerOverviewPanel, { props: { overview: normal, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } }) const section = wrapper.find('.exception-management') expect(section.text()).toContain('异常管理') expect(section.text()).toContain('正常') expect(section.text()).toContain('标记异常') expect(section.text()).toContain('暂无异常处理记录') expect(section.find('.el-empty').exists()).toBe(false) expect(wrapper.findAll('[aria-label="异常管理"]')).toHaveLength(1) }) it('keeps current exception summary and multiple history rows together without mixing transport logs', () => { const histories = [ ...overview.exceptionHistory, { id: '15', action: 'CANCEL', reason: '查验解除', operatorId: '16', operatorName: '复核员', occurredAt: '2026-08-13T11:30:00' }, ] const wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, exceptionHistory: histories }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } }) const section = wrapper.find('.exception-management') expect(section.text()).toContain('异常') expect(section.text()).toContain('码头查验') expect(section.text()).toContain('操作员 · 2026-08-13 11:00') expect(section.findAll('.exception-history li')).toHaveLength(2) expect(section.text()).toContain('取消异常') expect(section.text()).toContain('复核员') expect(section.text()).toContain('查验解除') expect(section.text()).not.toContain('更新预计到港时间') expect(wrapper.find('[aria-label="运输轨迹和操作日志"]').exists()).toBe(false) }) it('emits the unchanged manage action for mark and cancel buttons', async () => { let wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, exception: { active: false, reason: null, markedBy: null, markedByName: null, markedAt: null } }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } }) await wrapper.find('.exception-management button').trigger('click') expect(wrapper.emitted('manageException')).toHaveLength(1) wrapper.unmount() wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } }) await wrapper.find('.exception-management button').trigger('click') expect(wrapper.emitted('manageException')).toHaveLength(1) expect(wrapper.find('.exception-management button').text()).toContain('取消异常') wrapper.unmount() }) 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') }) 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('delegates warehouse events to the unified fulfillment timeline without exposing technical keys here', () => { 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()).not.toContain('领取 PDA 任务') expect(wrapper.text()).not.toContain('提交拣货结果') expect(wrapper.text()).not.toContain('完成装柜') expect(wrapper.text()).not.toContain(longKey) }) it('renders a compact empty photo state without exposing credentials', () => { const withoutPhotos = { ...overview, pda: overview.pda ? { ...overview.pda, photos: [] } : null } const wrapper = mount(ContainerOverviewPanel, { props: { overview: withoutPhotos, canManageDocuments: false, canManageCustoms: false, canManageException: false }, global: { plugins: [ElementPlus] } }) expect(wrapper.text()).toContain('暂无可查看的有效照片') expect(wrapper.text()).not.toContain('Authorization') }) })