173 lines
13 KiB
TypeScript
173 lines
13 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import ElementPlus, { ElInputNumber } 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 { uploadProcurementFile } from '../api/procurementCommands'
|
|
vi.mock('../api/procurementWorkflow', () => ({ submitProcurementWorkflow: vi.fn() }))
|
|
vi.mock('../api/procurementCommands', () => ({ uploadProcurementFile: 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, rejectionReason: null, photosNeedUpdate: false, receipts: [], outbounds: [] })
|
|
const detail = { customerView: { order: { purchaseQuantity: 20 } }, files: [] } as unknown as PlatformProcurement
|
|
const beforeOutbound = vi.fn<() => Promise<boolean | null>>()
|
|
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(null); vi.mocked(submitProcurementWorkflow).mockResolvedValue(workflow()) })
|
|
describe('simplified procurement operations', () => {
|
|
it('allows a rejected purchase to be resubmitted without uploading photos and hides the old reason after success', async () => {
|
|
const wrapper = render()
|
|
const rejected = { ...workflow(), rejectionReason: '请核对', photosNeedUpdate: true, receipts: [
|
|
{ id: 'r-1', quantity: 2, fileIds: ['old-1'], receivedAt: '' },
|
|
{ id: 'r-2', quantity: 3, fileIds: ['old-2'], receivedAt: '' },
|
|
] }
|
|
await wrapper.setProps({ workflow: rejected }); await flushPromises()
|
|
const button = wrapper.findAll('button').find(button => button.text() === '采购完成')!
|
|
expect(button.attributes('disabled')).toBeUndefined()
|
|
expect(wrapper.text()).toContain('请核对')
|
|
await button.trigger('click'); await flushPromises()
|
|
wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises()
|
|
expect(uploadProcurementFile).not.toHaveBeenCalled()
|
|
expect(submitProcurementWorkflow).toHaveBeenCalledWith('OP-1', '1001', 'request-confirmation', { version: 4, idempotencyKey: expect.any(String) })
|
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
|
await wrapper.setProps({ workflow: { ...rejected, status: 'WAIT_CUSTOMER_CONFIRM', version: 5 } })
|
|
expect(wrapper.text()).not.toContain('客户驳回原因')
|
|
expect(wrapper.text()).not.toContain('请核对')
|
|
await wrapper.setProps({ workflow: { ...rejected, status: 'CONFIRMED', version: 6 } })
|
|
expect(wrapper.text()).not.toContain('客户驳回原因')
|
|
expect(wrapper.text()).not.toContain('请核对')
|
|
})
|
|
it('hides the previous rejection after receipt photos are updated and shows a new rejection again', async () => {
|
|
const wrapper = render()
|
|
const rejected = { ...workflow(), rejectionReason: '照片不清晰', photosNeedUpdate: true, receipts: [
|
|
{ id: 'r-1', quantity: 2, fileIds: ['old-1'], receivedAt: '' },
|
|
{ id: 'r-2', quantity: 3, fileIds: ['old-2'], receivedAt: '' },
|
|
] }
|
|
await wrapper.setProps({ workflow: rejected })
|
|
expect(wrapper.text()).toContain('照片不清晰')
|
|
|
|
await wrapper.setProps({ workflow: { ...rejected, version: 5, rejectionReason: null, photosNeedUpdate: true } })
|
|
expect(wrapper.text()).not.toContain('客户驳回原因')
|
|
expect(wrapper.text()).not.toContain('照片不清晰')
|
|
expect(wrapper.findAll('button').some(button => button.text() === '重新上传照片')).toBe(true)
|
|
|
|
await wrapper.setProps({ workflow: { ...rejected, version: 6, rejectionReason: null, photosNeedUpdate: false } })
|
|
expect(wrapper.findAll('button').some(button => button.text() === '重新上传照片')).toBe(false)
|
|
|
|
await wrapper.setProps({ workflow: { ...rejected, version: 7, rejectionReason: '请补拍完整商品' } })
|
|
expect(wrapper.text()).toContain('请补拍完整商品')
|
|
})
|
|
it('accepts the first and subsequent receipt batches using the remaining purchase quantity', async () => {
|
|
const wrapper = render()
|
|
const receive = () => wrapper.findAll('button').find(button => button.text() === '登记收货')!
|
|
await wrapper.setProps({ workflow: { ...workflow(), receivedQuantity: 0, stockQuantity: 0, availableQuantity: 0 } })
|
|
await receive().trigger('click'); await flushPromises()
|
|
const quantity = wrapper.findComponent(ElInputNumber)
|
|
const increment = quantity.find('.el-input-number__increase')
|
|
expect(increment.attributes('aria-disabled')).not.toBe('true')
|
|
await increment.trigger('mousedown', { button: 0 })
|
|
document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
|
|
expect(quantity.props('modelValue')).toBe(2)
|
|
await quantity.find('input').setValue('10')
|
|
await quantity.find('input').trigger('blur')
|
|
expect(quantity.props('modelValue')).toBe(10)
|
|
const form = wrapper.findComponent({ name: 'ElForm' }).props('model')
|
|
form.fileIds = ['photo-1']
|
|
const dialog = wrapper.findComponent(AppDialog)
|
|
dialog.vm.$emit('confirm'); await flushPromises()
|
|
expect(submitProcurementWorkflow).toHaveBeenLastCalledWith('OP-1', '1001', 'receipts', expect.objectContaining({ version: 4, quantity: 10, fileIds: ['photo-1'] }))
|
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
|
|
|
await wrapper.setProps({ workflow: { ...workflow(), version: 5, receivedQuantity: 10, stockQuantity: 10, availableQuantity: 10 } })
|
|
await receive().trigger('click'); await flushPromises()
|
|
await wrapper.findComponent(ElInputNumber).find('input').setValue('10')
|
|
await wrapper.findComponent(ElInputNumber).find('input').trigger('blur')
|
|
form.fileIds = ['photo-2']
|
|
dialog.vm.$emit('confirm'); await flushPromises()
|
|
expect(submitProcurementWorkflow).toHaveBeenLastCalledWith('OP-1', '1001', 'receipts', expect.objectContaining({ version: 5, quantity: 10, fileIds: ['photo-2'] }))
|
|
expect(wrapper.emitted('changed')).toHaveLength(2)
|
|
const calls = vi.mocked(submitProcurementWorkflow).mock.calls
|
|
expect(calls[0]![3].idempotencyKey).not.toBe(calls[1]![3].idempotencyKey)
|
|
})
|
|
it.each([
|
|
{ received: 1, input: '10', expected: 10 },
|
|
{ received: 18, input: '3', expected: 2 },
|
|
])('limits receipts by remaining purchase quantity after $received units were received', async ({ received, input, expected }) => {
|
|
const wrapper = render()
|
|
await wrapper.setProps({ workflow: { ...workflow(), receivedQuantity: received, stockQuantity: received, availableQuantity: received } })
|
|
await wrapper.findAll('button').find(button => button.text() === '登记收货')!.trigger('click'); await flushPromises()
|
|
const quantity = wrapper.findComponent(ElInputNumber)
|
|
await quantity.find('input').setValue(input)
|
|
await quantity.find('input').trigger('blur')
|
|
expect(quantity.props('modelValue')).toBe(expected)
|
|
})
|
|
it('allows outbound when the fee check reports missing or unreviewed details', async () => {
|
|
const wrapper = render('CONFIRMED')
|
|
const button = wrapper.findAll('button').find(button => button.text() === '创建出库单')!
|
|
beforeOutbound.mockResolvedValueOnce(true)
|
|
await button.trigger('click'); await flushPromises()
|
|
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true)
|
|
await button.trigger('click'); await flushPromises()
|
|
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true)
|
|
expect(beforeOutbound).toHaveBeenCalledTimes(2)
|
|
})
|
|
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('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'] })
|
|
})
|
|
})
|