import ElementPlus from 'element-plus' import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import DispatchVehicleDocuments from './DispatchVehicleDocuments.vue' import { bindVehiclePhoto, getVehicleDocuments, recoverVehiclePhoto, uploadVehiclePhoto, type VehicleDocuments, type VehiclePhoto } from '../api/dispatchVehicleDocuments' const can = vi.hoisted(() => vi.fn(() => true)) vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: { id: 'operator' } }) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) })) vi.mock('../api/dispatchVehicleDocuments', () => ({ bindVehiclePhoto: vi.fn(), getVehicleDocuments: vi.fn(), recoverVehiclePhoto: vi.fn(), uploadVehiclePhoto: vi.fn(), readVehiclePhoto: vi.fn() })) const photo: VehiclePhoto = { slot: 'FUEL_START', fileId: '9007199254740995', fileName: 'fuel.png', contentType: 'image/png', savedAt: null } function response(version = '3'): VehicleDocuments { return { dispatchNo: 'PC/1', recordVersion: version, editable: true, photos: [] } } function setup() { return mount(DispatchVehicleDocuments, { props: { dispatchNo: 'PC/1' }, global: { plugins: [ElementPlus] } }) } async function upload(wrapper: ReturnType) { const input = wrapper.get('input'); Object.defineProperty(input.element, 'files', { value: [new File(['image'], 'fuel.png', { type: 'image/png' })], configurable: true }); await input.trigger('change'); await flushPromises() } beforeEach(() => { vi.clearAllMocks(); sessionStorage.clear(); can.mockReturnValue(true) vi.mocked(getVehicleDocuments).mockResolvedValue(response()) vi.mocked(uploadVehiclePhoto).mockResolvedValue(photo) vi.mocked(recoverVehiclePhoto).mockResolvedValue(photo) vi.mocked(bindVehiclePhoto).mockResolvedValue({ ...response('4'), photos: [{ ...photo, savedAt: '2026-10-21T00:00:00Z' }] }) }) it('does not restore failed bindings after reopening and allows a new upload', async () => { vi.mocked(bindVehiclePhoto).mockRejectedValueOnce(new Error('network timeout')) const first = setup(); await flushPromises(); await upload(first) const table = first.get('table[aria-label="油耗图片"]') expect(table.findAll('thead th').map(cell => cell.text())).toEqual(['操作', '图片类别', '文件名称', '保存时间']) expect(table.findAll('tbody tr').map(row => row.findAll('td')[1]!.text())).toEqual(['起始油耗表盘', '结束油耗表盘']) const original = vi.mocked(bindVehiclePhoto).mock.calls[0]![2] expect(original).toMatchObject({ recordVersion: '3', fileId: photo.fileId }) first.unmount() const reopened = setup(); await flushPromises() await upload(reopened) expect(vi.mocked(bindVehiclePhoto).mock.calls[1]![2].idempotencyKey).not.toBe(original.idempotencyKey) expect(uploadVehiclePhoto).toHaveBeenCalledTimes(2) expect(reopened.text()).toContain('fuel.png') expect(reopened.get('table tbody tr').text()).toContain('2026-10-21 00:00:00Z') expect(sessionStorage.length).toBe(0) reopened.unmount() }) it('unlocks uncertain uploads for a new file selection', async () => { vi.mocked(uploadVehiclePhoto).mockRejectedValueOnce(new Error('connection lost')) const wrapper = setup(); await flushPromises(); await upload(wrapper) const originalKey = vi.mocked(uploadVehiclePhoto).mock.calls[0]![3] await upload(wrapper) expect(recoverVehiclePhoto).not.toHaveBeenCalled(); expect(vi.mocked(uploadVehiclePhoto).mock.calls[1]![3]).not.toBe(originalKey) expect(uploadVehiclePhoto).toHaveBeenCalledTimes(2) expect(bindVehiclePhoto).toHaveBeenCalledTimes(1) wrapper.unmount() }) it('allows a new photo and current version after a binding conflict', async () => { vi.mocked(bindVehiclePhoto).mockRejectedValueOnce(new ApiResponseError('运营记录版本已变化,请刷新后重试', 'A0409', 409)) const wrapper = setup(); await flushPromises(); await upload(wrapper) const originalKey = vi.mocked(bindVehiclePhoto).mock.calls[0]![2].idempotencyKey vi.mocked(getVehicleDocuments).mockResolvedValue(response('8')) await upload(wrapper) expect(bindVehiclePhoto).toHaveBeenLastCalledWith('PC/1', 'FUEL_START', { recordVersion: '8', fileId: photo.fileId, idempotencyKey: expect.any(String) }) expect(vi.mocked(bindVehiclePhoto).mock.calls[1]![2].idempotencyKey).not.toBe(originalKey) expect(uploadVehiclePhoto).toHaveBeenCalledTimes(2) wrapper.unmount() }) it('does not offer upload with list permission alone', async () => { can.mockReturnValue(false) const wrapper = setup(); await flushPromises() expect(wrapper.find('input[type="file"]').exists()).toBe(false) expect(uploadVehiclePhoto).not.toHaveBeenCalled() wrapper.unmount() })