197 lines
15 KiB
TypeScript
197 lines
15 KiB
TypeScript
import ElementPlus, { ElMessage } from 'element-plus'
|
|
import { defineComponent, h } from 'vue'
|
|
import { DOMWrapper, flushPromises, mount } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import DeliveryDocumentsDialog from './DeliveryDocumentsDialog.vue'
|
|
import { commonPhase5Stubs } from '../pages/phase5TestStubs'
|
|
import { deliveryDocumentCategories, getDeliveryDocuments, saveDeliveryDocuments, uploadDeliveryDocument, type DeliveryDocuments } from '../api/deliveryDocuments'
|
|
const can = vi.hoisted(() => vi.fn(() => true))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
|
vi.mock('../api/deliveryDocuments', async importOriginal => ({ ...await importOriginal<typeof import('../api/deliveryDocuments')>(), getDeliveryDocuments: vi.fn(), saveDeliveryDocuments: vi.fn(), uploadDeliveryDocument: vi.fn(), downloadDeliveryDocument: vi.fn() }))
|
|
const dialogStub = defineComponent({
|
|
name: 'AppDialog', props: ['modelValue', 'showConfirm', 'confirmDisabled'], emits: ['confirm', 'update:modelValue'],
|
|
setup(props, { slots, emit }) { return () => props.modelValue ? h('section', [slots.default?.(), props.showConfirm ? h('button', { class: 'save', disabled: props.confirmDisabled, onClick: () => emit('confirm') }, '保存资料') : null, h('button', { class: 'close', onClick: () => emit('update:modelValue', false) }, '关闭')]) : null },
|
|
})
|
|
const uploadStub = defineComponent({
|
|
name: 'ElUpload', props: ['onChange', 'disabled'],
|
|
setup(props, { slots }) { return () => h('label', [slots.default?.(), h('input', { type: 'file', disabled: props.disabled, onChange: (event: Event) => props.onChange({ raw: (event.target as HTMLInputElement).files?.[0] }) })]) },
|
|
})
|
|
const originalFile = { fileId: '9007199254740995', fileName: 'original.pdf', contentType: 'application/pdf', sizeBytes: 4, uploadedAt: '2026-09-09T09:00:00' }
|
|
function response(): DeliveryDocuments {
|
|
return { deliveryNo: 'DL/1', customerId: '100', version: 9, bolNo: 'BOL-1', uploadedCategoryCount: 1, totalCategoryCount: 3, allowedActions: ['MANAGE_DOCUMENTS'], replayed: false, categories: deliveryDocumentCategories.map(item => ({ category: item.category, status: item.category === 'BOL' ? 'UPLOADED' : 'MISSING', fileCount: item.category === 'BOL' ? 1 : 0, files: item.category === 'BOL' ? [{ ...originalFile }] : [] })) }
|
|
}
|
|
function mountDialog() {
|
|
const wrapper = mount(DeliveryDocumentsDialog, { props: { modelValue: true, deliveryNo: 'DL/1', customerId: '100' }, global: { plugins: [ElementPlus], stubs: { ...commonPhase5Stubs, AppDialog: dialogStub, ElUpload: uploadStub } } })
|
|
return wrapper
|
|
}
|
|
async function choose(input: ReturnType<ReturnType<typeof mountDialog>['find']>, file: File) {
|
|
Object.defineProperty(input.element, 'files', { value: [file], configurable: true })
|
|
await input.trigger('change'); await flushPromises()
|
|
}
|
|
describe('delivery document drafts', () => {
|
|
beforeEach(() => {
|
|
can.mockReset().mockReturnValue(true)
|
|
vi.mocked(getDeliveryDocuments).mockReset().mockResolvedValue(response())
|
|
vi.mocked(saveDeliveryDocuments).mockReset().mockResolvedValue(response())
|
|
vi.mocked(uploadDeliveryDocument).mockReset().mockResolvedValue({ ...originalFile, fileId: '9007199254740996', fileName: 'new.pdf' })
|
|
})
|
|
it('loads once and stages replacement and deletion until a full versioned save', async () => {
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
expect(getDeliveryDocuments).toHaveBeenCalledTimes(1)
|
|
expect(wrapper.find('[aria-label="派送资料摘要"]').exists()).toBe(false)
|
|
expect(wrapper.findAll('.document-file')).toHaveLength(1)
|
|
expect(wrapper.findAll('.document-files th').map(cell => cell.text())).toEqual(['操作', '文件名称'])
|
|
await choose(wrapper.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['next'], 'new.pdf'))
|
|
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
|
expect(wrapper.text()).not.toContain('original.pdf')
|
|
expect(wrapper.text()).toContain('new.pdf')
|
|
expect(wrapper.get('.document-file-pending').text()).toBe('待保存')
|
|
await wrapper.find('[data-category="BOL"] .document-file').findAll('button').find(button => button.text() === '删除')!.trigger('click')
|
|
expect(wrapper.findAll('.document-file')).toHaveLength(0)
|
|
await wrapper.find('.save').trigger('click'); await flushPromises()
|
|
expect(saveDeliveryDocuments).toHaveBeenCalledWith('DL/1', '100', { expectedVersion: 9, idempotencyKey: expect.any(String), bolNo: 'BOL-1', documents: deliveryDocumentCategories.map(item => ({ category: item.category, fileIds: [] })) })
|
|
expect(wrapper.emitted('saved')).toHaveLength(1)
|
|
wrapper.unmount()
|
|
})
|
|
it('uses real upload controls to add, replace, delete and reload the saved three-category snapshot', async () => {
|
|
const success = vi.spyOn(ElMessage, 'success')
|
|
const receipt = { ...originalFile, fileId: 'receipt-1', fileName: 'pod.pdf' }
|
|
const courier = { ...originalFile, fileId: 'receipt-2', fileName: 'courier.pdf' }
|
|
const replacement = { ...originalFile, fileId: 'bol-2', fileName: 'signed-bol.pdf' }
|
|
vi.mocked(uploadDeliveryDocument).mockResolvedValueOnce(receipt).mockResolvedValueOnce(courier).mockResolvedValueOnce(replacement)
|
|
const wrapper = mount(DeliveryDocumentsDialog, { attachTo: document.body, props: { modelValue: true, deliveryNo: 'DL/1', customerId: '100' }, global: { plugins: [ElementPlus] } })
|
|
const body = new DOMWrapper(document.body)
|
|
try {
|
|
await flushPromises()
|
|
const dialog = body.find('[role="dialog"]')
|
|
expect(dialog.findAll('[data-category]')).toHaveLength(3)
|
|
await choose(dialog.find('[data-category="RECEIPT"] header input[type="file"]'), new File(['pod'], 'pod.pdf'))
|
|
await choose(dialog.find('[data-category="RECEIPT"] header input[type="file"]'), new File(['courier'], 'courier.pdf'))
|
|
expect(dialog.findAll('[data-category="RECEIPT"] .document-file')).toHaveLength(2)
|
|
await choose(dialog.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['signed'], 'signed-bol.pdf'))
|
|
await dialog.find('[data-category="RECEIPT"] .document-file').findAll('button').find(button => button.text() === '删除')!.trigger('click')
|
|
expect(dialog.text()).not.toContain('pod.pdf')
|
|
expect(dialog.text()).toContain('courier.pdf')
|
|
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
|
await dialog.findAll('button').find(button => button.text() === '保存资料')!.trigger('click'); await flushPromises()
|
|
expect(saveDeliveryDocuments).toHaveBeenCalledWith('DL/1', '100', { expectedVersion: 9, idempotencyKey: expect.any(String), bolNo: 'BOL-1', documents: [{ category: 'LOADING_PHOTO', fileIds: [] }, { category: 'BOL', fileIds: ['bol-2'] }, { category: 'RECEIPT', fileIds: ['receipt-2'] }] })
|
|
expect(wrapper.emitted('saved')).toHaveLength(1)
|
|
expect(success).toHaveBeenCalledWith('签收文件已保存,派送单已自动完成')
|
|
const persisted = response()
|
|
persisted.version = 10; persisted.uploadedCategoryCount = 2
|
|
persisted.categories = persisted.categories.map(group => ({ ...group, files: group.category === 'BOL' ? [replacement] : group.category === 'RECEIPT' ? [courier] : [], fileCount: group.category === 'LOADING_PHOTO' ? 0 : 1, status: group.category === 'LOADING_PHOTO' ? 'MISSING' : 'UPLOADED' }))
|
|
vi.mocked(getDeliveryDocuments).mockResolvedValue(persisted)
|
|
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
|
const reopened = body.find('[role="dialog"]')
|
|
expect(reopened.findAll('.document-file')).toHaveLength(2)
|
|
expect(reopened.find('.document-file-pending').exists()).toBe(false)
|
|
expect(reopened.text()).toContain('signed-bol.pdf')
|
|
expect(reopened.text()).toContain('courier.pdf')
|
|
expect(reopened.text()).not.toContain('original.pdf')
|
|
expect(getDeliveryDocuments).toHaveBeenCalledTimes(2)
|
|
} finally { wrapper.unmount() }
|
|
})
|
|
it('discards unsaved uploads on cancel and reloads persisted categories on reopening', async () => {
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), new File(['proof'], 'new.pdf'))
|
|
expect(wrapper.findAll('.document-file')).toHaveLength(2)
|
|
await wrapper.find('.close').trigger('click')
|
|
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
|
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
|
expect(wrapper.emitted('saved')).toBeUndefined()
|
|
expect(wrapper.findAll('.document-file')).toHaveLength(1)
|
|
expect(wrapper.find('.document-file-pending').exists()).toBe(false)
|
|
expect(wrapper.text()).not.toContain('new.pdf')
|
|
wrapper.unmount()
|
|
})
|
|
it('preserves the draft and idempotency key on a failed save, and prevents duplicate submissions', async () => {
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
let reject!: (error: Error) => void
|
|
vi.mocked(saveDeliveryDocuments).mockReturnValueOnce(new Promise((_resolve, rejectRequest) => { reject = rejectRequest }))
|
|
await wrapper.find('.save').trigger('click'); await wrapper.find('.save').trigger('click')
|
|
expect(saveDeliveryDocuments).toHaveBeenCalledTimes(1)
|
|
reject(new Error('保存失败')); await flushPromises()
|
|
expect(wrapper.text()).toContain('original.pdf')
|
|
expect(wrapper.emitted('saved')).toBeUndefined()
|
|
const key = vi.mocked(saveDeliveryDocuments).mock.calls[0]![2].idempotencyKey
|
|
await wrapper.find('.save').trigger('click'); await flushPromises()
|
|
expect(vi.mocked(saveDeliveryDocuments).mock.calls[1]![2].idempotencyKey).toBe(key)
|
|
wrapper.unmount()
|
|
})
|
|
it('retains the existing file after replacement upload fails and rejects invalid file sizes', async () => {
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
vi.mocked(uploadDeliveryDocument).mockRejectedValueOnce(new Error('上传失败'))
|
|
await choose(wrapper.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['data'], 'bad.pdf'))
|
|
expect(wrapper.text()).toContain('original.pdf')
|
|
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), new File([], 'empty.bin'))
|
|
const oversized = new File(['data'], 'large.bin'); Object.defineProperty(oversized, 'size', { value: 52428801 })
|
|
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), oversized)
|
|
expect(uploadDeliveryDocument).toHaveBeenCalledTimes(1)
|
|
const exact = new File(['data'], 'valid.bin'); Object.defineProperty(exact, 'size', { value: 52428800 })
|
|
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), exact)
|
|
expect(uploadDeliveryDocument).toHaveBeenCalledTimes(2)
|
|
wrapper.unmount()
|
|
})
|
|
it.each(['permission', 'action'])('is read-only without the required %s', async kind => {
|
|
if (kind === 'permission') can.mockReturnValue(false)
|
|
else vi.mocked(getDeliveryDocuments).mockResolvedValue({ ...response(), allowedActions: [] })
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
expect(wrapper.find('.save').exists()).toBe(false)
|
|
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
|
|
expect(wrapper.text()).toContain('original.pdf')
|
|
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
it('blocks retries after a version conflict without losing draft files', async () => {
|
|
vi.mocked(saveDeliveryDocuments).mockRejectedValue({ isAxiosError: true, response: { status: 409, data: { code: 'A0409', message: '版本已变化', requestId: 'req-1', traceId: 'trace-1' } } })
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
await wrapper.find('.save').trigger('click'); await flushPromises()
|
|
expect(wrapper.find('.save').attributes('disabled')).toBeDefined()
|
|
expect(wrapper.text()).toContain('original.pdf')
|
|
expect(wrapper.emitted('saved')).toBeUndefined()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('limits each category to twenty files while allowing replacement', async () => {
|
|
const snapshot = response()
|
|
snapshot.categories.find(item => item.category === 'BOL')!.files = Array.from({ length: 20 }, (_, index) => ({ ...originalFile, fileId: String(index + 1) }))
|
|
snapshot.categories.find(item => item.category === 'BOL')!.fileCount = 20
|
|
vi.mocked(getDeliveryDocuments).mockResolvedValue(snapshot)
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
expect(wrapper.find('[data-category="BOL"] header input[type="file"]').attributes('disabled')).toBeDefined()
|
|
await choose(wrapper.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['data'], 'replace.pdf'))
|
|
await wrapper.find('.save').trigger('click'); await flushPromises()
|
|
expect(vi.mocked(saveDeliveryDocuments).mock.calls[0]![2].documents.find(item => item.category === 'BOL')!.fileIds).toHaveLength(20)
|
|
expect(vi.mocked(saveDeliveryDocuments).mock.calls[0]![2].documents.find(item => item.category === 'BOL')!.fileIds).toContain('9007199254740996')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('ignores a late save response after switching to another customer delivery', async () => {
|
|
let finish!: (value: DeliveryDocuments) => void
|
|
vi.mocked(saveDeliveryDocuments).mockReturnValueOnce(new Promise(resolve => { finish = resolve }))
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
await wrapper.find('.save').trigger('click')
|
|
vi.mocked(getDeliveryDocuments).mockResolvedValue({ ...response(), deliveryNo: 'DL/2', customerId: '200', bolNo: 'BOL-2' })
|
|
await wrapper.setProps({ deliveryNo: 'DL/2', customerId: '200' }); await flushPromises()
|
|
finish(response()); await flushPromises()
|
|
expect(wrapper.emitted('saved')).toBeUndefined()
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
expect(wrapper.find('.save').exists()).toBe(true)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('ignores a late upload failure after closing and reopening on another delivery', async () => {
|
|
let fail!: (reason: Error) => void
|
|
vi.mocked(uploadDeliveryDocument).mockReturnValueOnce(new Promise((_resolve, reject) => { fail = reject }))
|
|
const wrapper = mountDialog(); await flushPromises()
|
|
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), new File(['data'], 'pending.pdf'))
|
|
await wrapper.setProps({ modelValue: false }); await flushPromises()
|
|
await wrapper.setProps({ modelValue: true, deliveryNo: 'DL/2', customerId: '200' }); await flushPromises()
|
|
fail(new Error('old upload failed')); await flushPromises()
|
|
expect(wrapper.text()).not.toContain('old upload failed')
|
|
expect(wrapper.findAll('.document-file')).toHaveLength(1)
|
|
expect(wrapper.find('[data-category="RECEIPT"] input[type="file"]').attributes('disabled')).toBeUndefined()
|
|
wrapper.unmount()
|
|
})
|
|
})
|