This commit is contained in:
2026-10-27 17:39:19 +08:00
parent b5d8ccdffe
commit c148912b73
49 changed files with 1134 additions and 464 deletions
@@ -0,0 +1,99 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import ElementPlus from 'element-plus'
import AppDialog from '@/shared/components/AppDialog.vue'
import { useAuthStore } from '@/core/auth/auth'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ProcurementWorkflowPanel from './ProcurementWorkflowPanel.vue'
import type { PlatformProcurement } from '../procurementTypes'
import type { ProcurementWorkflow } from '../api/procurementWorkflow'
import { submitProcurementWorkflow } from '../api/procurementWorkflow'
import { submitProcurementCommand, uploadProcurementFile } from '../api/procurementCommands'
import { listOverseasPurchaseWarehouseOptions } from '../api/platformOverseasBusiness'
vi.mock('../api/procurementWorkflow', () => ({ submitProcurementWorkflow: vi.fn() }))
vi.mock('../api/procurementCommands', () => ({ submitProcurementCommand: vi.fn(), uploadProcurementFile: vi.fn() }))
vi.mock('../api/platformOverseasBusiness', () => ({ listOverseasPurchaseWarehouseOptions: vi.fn() }))
vi.mock('@/core/unsaved-work/useUnsavedWorkRegistration', () => ({ useUnsavedWorkRegistration: () => ({ captureBaseline: vi.fn() }) }))
enableAutoUnmount(afterEach)
const workflow = (status = 'RECEIVING'): ProcurementWorkflow => ({ orderNo: 'OP-1', version: 4, status, receivedQuantity: 5, stockQuantity: 5, reservedQuantity: 0, availableQuantity: 5, receipts: [], outbounds: [] })
const detail = { customerView: { order: { purchaseQuantity: 20 } }, files: [] } as unknown as PlatformProcurement
const beforeOutbound = vi.fn<() => Promise<void>>()
function render(status = 'RECEIVING', permissions = ['receive-overseas', 'ship-customer']) {
const pinia = createPinia(); setActivePinia(pinia)
const auth = useAuthStore(); auth.token = 'test'; auth.permissions = permissions.map(value => `platform:overseas-business:purchase:${value}`); useAccessPreviewStore().initializeAfterLogin()
return mount(ProcurementWorkflowPanel, { props: { detail, workflow: workflow(status), customerId: '1001', beforeOutbound }, global: { plugins: [pinia, ElementPlus] } })
}
beforeEach(() => { vi.clearAllMocks(); beforeOutbound.mockResolvedValue(undefined); vi.mocked(submitProcurementWorkflow).mockResolvedValue(workflow()); vi.mocked(listOverseasPurchaseWarehouseOptions).mockResolvedValue([]) })
describe('simplified procurement operations', () => {
it('blocks the outbound dialog when fees are missing or not approved and permits retry after approval', async () => {
const wrapper = render('CONFIRMED')
const button = wrapper.findAll('button').find(button => button.text() === '创建出库单')!
beforeOutbound.mockRejectedValueOnce(new Error('请先添加费用明细并审核通过后再出库'))
await button.trigger('click'); await flushPromises()
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false)
expect(submitProcurementWorkflow).not.toHaveBeenCalled()
beforeOutbound.mockRejectedValueOnce(new Error('费用明细尚未全部审核通过'))
await button.trigger('click'); await flushPromises()
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false)
await button.trigger('click'); await flushPromises()
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true)
})
it('gates receipt and outbound actions with the existing operation permissions', () => {
const receipt = render('RECEIVING', [])
expect(receipt.findAll('button').find(button => button.text() === '登记收货')!.attributes('disabled')).toBeDefined()
expect(receipt.text()).toContain('当前账号未开通采购收货权限')
const outbound = render('CONFIRMED', [])
expect(outbound.findAll('button').find(button => button.text() === '创建出库单')!.attributes('disabled')).toBeDefined()
expect(outbound.text()).toContain('当前账号未开通采购出库及签收权限')
expect(render('CONFIRMED').text()).toContain('创建出库单')
})
it('requires receipt photos and uploads them before submitting the current batch quantity', async () => {
const wrapper = render(); await wrapper.findAll('button').find(button => button.text() === '登记收货')!.trigger('click')
const dialog = wrapper.findComponent(AppDialog)
dialog.vm.$emit('confirm'); await flushPromises(); expect(submitProcurementWorkflow).not.toHaveBeenCalled()
vi.mocked(uploadProcurementFile).mockResolvedValue({ id: '1', fileId: '9007199254740993', category: 'RECEIPT', fileName: 'receipt.png', contentType: 'image/png', sizeBytes: 10, createdAt: '' })
const input = document.querySelector('input[type="file"]') || wrapper.find('input[type="file"]').element
Object.defineProperty(input, 'files', { configurable: true, value: [new File(['photo'], 'receipt.png', { type: 'image/png' })] })
input.dispatchEvent(new Event('change', { bubbles: true })); await flushPromises()
dialog.vm.$emit('confirm'); await flushPromises()
expect(submitProcurementWorkflow).toHaveBeenCalledWith('OP-1', '1001', 'receipts', { version: 4, idempotencyKey: expect.any(String), quantity: 1, fileIds: ['9007199254740993'] })
})
it('retains the outbound input and idempotency key after a failed submit', async () => {
vi.mocked(submitProcurementWorkflow).mockRejectedValue(new Error('暂时失败'))
const wrapper = render('CONFIRMED'); await wrapper.findAll('button').find(button => button.text() === '创建出库单')!.trigger('click')
await flushPromises()
const dialog = wrapper.findComponent(AppDialog), form = wrapper.findComponent({ name: 'ElForm' }).props('model')
form.trackingNo = 'TRACK-1'; form.quantity = 3
dialog.vm.$emit('confirm'); await flushPromises(); dialog.vm.$emit('confirm'); await flushPromises()
expect(submitProcurementWorkflow).toHaveBeenCalledTimes(2)
expect(vi.mocked(submitProcurementWorkflow).mock.calls[0]![3]).toEqual(vi.mocked(submitProcurementWorkflow).mock.calls[1]![3])
expect(form.trackingNo).toBe('TRACK-1'); expect(form.quantity).toBe(3)
form.quantity = 6; dialog.vm.$emit('confirm'); await flushPromises(); expect(submitProcurementWorkflow).toHaveBeenCalledTimes(2)
})
it('requires a rejection reason and preserves the current version and customer scope', async () => {
const wrapper = render('PENDING_REVIEW', ['review'])
await wrapper.findAll('button').find(button => button.text() === '审核')!.trigger('click'); await flushPromises()
const form = wrapper.findComponent({ name: 'ElForm' }).props('model'), dialog = wrapper.findComponent(AppDialog)
form.decision = 'REJECT'; dialog.vm.$emit('confirm'); await flushPromises(); expect(submitProcurementCommand).not.toHaveBeenCalled()
form.reason = '商品无法采购'; dialog.vm.$emit('confirm'); await flushPromises()
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-1', '1001', 'review', expect.objectContaining({ version: 4, decision: 'REJECT', reason: '商品无法采购', purchaseWarehouseId: undefined }))
})
it('only completes the selected delivering batch after signature evidence is supplied', async () => {
const wrapper = render('DELIVERING')
await wrapper.setProps({ workflow: { ...workflow('DELIVERING'), outbounds: [{ id: '9007199254740993', outboundNo: 'OUT-1', status: 'DELIVERING', quantity: 3, trackingNo: 'TRACK-1', fileIds: [], createdAt: '', completedAt: null }] } })
await flushPromises()
await wrapper.findAll('button').find(button => button.text() === '上传签收凭证')!.trigger('click'); await flushPromises()
const form = wrapper.findComponent({ name: 'ElForm' }).props('model'), dialog = wrapper.findComponent(AppDialog)
dialog.vm.$emit('confirm'); await flushPromises(); expect(submitProcurementWorkflow).not.toHaveBeenCalled()
vi.mocked(uploadProcurementFile).mockResolvedValue({ id: '2', fileId: '9007199254740995', category: 'RECEIPT', fileName: 'signed.pdf', contentType: 'application/pdf', sizeBytes: 10, createdAt: '' })
const input = document.querySelector('input[type="file"]') || wrapper.find('input[type="file"]').element
expect(input.getAttribute('accept')).toContain('application/pdf')
Object.defineProperty(input, 'files', { configurable: true, value: [new File(['signed'], 'signed.pdf', { type: 'application/pdf' })] })
input.dispatchEvent(new Event('change', { bubbles: true })); await flushPromises()
expect(uploadProcurementFile).toHaveBeenCalledWith('OP-1', '1001', 'RECEIPT', expect.any(File))
expect(form.fileIds).toEqual(['9007199254740995'])
dialog.vm.$emit('confirm'); await flushPromises()
expect(submitProcurementWorkflow).toHaveBeenCalledWith('OP-1', '1001', 'outbounds/9007199254740993/complete', { version: 4, idempotencyKey: expect.any(String), fileIds: ['9007199254740995'] })
})
})