import { mount, flushPromises } from '@vue/test-utils' import ElementPlus from 'element-plus' import { beforeEach, expect, it, vi } from 'vitest' import TransportExecutionPanel from './TransportExecutionPanel.vue' import { assignTransport, commandTransport, getTransport, uploadTransport, type TransportDetail } from '../api/transportTasks' import { getDriver } from '@/modules/overseas-master-data' import DispatchEvidenceFiles from './DispatchEvidenceFiles.vue' vi.mock('vue-router', () => ({ onBeforeRouteLeave: vi.fn() })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('@/modules/overseas-master-data', () => ({ listVehicles: vi.fn(), listDrivers: vi.fn(), getDriver: vi.fn(), downloadDriverLicense: vi.fn(), pickupStatusLabels: { ASSIGNED: '待取货', PICKED: '已取货', ARRIVED: '已到仓' } })) vi.mock('../api/transportTasks', () => ({ getTransport: vi.fn(), assignTransport: vi.fn(), transportOccupancy: vi.fn(), cancelTransport: vi.fn(), releaseTransport: vi.fn(), commandTransport: vi.fn(), uploadTransport: vi.fn(), downloadTransportEvidence: vi.fn(), transportLabels: { PICKUP: '到仓取货', LOCAL_TRUCK: '本土卡车' } })) const facts = (arriving = false): TransportDetail => ({ dispatchNo: 'PC1', taskType: 'PICKUP', execution: { trip: { status: arriving ? 'IN_TRANSIT' : 'PLANNED', recordVersion: 4, warehouseName: '洛杉矶仓', vehicleId: '1', vehiclePlate: 'CA-1', driverId: '2', driverName: '司机甲', driverPhone: '555' } }, pickups: [{ customerId: 'C1', transferNo: 'WT1', packageCount: 5, actualPackages: arriving ? 5 : null, status: arriving ? 'PICKED' : 'ASSIGNED', evidence: [] }], completionEvidence: [] }) as unknown as TransportDetail const options = { global: { plugins: [ElementPlus] } } beforeEach(() => { vi.clearAllMocks(); vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) vi.mocked(getTransport).mockResolvedValue(facts()) vi.mocked(getDriver).mockResolvedValue({ id: '2', licenseFileId: '33' } as Awaited>) vi.mocked(uploadTransport).mockResolvedValue({ fileId: '88', fileName: '现场.jpg', contentType: 'image/jpeg', sizeBytes: 10 }) }) const photoOptions = { global: { plugins: [ElementPlus], stubs: { DispatchEvidenceFiles: true } } } it('refreshes the selected driver master license when reopening resource maintenance', async () => { const value = facts() value.resources = { vehicle: null, driver: { driverId: '2', evidence: { photoFileIds: ['22'], fileIds: ['30'], masterLicenseFileId: '30' } } } as TransportDetail['resources'] vi.mocked(getTransport).mockResolvedValue(value) const wrapper = mount(TransportExecutionPanel, { ...photoOptions, props: { dispatchNo: 'PC1', editResources: true } }); await flushPromises() const license = wrapper.findAllComponents(DispatchEvidenceFiles)[2]! expect(license.props('referenceIds')).toEqual(['33']) expect(license.props('modelValue')).toEqual([]) wrapper.unmount() }) it('requires three categories and sends photos with both current versions for review', async () => { const value = facts(); value.dispatchRecordVersion = 9 vi.mocked(getTransport).mockResolvedValue(value) vi.mocked(assignTransport).mockResolvedValue(value) const wrapper = mount(TransportExecutionPanel, { ...photoOptions, props: { dispatchNo: 'PC1', editResources: true } }); await flushPromises() const save = wrapper.findAll('button').find(button => button.text() === '保存车辆司机')! expect(save.attributes('disabled')).toBeDefined() const cards = wrapper.findAllComponents(DispatchEvidenceFiles) expect(cards.map(card => card.props('label'))).toEqual(['车辆图片', '司机照片', '驾驶证照片']) expect(cards[2]!.props('referenceIds')).toEqual(['33']) cards[0]!.vm.$emit('update:modelValue', ['21']); cards[1]!.vm.$emit('update:modelValue', ['22']); await flushPromises() expect(save.attributes('disabled')).toBeUndefined() await save.trigger('click'); await flushPromises() expect(assignTransport).toHaveBeenCalledWith('PC1', expect.objectContaining({ recordVersion: 4, dispatchRecordVersion: 9, vehicleId: '1', driverId: '2', masterLicenseFileId: '33', reviewPhotos: { vehicleFileIds: ['21'], driverPhotoFileIds: ['22'], licenseFileIds: [] } })) wrapper.unmount() }) it('restores saved photos and retains uploads after a failed save', async () => { const value = facts(); value.dispatchRecordVersion = 9 value.resources = { vehicle: { vehicleId: '1', evidenceFileIds: ['21'] }, driver: { driverId: '2', evidence: { photoFileIds: ['22'], fileIds: ['23'] } } } as TransportDetail['resources'] vi.mocked(getTransport).mockResolvedValue(value); vi.mocked(assignTransport).mockRejectedValue(new Error('保存失败')) const wrapper = mount(TransportExecutionPanel, { ...photoOptions, props: { dispatchNo: 'PC1', editResources: true } }); await flushPromises() const cards = wrapper.findAllComponents(DispatchEvidenceFiles) expect(cards.map(card => card.props('modelValue'))).toEqual([['21'], ['22'], ['23']]) expect(getDriver).not.toHaveBeenCalled() cards[0]!.vm.$emit('update:modelValue', ['24']); await flushPromises() await wrapper.findAll('button').find(button => button.text() === '保存车辆司机')!.trigger('click'); await flushPromises() expect(cards[0]!.props('modelValue')).toEqual(['24']) expect(wrapper.emitted('saved')).toBeUndefined(); wrapper.unmount() }) it('clears old photos when selecting another vehicle or driver and ignores late license responses', async () => { let resolve!: (value: Awaited>) => void vi.mocked(getDriver).mockImplementationOnce(() => new Promise(done => { resolve = done })).mockResolvedValue({ id: '4', licenseFileId: '44' } as Awaited>) const wrapper = mount(TransportExecutionPanel, { ...photoOptions, props: { dispatchNo: 'PC1', editResources: true } }); await flushPromises() const cards = wrapper.findAllComponents(DispatchEvidenceFiles) cards[0]!.vm.$emit('update:modelValue', ['21']); cards[1]!.vm.$emit('update:modelValue', ['22']); cards[2]!.vm.$emit('update:modelValue', ['23']) const selectors = wrapper.findAllComponents({ name: 'ElSelect' }) selectors[0]!.vm.$emit('update:modelValue', '3'); selectors[1]!.vm.$emit('update:modelValue', '4'); await flushPromises() resolve({ id: '2', licenseFileId: '33' } as Awaited>); await flushPromises() expect(cards.map(card => card.props('modelValue'))).toEqual([[], [], []]) expect(cards[2]!.props('referenceIds')).toEqual(['44']); wrapper.unmount() }) it.each([false, true])('confirms the %s photo stage with its own evidence and current transport version', async arriving => { vi.mocked(getTransport).mockResolvedValue(facts(arriving)) const wrapper = mount(TransportExecutionPanel, { ...options, props: { dispatchNo: 'PC1', pickupApproved: true } }); await flushPromises() expect(wrapper.text()).toContain(arriving ? '派送中' : '装货中') expect(wrapper.findAll('button').some(button => button.text() === '确认发车')).toBe(false) await wrapper.findAll('button').find(button => button.text() === (arriving ? '上传到仓照片' : '上传装货照片'))!.trigger('click'); await flushPromises() if (!arriving) { const quantity = wrapper.get('input[role="spinbutton"]') expect((quantity.element as HTMLInputElement).value).toBe('') await quantity.setValue('3'); await quantity.trigger('change'); await flushPromises() } const input = wrapper.get('input[type="file"]') Object.defineProperty(input.element, 'files', { value: [new File(['photo'], '现场.jpg', { type: 'image/jpeg' })], configurable: true }) await input.trigger('change'); await flushPromises() await wrapper.findAll('button').find(button => button.text() === (arriving ? '确认到仓' : '确认取货'))!.trigger('click'); await flushPromises() expect(commandTransport).toHaveBeenCalledWith('PC1', `pickup/C1/WT1/${arriving ? 'arrive' : 'picked'}`, expect.objectContaining({ recordVersion: 4, evidenceFileIds: ['88'], ...(arriving ? { reason: '上传到仓照片,确认取货派送完成' } : { actualPackages: 3 }) })) if (arriving) expect(vi.mocked(commandTransport).mock.calls[0]![2]).not.toHaveProperty('actualPackages') expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount() }) it('keeps photo confirmation unavailable until both reviews are approved', async () => { const wrapper = mount(TransportExecutionPanel, { ...options, props: { dispatchNo: 'PC1', pickupApproved: false } }); await flushPromises() expect(wrapper.findAll('button').some(button => button.text() === '上传装货照片')).toBe(false) expect(commandTransport).not.toHaveBeenCalled(); wrapper.unmount() }) it('requires an explicitly entered pickup quantity even after a photo has uploaded', async () => { const wrapper = mount(TransportExecutionPanel, { ...options, props: { dispatchNo: 'PC1', pickupApproved: true } }); await flushPromises() await wrapper.findAll('button').find(button => button.text() === '上传装货照片')!.trigger('click'); await flushPromises() const input = wrapper.get('input[type="file"]') Object.defineProperty(input.element, 'files', { value: [new File(['photo'], '现场.jpg', { type: 'image/jpeg' })], configurable: true }) await input.trigger('change'); await flushPromises() const confirm = wrapper.findAll('button').find(button => button.text() === '确认取货')! expect(confirm.attributes('disabled')).toBeDefined() await confirm.trigger('click') expect(commandTransport).not.toHaveBeenCalled() wrapper.unmount() }) it.each([false, true])('renders read-only transport facts without resource editing or execution controls (%s)', async arriving => { const value = facts(arriving) value.execution.trip.vehicleId = null value.execution.trip.driverId = null vi.mocked(getTransport).mockResolvedValue(value) const wrapper = mount(TransportExecutionPanel, { ...options, props: { dispatchNo: 'PC1', pickupApproved: true, readonly: true } }); await flushPromises() expect(wrapper.text()).toContain('WT1') expect(wrapper.text()).toContain('洛杉矶仓') expect(wrapper.findAll('input, select, textarea')).toHaveLength(0) expect(wrapper.findAll('button')).toHaveLength(0) expect(wrapper.findAll('th').map(column => column.text())).not.toContain('操作') expect(commandTransport).not.toHaveBeenCalled() wrapper.unmount() }) it('keeps pickup addresses and contacts on their own transfer rows', async () => { const value = facts() value.pickups[0] = { ...value.pickups[0]!, pickupAddress: '100 Test Pickup Avenue', contactName: '取货联系人甲', contactPhone: '0000000001' } value.pickups.push({ ...value.pickups[0]!, transferNo: 'WT2', customerId: 'C2', pickupAddress: '200 Test Pickup Road', contactName: '取货联系人乙', contactPhone: '0000000002' }) vi.mocked(getTransport).mockResolvedValue(value) const wrapper = mount(TransportExecutionPanel, { ...options, props: { dispatchNo: 'PC1', readonly: true } }); await flushPromises() const rows = wrapper.get('[aria-label="取货明细"]').findAll('tbody tr') expect(rows[0]!.text()).toContain('100 Test Pickup Avenue') expect(rows[0]!.text()).toContain('取货联系人甲') expect(rows[0]!.text()).toContain('0000000001') expect(rows[0]!.text()).not.toContain('200 Test Pickup Road') expect(rows[1]!.text()).toContain('200 Test Pickup Road') expect(rows[1]!.text()).toContain('取货联系人乙') expect(rows[1]!.text()).toContain('0000000002') expect(wrapper.findAll('button')).toHaveLength(0) wrapper.unmount() })