Files
admin-web/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.test.ts
T

110 lines
7.3 KiB
TypeScript
Raw Normal View History

2026-10-21 17:08:59 +08:00
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', 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>) {
const input = wrapper.get('input[aria-label="装车照片 1"]')
Object.defineProperty(input.element, 'files', { configurable: true, value: [new File(['image'], 'loading.png', { type: 'image/png' })] })
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('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)
2026-10-22 11:56:54 +08:00
const table = wrapper.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(['装车照片 1', '装车照片 2', '装车照片 3', '装车照片 4', '装车照片 5'])
2026-10-21 17:08:59 +08:00
await upload(wrapper)
expect(api.uploadLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.any(File), expect.any(String))
expect(api.bindLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ recordVersion: '0', fileId: '101' }))
expect(wrapper.emitted('saved')).toHaveLength(1)
expect(wrapper.text()).toContain('图片读取失败')
2026-10-22 11:56:54 +08:00
expect(table.get('tbody tr').text()).toContain('loading.png')
2026-10-21 17:08:59 +08:00
wrapper.unmount()
})
it('unlocks after binding failure and accepts a new upload with a fresh key', async () => {
2026-10-21 17:08:59 +08:00
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)
2026-10-21 17:08:59 +08:00
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
})
it('uses the current version when the operator chooses a photo after a conflict', async () => {
2026-10-21 17:08:59 +08:00
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)
2026-10-21 17:08:59 +08:00
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()
2026-10-21 17:08:59 +08:00
})
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()
})
2026-10-21 17:26:49 +08:00
2026-10-21 17:49:04 +08:00
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()
})
2026-10-21 17:26:49 +08:00
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()).toContain('可边装车边上传')
await upload(wrapper)
expect(wrapper.emitted('saved')).toHaveLength(1)
expect(wrapper.text()).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')
})