236 lines
16 KiB
TypeScript
236 lines
16 KiB
TypeScript
import { flushPromises, mount } from '@vue/test-utils'
|
|
import ElementPlus from 'element-plus'
|
|
import { beforeEach, expect, it, vi } from 'vitest'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import DispatchLoadingDocuments from './DispatchLoadingDocuments.vue'
|
|
import * as api from '../api/dispatchLoadingDocuments'
|
|
|
|
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/dispatchLoadingDocuments', async importOriginal => ({ ...await importOriginal<typeof api>(), getLoadingDocuments: vi.fn(), uploadLoadingPhoto: vi.fn(), recoverLoadingPhoto: vi.fn(), bindLoadingPhoto: vi.fn(), readLoadingPhoto: vi.fn() }))
|
|
const photo: api.LoadingPhoto = { slot: 'LOADING_SCENE', fileId: '101', fileName: 'loading.png', contentType: 'image/png', savedAt: null }
|
|
const data = (extra: Partial<api.LoadingDocuments> = {}): api.LoadingDocuments => ({ dispatchNo: 'PC/1', recordVersion: '0', dispatchRecordVersion: '3', reviewRound: '1', stage: 'AFTER_LOADING', editable: true, complete: false, photos: [], ...extra })
|
|
function setup() { return mount(DispatchLoadingDocuments, { props: { dispatchNo: 'PC/1' }, global: { plugins: [ElementPlus] } }) }
|
|
async function upload(wrapper: ReturnType<typeof setup>, files = [new File(['image'], 'loading.png', { type: 'image/png' })]) {
|
|
const input = wrapper.get('input[aria-label="上传司机驾驶证"]')
|
|
Object.defineProperty(input.element, 'files', { configurable: true, value: files })
|
|
await input.trigger('change'); await flushPromises()
|
|
}
|
|
beforeEach(() => {
|
|
vi.clearAllMocks(); sessionStorage.clear(); can.mockReturnValue(true)
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data())
|
|
vi.mocked(api.uploadLoadingPhoto).mockResolvedValue(photo)
|
|
vi.mocked(api.bindLoadingPhoto).mockResolvedValue(data({ recordVersion: '1', photos: [photo] }))
|
|
vi.mocked(api.readLoadingPhoto).mockRejectedValue(new Error('preview unavailable'))
|
|
})
|
|
it('refreshes photos added or replaced by PDA without writing and notifies the parent to reload the stage', async () => {
|
|
can.mockReturnValue(false)
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ photos: [photo] }))
|
|
const wrapper = setup(); await flushPromises()
|
|
const photos = api.loadingPhotoSlots.flatMap((slot, category) => Array.from({ length: 5 }, (_, index) => ({
|
|
...photo, slot, fileId: `${category + 2}${index}`, fileName: `pda-${category}-${index}.png`,
|
|
})))
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ recordVersion: '26', stage: 'DELIVERING', complete: true, photos }))
|
|
await wrapper.get('[aria-label="刷新装车资料"]').trigger('click'); await flushPromises()
|
|
expect(wrapper.findAll('.loading-photo-row')).toHaveLength(25)
|
|
for (const item of photos) expect(wrapper.text()).not.toContain(item.fileName)
|
|
expect(wrapper.text()).not.toContain(photo.fileName)
|
|
expect(wrapper.findAll('.loading-document-heading').map(row => row.text())).toEqual(Object.values(api.loadingPhotoLabels).map(label => `${label}5/5 张`))
|
|
expect(wrapper.emitted('saved')).toHaveLength(1)
|
|
expect(api.uploadLoadingPhoto).not.toHaveBeenCalled(); expect(api.bindLoadingPhoto).not.toHaveBeenCalled()
|
|
vi.mocked(api.getLoadingDocuments).mockRejectedValueOnce(new Error('刷新失败'))
|
|
await wrapper.get('[aria-label="刷新装车资料"]').trigger('click'); await flushPromises()
|
|
expect(wrapper.findAll('.loading-photo-row')).toHaveLength(25)
|
|
expect(wrapper.emitted('saved')).toHaveLength(1)
|
|
wrapper.unmount()
|
|
})
|
|
it('offers five whole-dispatch slots and only reports saved after binding succeeds', async () => {
|
|
const wrapper = setup(); await flushPromises()
|
|
expect(wrapper.findAll('input[type="file"]')).toHaveLength(5)
|
|
const list = wrapper.get('[aria-label="装车照片"]')
|
|
expect(wrapper.find('table').exists()).toBe(false)
|
|
expect(list.findAll('h3').map(heading => heading.text())).toEqual(Object.values(api.loadingPhotoLabels))
|
|
expect(list.findAll('.photo-count').map(count => count.text())).toEqual(Array(5).fill('0/5 张'))
|
|
expect(list.findAll('.photo-empty')).toHaveLength(5)
|
|
await upload(wrapper)
|
|
expect(api.uploadLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.any(File), expect.any(String), '1')
|
|
expect(api.bindLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ recordVersion: '0', fileId: '101' }))
|
|
expect(vi.mocked(api.bindLoadingPhoto).mock.calls[0]![2].uploadKey).toBe(vi.mocked(api.uploadLoadingPhoto).mock.calls[0]![3])
|
|
expect(wrapper.emitted('saved')).toHaveLength(1)
|
|
expect(wrapper.text()).toContain('图片读取失败')
|
|
expect(list.get('.loading-photo-row').attributes('aria-label')).toBe('司机驾驶证第1张')
|
|
expect(list.text()).not.toContain('loading.png')
|
|
wrapper.unmount()
|
|
})
|
|
it('stops a batch when re-review changes the source during upload instead of rebasing old files', async () => {
|
|
const wrapper = setup(); await flushPromises()
|
|
vi.mocked(api.uploadLoadingPhoto).mockImplementationOnce(async () => {
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ recordVersion: '8', dispatchRecordVersion: '12', reviewRound: '2' }))
|
|
return photo
|
|
})
|
|
await upload(wrapper, [new File(['a'], 'old-driver.png', { type: 'image/png' }), new File(['b'], 'old-driver-2.png', { type: 'image/png' })])
|
|
expect(api.bindLoadingPhoto).not.toHaveBeenCalled()
|
|
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(1)
|
|
expect(vi.mocked(api.uploadLoadingPhoto).mock.calls[0]![4]).toBe('1')
|
|
expect(wrapper.text()).toContain('审核轮次已变化')
|
|
expect(wrapper.emitted('saved')).toHaveLength(1)
|
|
wrapper.unmount()
|
|
})
|
|
it('shows an imported PDF license as an authenticated document and accepts an exact PDF replacement', async () => {
|
|
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:license')
|
|
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})
|
|
const license = { ...photo, fileName: 'license.pdf', contentType: 'application/pdf' }
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ photos: [license] }))
|
|
vi.mocked(api.readLoadingPhoto).mockResolvedValue(new Blob(['%PDF-1.4'], { type: 'application/pdf' }))
|
|
const wrapper = setup()
|
|
try {
|
|
await flushPromises()
|
|
expect(wrapper.get('a').attributes('href')).toBe('blob:license')
|
|
expect(wrapper.findComponent({ name: 'ElImage' }).exists()).toBe(false)
|
|
const input = wrapper.get('input[aria-label="更换司机驾驶证第1张"]')
|
|
expect(input.attributes('accept')).toContain('application/pdf')
|
|
expect(wrapper.get('input[aria-label="上传车牌照片"]').attributes('accept')).not.toContain('application/pdf')
|
|
Object.defineProperty(input.element, 'files', { configurable: true, value: [new File(['%PDF-1.4'], 'new.pdf', { type: 'application/pdf' })] })
|
|
await input.trigger('change'); await flushPromises()
|
|
expect(api.bindLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ replaceFileId: '101' }))
|
|
} finally { wrapper.unmount(); vi.restoreAllMocks() }
|
|
})
|
|
it('shows every photo in a category, stops at five and replaces the selected image only', async () => {
|
|
const photos = Array.from({ length: 5 }, (_, index) => ({ ...photo, fileId: String(101 + index), fileName: `photo-${index}.png`, savedAt: `2026-10-26T11:49:0${index}.978960362Z` }))
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ recordVersion: '5', photos }))
|
|
const replacement = { ...photo, fileId: '999' }
|
|
vi.mocked(api.uploadLoadingPhoto).mockResolvedValue(replacement)
|
|
vi.mocked(api.bindLoadingPhoto).mockResolvedValue(data({ recordVersion: '6', photos: photos.map(item => item.fileId === '103' ? replacement : item) }))
|
|
const wrapper = setup(); await flushPromises()
|
|
expect(wrapper.get('input[aria-label="上传司机驾驶证"]').attributes('disabled')).toBeDefined()
|
|
const rows = wrapper.findAll('.loading-photo-row')
|
|
expect(rows).toHaveLength(5)
|
|
rows.forEach((row, index) => {
|
|
expect(row.attributes('aria-label')).toBe(`司机驾驶证第${index + 1}张`)
|
|
expect(row.get('.photo-time').text()).toMatch(new RegExp(`保存时间:2026-10-26 \\d{2}:49:0${index}$`))
|
|
expect(row.findAll('input[type="file"]')).toHaveLength(1)
|
|
})
|
|
for (const item of photos) expect(wrapper.text()).not.toContain(item.fileName)
|
|
const input = rows[2]!.get('input[aria-label="更换司机驾驶证第3张"]')
|
|
Object.defineProperty(input.element, 'files', { configurable: true, value: [new File(['new'], 'new.png', { type: 'image/png' })] })
|
|
await input.trigger('change'); await flushPromises()
|
|
expect(api.bindLoadingPhoto).toHaveBeenLastCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ recordVersion: '5', fileId: '999', replaceFileId: '103' }))
|
|
expect(wrapper.findAll('.loading-photo-row')).toHaveLength(5)
|
|
expect(wrapper.findAll('.loading-photo-row')[0]!.get('.photo-time').text()).toMatch(/:49:00$/)
|
|
expect(wrapper.findAll('.loading-photo-row')[2]!.get('.photo-time').text()).toBe('保存时间:—')
|
|
expect(wrapper.findAll('.loading-photo-row')[4]!.get('.photo-time').text()).toMatch(/:49:04$/)
|
|
wrapper.unmount()
|
|
})
|
|
it('saves a multiple selection with fresh versions and notifies the parent once', async () => {
|
|
let latest = data(), id = 200
|
|
vi.mocked(api.getLoadingDocuments).mockImplementation(async () => latest)
|
|
vi.mocked(api.uploadLoadingPhoto).mockImplementation(async () => ({ ...photo, fileId: String(++id) }))
|
|
vi.mocked(api.bindLoadingPhoto).mockImplementation(async (_no, _slot, body) => {
|
|
latest = data({ recordVersion: String(Number(body.recordVersion) + 1), photos: [...latest.photos, { ...photo, fileId: body.fileId }] }); return latest
|
|
})
|
|
const wrapper = setup(); await flushPromises()
|
|
expect(wrapper.get('input[aria-label="上传司机驾驶证"]').attributes('multiple')).toBeDefined()
|
|
await upload(wrapper, Array.from({ length: 3 }, () => new File(['image'], 'photo.png', { type: 'image/png' })))
|
|
expect(api.bindLoadingPhoto).toHaveBeenCalledTimes(3)
|
|
expect(vi.mocked(api.bindLoadingPhoto).mock.calls.map(call => call[2].recordVersion)).toEqual(['0', '1', '2'])
|
|
expect(wrapper.emitted('saved')).toHaveLength(1); expect(wrapper.text()).toContain('3/5 张')
|
|
await upload(wrapper, Array.from({ length: 3 }, () => new File(['image'], 'photo.png', { type: 'image/png' })))
|
|
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(3)
|
|
wrapper.unmount()
|
|
})
|
|
it('stops a batch after an unknown binding result and displays the server snapshot', async () => {
|
|
let latest = data(), id = 200, binds = 0
|
|
vi.mocked(api.getLoadingDocuments).mockImplementation(async () => latest)
|
|
vi.mocked(api.uploadLoadingPhoto).mockImplementation(async () => ({ ...photo, fileId: String(++id) }))
|
|
vi.mocked(api.bindLoadingPhoto).mockImplementation(async (_no, _slot, body) => {
|
|
latest = data({ recordVersion: String(++binds), photos: [...latest.photos, { ...photo, fileId: body.fileId }] })
|
|
if (binds === 2) throw new Error('保存响应超时')
|
|
return latest
|
|
})
|
|
const wrapper = setup(); await flushPromises()
|
|
await upload(wrapper, Array.from({ length: 3 }, () => new File(['image'], 'photo.png', { type: 'image/png' })))
|
|
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(2); expect(api.bindLoadingPhoto).toHaveBeenCalledTimes(2)
|
|
expect(wrapper.text()).toContain('2/5 张'); expect(wrapper.text()).toContain('已读取最新资料')
|
|
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
|
|
})
|
|
it('requires a successful refresh after both binding and reconciliation fail', async () => {
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValueOnce(data()).mockResolvedValueOnce(data()).mockRejectedValueOnce(new Error('读取失败')).mockResolvedValue(data())
|
|
vi.mocked(api.bindLoadingPhoto).mockRejectedValueOnce(new Error('保存超时'))
|
|
const wrapper = setup(); await flushPromises(); await upload(wrapper)
|
|
expect(wrapper.text()).toContain('保存状态尚未核实')
|
|
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
|
|
wrapper.findComponent({ name: 'LoadErrorState' }).vm.$emit('retry'); await flushPromises()
|
|
expect(wrapper.find('input[aria-label="上传司机驾驶证"]').exists()).toBe(true)
|
|
expect(api.bindLoadingPhoto).toHaveBeenCalledTimes(1); wrapper.unmount()
|
|
})
|
|
it('unlocks after binding failure and accepts a new upload with a fresh key', async () => {
|
|
vi.mocked(api.bindLoadingPhoto).mockRejectedValueOnce(new Error('network unknown'))
|
|
const wrapper = setup(); await flushPromises(); await upload(wrapper)
|
|
expect(wrapper.emitted('saved')).toBeUndefined()
|
|
const original = vi.mocked(api.bindLoadingPhoto).mock.calls[0]![2]
|
|
await upload(wrapper)
|
|
expect(api.bindLoadingPhoto).toHaveBeenLastCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ fileId: original.fileId })); expect(vi.mocked(api.bindLoadingPhoto).mock.calls[1]![2].idempotencyKey).not.toBe(original.idempotencyKey)
|
|
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(2)
|
|
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
|
|
})
|
|
it('uses the current version when the operator chooses a photo after a conflict', async () => {
|
|
vi.mocked(api.bindLoadingPhoto).mockRejectedValueOnce(new ApiResponseError('version conflict', 'A0409'))
|
|
const wrapper = setup(); await flushPromises(); await upload(wrapper)
|
|
const original = vi.mocked(api.bindLoadingPhoto).mock.calls[0]![2]
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ recordVersion: '4' }))
|
|
vi.mocked(api.bindLoadingPhoto).mockResolvedValue(data({ stage: 'DELIVERING', complete: true, recordVersion: '5' }))
|
|
await upload(wrapper)
|
|
expect(api.bindLoadingPhoto).toHaveBeenLastCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ recordVersion: '4', fileId: '101' }))
|
|
expect(vi.mocked(api.bindLoadingPhoto).mock.calls[1]![2].idempotencyKey).not.toBe(original.idempotencyKey)
|
|
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(2); expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
|
|
})
|
|
it('shows existing photos without exposing upload for a read-only account', async () => {
|
|
can.mockReturnValue(false)
|
|
const wrapper = setup(); await flushPromises()
|
|
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
|
|
expect(api.uploadLoadingPhoto).not.toHaveBeenCalled(); wrapper.unmount()
|
|
})
|
|
|
|
it('allows choosing another image after the server definitively rejects the file', async () => {
|
|
vi.mocked(api.uploadLoadingPhoto).mockRejectedValueOnce(new ApiResponseError('图片内容无效', 'A0400'))
|
|
const wrapper = setup(); await flushPromises(); await upload(wrapper)
|
|
expect(wrapper.text()).toContain('图片内容无效')
|
|
expect(wrapper.find('[role="status"]').exists()).toBe(false)
|
|
expect(api.bindLoadingPhoto).not.toHaveBeenCalled()
|
|
await upload(wrapper)
|
|
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(2)
|
|
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
|
|
})
|
|
|
|
it('allows saving photos while loading and keeps the loading stage', async () => {
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ stage: 'LOADING' }))
|
|
vi.mocked(api.bindLoadingPhoto).mockResolvedValue(data({ stage: 'LOADING', recordVersion: '1', photos: [photo] }))
|
|
const wrapper = setup(); await flushPromises()
|
|
expect(wrapper.text()).not.toContain('可边装车边上传')
|
|
await upload(wrapper)
|
|
expect(wrapper.emitted('saved')).toHaveLength(1)
|
|
expect(wrapper.text()).not.toContain('可边装车边上传'); wrapper.unmount()
|
|
})
|
|
|
|
it('renders the authenticated saved photo and fullscreen preview for a read-only Admin', async () => {
|
|
can.mockReturnValue(false)
|
|
const originalURL = URL
|
|
const create = vi.fn(() => 'blob:admin-loading-photo'), revoke = vi.fn()
|
|
vi.stubGlobal('URL', class extends originalURL { static createObjectURL = create; static revokeObjectURL = revoke })
|
|
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ stage: 'LOADING', photos: [photo] }))
|
|
const image = new Blob(['verified image'], { type: 'image/png' })
|
|
vi.mocked(api.readLoadingPhoto).mockResolvedValue(image)
|
|
const wrapper = setup()
|
|
try {
|
|
await flushPromises()
|
|
expect(api.readLoadingPhoto).toHaveBeenCalledWith('PC/1', photo)
|
|
expect(create).toHaveBeenCalledWith(image)
|
|
const preview = wrapper.findComponent({ name: 'ElImage' })
|
|
expect(preview.props('src')).toBe('blob:admin-loading-photo')
|
|
expect(preview.props('previewSrcList')).toEqual(['blob:admin-loading-photo'])
|
|
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
|
|
} finally { wrapper.unmount(); vi.stubGlobal('URL', originalURL) }
|
|
expect(revoke.mock.calls[0]?.[0]).toBe('blob:admin-loading-photo')
|
|
})
|