feat(fleet): complete exception processing and trip evidence interfaces
This commit is contained in:
@@ -2,9 +2,10 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils'
|
||||
import TripOperationsRecordDialog from './TripOperationsRecordDialog.vue'
|
||||
import * as api from '../api/tripOperations'
|
||||
vi.mock('../api/tripOperations', () => ({ getTripOperationsRecord: vi.fn(), saveTripOperationsRecord: vi.fn(), listTripOperationsRecordHistory: vi.fn() }))
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
vi.mock('../api/tripOperations', () => ({ getTripOperationsRecord: vi.fn(), saveTripOperationsRecord: vi.fn(), listTripOperationsRecordHistory: vi.fn(), uploadTripOperationsEvidence: vi.fn(), downloadTripOperationsEvidence: vi.fn() }))
|
||||
const row: api.TripOperationsRow = { tripNo: 'TR1', status: 'IN_TRANSIT', vehiclePlate: 'TEST-1', driverName: '司机', warehouseName: '起运仓', departedAt: '2026-10-01T00:00:00Z', completedAt: null, destinations: [], physicalPallets: 1, palletSpaces: 1, ownPackages: 1, externalPackages: 0, externalAgreedAmounts: [], externalPalletRates: [], measured: null, financeStatus: 'NOT_QUERIED', costStatus: 'FORBIDDEN', costs: null }
|
||||
const record: api.TripOperationsRecord = { tripNo: 'TR1', recordVersion: '0', details: { actualDistanceMiles: null, actualFuelLitres: null, remark: null }, updatedAt: null }
|
||||
const record: api.TripOperationsRecord = { tripNo: 'TR1', recordVersion: '0', details: { actualDistanceMiles: null, actualFuelLitres: null, remark: null, evidenceFiles: [] }, updatedAt: null }
|
||||
const wrappers: VueWrapper[] = []
|
||||
const slot = { template: '<div><slot/></div>' }
|
||||
const input = { name: 'ElInputNumber', props: ['modelValue'], emits: ['update:modelValue'], template: '<input :value="modelValue" @input="$emit(\'update:modelValue\',Number($event.target.value))" />' }
|
||||
@@ -41,3 +42,113 @@ it('a failed initial read cannot save a guessed record version', async () => {
|
||||
expect(wrapper.findAll('button').find(b => b.text() === '保存记录')!.attributes('disabled')).toBeDefined()
|
||||
expect(api.saveTripOperationsRecord).not.toHaveBeenCalled()
|
||||
})
|
||||
const evidence: api.TripOperationsEvidence = { fileId: '9007199254740993', fileName: 'meter.pdf', contentType: 'application/pdf', sizeBytes: 4 }
|
||||
async function choose(wrapper: VueWrapper, file = new File(['test'], 'meter.pdf', { type: 'application/pdf' })) {
|
||||
const input = wrapper.find('[data-testid="evidence-input"]'); Object.defineProperty(input.element, 'files', { configurable: true, value: [file] }); await input.trigger('change'); await flushPromises(); return file
|
||||
}
|
||||
it('uploads optionally without creating records; saving binds only the exact IDs and unknown save retains them', async () => {
|
||||
vi.mocked(api.uploadTripOperationsEvidence).mockResolvedValue(evidence)
|
||||
vi.mocked(api.saveTripOperationsRecord).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce({ ...record, recordVersion: '1' })
|
||||
const wrapper = await mount(); await choose(wrapper)
|
||||
expect(api.saveTripOperationsRecord).not.toHaveBeenCalled(); expect(wrapper.text()).toContain('meter.pdf')
|
||||
expect(wrapper.text()).toContain('保存后可预览'); expect(wrapper.findAll('button').find(b => b.text() === 'meter.pdf')!.attributes('disabled')).toBeDefined()
|
||||
await wrapper.findAll('button').find(b => b.text() === '保存记录')!.trigger('click'); await flushPromises()
|
||||
const request = vi.mocked(api.saveTripOperationsRecord).mock.calls[0]![1]
|
||||
expect(request.details.evidenceFileIds).toEqual([evidence.fileId]); expect(request.details).not.toHaveProperty('evidenceFiles')
|
||||
await wrapper.findAll('button').find(b => b.text() === '按原请求重试')!.trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(api.saveTripOperationsRecord).mock.calls[1]![1]).toEqual(request)
|
||||
})
|
||||
it('a committed upload with lost response never retransmits after reopen or reload and preserves bound evidence', async () => {
|
||||
const existing = { ...evidence, fileId: '81', fileName: 'bound.pdf' }
|
||||
vi.mocked(api.getTripOperationsRecord).mockResolvedValue({ ...record, details: { ...record.details, evidenceFiles: [existing] } })
|
||||
let storedFiles = 0
|
||||
vi.mocked(api.uploadTripOperationsEvidence).mockImplementation(async () => { storedFiles++; throw new Error('response lost after storage commit') })
|
||||
const wrapper = await mount(); await choose(wrapper)
|
||||
expect(storedFiles).toBe(1); expect(wrapper.text()).toContain('可能已上传但尚未绑定'); expect(wrapper.text()).toContain('bound.pdf')
|
||||
expect(wrapper.text()).not.toContain('重试原文件'); expect(api.saveTripOperationsRecord).not.toHaveBeenCalled()
|
||||
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
||||
expect(wrapper.text()).toContain('meter.pdf'); expect(wrapper.text()).toContain('可能已上传但尚未绑定')
|
||||
expect(wrapper.find('[data-testid="evidence-input"]').attributes('disabled')).toBeDefined()
|
||||
await wrapper.findAll('button').find(b => b.text() === '重新读取')!.trigger('click'); await flushPromises()
|
||||
await choose(wrapper)
|
||||
expect(storedFiles).toBe(1); expect(api.uploadTripOperationsEvidence).toHaveBeenCalledTimes(1); expect(api.saveTripOperationsRecord).not.toHaveBeenCalled()
|
||||
await wrapper.findAll('button').find(b => b.text() === '暂不绑定此附件')!.trigger('click'); await flushPromises()
|
||||
vi.mocked(api.saveTripOperationsRecord).mockResolvedValue({ ...record, recordVersion: '1' })
|
||||
await wrapper.findAll('button').find(b => b.text() === '保存记录')!.trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(api.saveTripOperationsRecord).mock.calls[0]![1].details.evidenceFileIds).toEqual(['81'])
|
||||
expect(storedFiles).toBe(1)
|
||||
const refreshed = await mount(); expect(refreshed.text()).toContain('bound.pdf'); expect(storedFiles).toBe(1)
|
||||
})
|
||||
it('explicit upload rejection allows corrected file selection without creating a record', async () => {
|
||||
vi.mocked(api.uploadTripOperationsEvidence).mockRejectedValueOnce(new ApiResponseError('文件格式不符', 'A0400', 400)).mockResolvedValueOnce(evidence)
|
||||
const wrapper = await mount(); await choose(wrapper)
|
||||
expect(wrapper.find('[data-testid="evidence-input"]').attributes('disabled')).toBeUndefined()
|
||||
expect(wrapper.text()).not.toContain('可能已上传但尚未绑定')
|
||||
const corrected = new File(['new'], 'corrected.pdf', { type: 'application/pdf' }); await choose(wrapper, corrected)
|
||||
expect(api.uploadTripOperationsEvidence).toHaveBeenNthCalledWith(2, 'TR1', corrected)
|
||||
expect(api.saveTripOperationsRecord).not.toHaveBeenCalled()
|
||||
})
|
||||
it('late upload from another trip cannot attach a file to the newly opened record', async () => {
|
||||
let done!: (value: api.TripOperationsEvidence) => void
|
||||
vi.mocked(api.uploadTripOperationsEvidence).mockReturnValue(new Promise(resolve => { done = resolve }))
|
||||
const wrapper = await mount(); await choose(wrapper)
|
||||
vi.mocked(api.getTripOperationsRecord).mockResolvedValue({ ...record, tripNo: 'TR2' })
|
||||
await wrapper.setProps({ row: { ...row, tripNo: 'TR2' } }); await flushPromises(); done(evidence); await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('meter.pdf'); expect(api.saveTripOperationsRecord).not.toHaveBeenCalled()
|
||||
vi.mocked(api.getTripOperationsRecord).mockResolvedValue(record)
|
||||
await wrapper.setProps({ row }); await flushPromises()
|
||||
expect(wrapper.text()).toContain('meter.pdf'); expect(wrapper.text()).toContain('保存后可预览')
|
||||
})
|
||||
it('upload completing on history tab remains recoverable on the same trip', async () => {
|
||||
let done!: (value: api.TripOperationsEvidence) => void
|
||||
vi.mocked(api.uploadTripOperationsEvidence).mockReturnValue(new Promise(resolve => { done = resolve }))
|
||||
const wrapper = await mount(); await choose(wrapper)
|
||||
wrapper.findComponent({ name: 'ElTabs' }).vm.$emit('update:modelValue', 'history'); await flushPromises()
|
||||
done(evidence); await flushPromises()
|
||||
wrapper.findComponent({ name: 'ElTabs' }).vm.$emit('update:modelValue', 'measured'); await flushPromises()
|
||||
expect(wrapper.text()).toContain('meter.pdf'); expect(wrapper.text()).toContain('保存后可预览')
|
||||
expect(wrapper.findAll('button').find(b => b.text() === '保存记录')!.attributes('disabled')).toBeUndefined()
|
||||
expect(api.saveTripOperationsRecord).not.toHaveBeenCalled()
|
||||
})
|
||||
it('preview is fetched only on explicit click and stale response cannot appear on another trip', async () => {
|
||||
vi.mocked(api.getTripOperationsRecord).mockResolvedValue({ ...record, details: { ...record.details, evidenceFiles: [evidence] } })
|
||||
let done!: (value: Blob) => void
|
||||
vi.mocked(api.downloadTripOperationsEvidence).mockReturnValue(new Promise(resolve => { done = resolve }))
|
||||
const wrapper = await mount(true); expect(api.downloadTripOperationsEvidence).not.toHaveBeenCalled(); expect(wrapper.find('[data-testid="evidence-input"]').exists()).toBe(false)
|
||||
await wrapper.findAll('button').find(b => b.text() === 'meter.pdf')!.trigger('click')
|
||||
vi.mocked(api.getTripOperationsRecord).mockResolvedValue({ ...record, tripNo: 'TR2' })
|
||||
await wrapper.setProps({ row: { ...row, tripNo: 'TR2' } }); await flushPromises(); done(new Blob(['pdf'], { type: 'application/pdf' })); await flushPromises()
|
||||
expect(wrapper.find('iframe').exists()).toBe(false); expect(api.downloadTripOperationsEvidence).toHaveBeenCalledExactlyOnceWith('TR1', evidence.fileId)
|
||||
})
|
||||
it('late save cannot close another trip dialog', async () => {
|
||||
let done!: (value: api.TripOperationsRecord) => void
|
||||
vi.mocked(api.saveTripOperationsRecord).mockReturnValue(new Promise(resolve => { done = resolve }))
|
||||
const wrapper = await mount(); await wrapper.findAll('button').find(b => b.text() === '保存记录')!.trigger('click')
|
||||
vi.mocked(api.getTripOperationsRecord).mockResolvedValue({ ...record, tripNo: 'TR2' }); await wrapper.setProps({ row: { ...row, tripNo: 'TR2' } }); await flushPromises()
|
||||
done({ ...record, recordVersion: '1' }); await flushPromises()
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined(); expect(wrapper.text()).toContain('TR2')
|
||||
})
|
||||
it('closing and reopening the same trip cannot start a concurrent save retry', async () => {
|
||||
let reject!: (error: Error) => void
|
||||
vi.mocked(api.saveTripOperationsRecord).mockReturnValue(new Promise((_, fail) => { reject = fail }))
|
||||
const wrapper = await mount(); await wrapper.findAll('button').find(b => b.text() === '保存记录')!.trigger('click')
|
||||
const original = vi.mocked(api.saveTripOperationsRecord).mock.calls[0]![1]
|
||||
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
||||
const retry = wrapper.findAll('button').find(b => b.text() === '按原请求重试')!
|
||||
expect(retry.attributes('disabled')).toBeDefined(); await retry.trigger('click'); expect(api.saveTripOperationsRecord).toHaveBeenCalledTimes(1)
|
||||
reject(new Error('unknown')); await flushPromises()
|
||||
vi.mocked(api.saveTripOperationsRecord).mockResolvedValue({ ...record, recordVersion: '1' })
|
||||
await wrapper.findAll('button').find(b => b.text() === '按原请求重试')!.trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(api.saveTripOperationsRecord).mock.calls[1]![1]).toEqual(original)
|
||||
})
|
||||
it('closing and reopening the same trip cannot duplicate an in-flight upload', async () => {
|
||||
let done!: (value: api.TripOperationsEvidence) => void
|
||||
vi.mocked(api.uploadTripOperationsEvidence).mockReturnValue(new Promise(resolve => { done = resolve }))
|
||||
const wrapper = await mount(); await choose(wrapper)
|
||||
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
||||
expect(wrapper.find('[data-testid="evidence-input"]').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.text()).not.toContain('重试原文件'); await choose(wrapper); expect(api.uploadTripOperationsEvidence).toHaveBeenCalledTimes(1)
|
||||
done(evidence); await flushPromises()
|
||||
expect(wrapper.text()).toContain('meter.pdf'); expect(wrapper.text()).toContain('保存后可预览')
|
||||
expect(wrapper.findAll('button').find(b => b.text() === '保存记录')!.attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user