This commit is contained in:
2026-10-21 17:08:59 +08:00
parent c510cc4e2e
commit 67acd02ea5
6 changed files with 82 additions and 11 deletions
@@ -15,7 +15,7 @@ const dialogStub = defineComponent({ props: ['modelValue', 'showConfirm'], emits
function documents(no = 'DL-1', customerId = '101'): DeliveryDocuments {
return { deliveryNo: no, customerId, version: 1, bolNo: 'BOL-ORIGINAL', uploadedCategoryCount: 1, totalCategoryCount: 3, allowedActions: ['MANAGE_DOCUMENTS'], replayed: false, categories: deliveryDocumentCategories.map(item => ({ category: item.category, status: item.category === 'RECEIPT' ? 'UPLOADED' : 'MISSING', fileCount: item.category === 'RECEIPT' ? 1 : 0, files: item.category === 'RECEIPT' ? [{ fileId: '99', fileName: 'existing-pod.pdf', contentType: 'application/pdf', sizeBytes: 10, uploadedAt: '2026-10-21T00:00:00' }] : [] })) }
}
function setup() { return mount(DispatchDocumentsDialog, { props: { modelValue: true, dispatchNo: 'PC/1' }, global: { plugins: [ElementPlus], stubs: { AppDialog: dialogStub, DispatchVehicleDocuments: true, DispatchLoadingDocuments: true, DispatchEvidenceFiles: true } } }) }
function setup() { return mount(DispatchDocumentsDialog, { props: { modelValue: true, dispatchNo: 'PC/1' }, global: { plugins: [ElementPlus], stubs: { AppDialog: dialogStub, DispatchVehicleDocuments: true, DispatchLoadingDocuments: true, DispatchLoadingDocuments: true, DispatchEvidenceFiles: true } } }) }
beforeEach(() => {
vi.clearAllMocks(); can.mockReturnValue(true)
const value = detail(); value.stage = 'DELIVERING'
@@ -27,6 +27,14 @@ beforeEach(() => {
vi.mocked(getDeliveryDocuments).mockImplementation(async (no, customer) => documents(no, customer))
vi.mocked(saveDeliveryDocuments).mockResolvedValue({ ...documents(), version: 2 })
})
it('opens loading documents before delivery orders exist and without delivery-list permission', async () => {
vi.mocked(getDispatch).mockResolvedValue(detail({ stage: 'AFTER_LOADING' }))
can.mockImplementation((value?: string) => value !== 'platform:overseas-warehouse:delivery:list')
const wrapper = setup(); await flushPromises()
expect(wrapper.find('dispatch-loading-documents-stub').exists()).toBe(true)
expect(wrapper.findAll('[role="tab"]').find(tab => tab.text() === '装车资料')!.attributes('aria-selected')).toBe('true')
expect(getDeliveryDocuments).not.toHaveBeenCalled(); wrapper.unmount()
})
it('reads existing delivery files in one dialog and saves receipt without requiring loading or BOL files', async () => {
const wrapper = setup(); await flushPromises()
expect(getDeliveryDocuments).toHaveBeenCalledWith('DL-1', '101')
@@ -0,0 +1,63 @@
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)
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('图片读取失败')
wrapper.unmount()
})
it('keeps the original binding and file after an unknown result and never uploads again', 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 wrapper.findAll('button').find(item => item.text() === '核对原请求并继续保存')!.trigger('click'); await flushPromises()
expect(api.bindLoadingPhoto).toHaveBeenLastCalledWith('PC/1', 'LOADING_SCENE', original)
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(1)
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
})
it('refreshes a definitive version conflict while retaining the uploaded file', 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 wrapper.findAll('button').find(item => item.text() === '刷新并重试保存')!.trigger('click'); await flushPromises()
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(1); 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()
})