335 lines
22 KiB
TypeScript
335 lines
22 KiB
TypeScript
import { createRequire } from 'node:module'
|
|
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ElMessageBox } from 'element-plus'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
|
import * as api from '../api/finance'
|
|
import type { FinanceFeeSourceDetail, FinanceReconciliationDetail, FinanceStatementReadModel } from '../types'
|
|
import FinanceCustomerArapView from './FinanceCustomerArapView.vue'
|
|
import FinanceCustomerStatementsView from './FinanceCustomerStatementsView.vue'
|
|
|
|
const { default: ElementPlus, ElDialog } = createRequire(import.meta.url)('element-plus') as typeof import('element-plus')
|
|
const can = vi.hoisted(() => vi.fn((_permission: string) => true))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
|
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
|
|
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
|
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
|
|
vi.mock('../api/finance', () => ({
|
|
listFinanceFeeSources: vi.fn(), getFinanceFeeSource: vi.fn(), reviewFinanceFeeSource: vi.fn(),
|
|
editFinanceFeeSource: vi.fn(), adjustFinanceFeeSource: vi.fn(), submitFinanceFeeSource: vi.fn(), voidFinanceFeeSource: vi.fn(),
|
|
listFinanceStatements: vi.fn(), getReconciliationStatement: vi.fn(), createReconciliationStatement: vi.fn(), actionReconciliationStatement: vi.fn(),
|
|
}))
|
|
const customer: PlatformTenantResource = {
|
|
id: '9223372036854775001', tenantCode: 'C-TEST', tenantName: '测试客户', status: 'ACTIVE', legalName: null, legalRepresentative: null,
|
|
unifiedSocialCreditCode: null, contactName: null, contactPhone: null, contactEmail: null, servicePackage: null, contactWechat: null, salesOwnerName: null,
|
|
paymentTermDays: null, enterpriseType: null, establishedDate: null, registeredCapital: null, registeredCapitalCurrency: null, registeredAddress: null,
|
|
internalRemark: null, createdAt: null, approvedAt: null,
|
|
}
|
|
const fee: FinanceFeeSourceDetail = {
|
|
poolItemId: '9223372036854775002', feeSourceId: '9223372036854775003', direction: 'RECEIVABLE', sourceType: 'MANUAL', sourceId: '9223372036854775004',
|
|
sourceFeeId: '9223372036854775004', sourceVersion: 3, sourceSystem: 'FINANCE', sourceBusinessId: 'IN-TEST-1',
|
|
customerId: String(customer.id), customerName: customer.tenantName, supplierId: null, supplierName: '', orderId: null,
|
|
inboundId: '9223372036854775005', inboundCode: 'IN-TEST-1', containerId: null, containerNo: '', businessDate: '2026-10-01', feeItemName: '运费',
|
|
quantity: 2, unitPrice: 50, amount: 100, currency: 'CNY', reviewStatus: 'DRAFT', sourceStatus: 'AVAILABLE', status: 'AVAILABLE',
|
|
statementGenerated: false, operationVersion: 9, createdAt: '2026-10-01', operationLogs: [],
|
|
}
|
|
const statement: FinanceStatementReadModel = {
|
|
id: '9223372036854775010', customerId: String(customer.id), billNo: 'REC202610200001', customerName: customer.tenantName,
|
|
billDate: '2026-10-20', dueDate: '', periodStart: '2026-10-01', periodEnd: '2026-10-20', creditDays: 30, lineCount: 1, sourceCount: 1,
|
|
totalAmount: 100, receivedAmount: 0, outstandingAmount: 100, currency: 'CNY', status: 'ISSUED', version: 3,
|
|
}
|
|
const detail: FinanceReconciliationDetail = {
|
|
...statement, paidAmount: 0, periodStart: '2026-10-01', periodEnd: '2026-10-20', creditDays: 30, settled: false, remark: '', version: 8,
|
|
lines: [{ statementLineId: '9223372036854775011', sourceFeeId: fee.sourceFeeId, sourceFeeVersion: 3, sourceHash: 'test-hash', direction: 'RECEIVABLE',
|
|
sourceType: 'MANUAL', sourceNo: 'IN-TEST-1', orderId: null, inboundId: fee.inboundId, inboundCode: 'IN-TEST-1', containerId: null, containerNo: '',
|
|
businessDate: '2026-10-01', quantity: 2, unitPrice: 50, feeItemName: '运费', sourceAmount: 100, amount: 100 }],
|
|
paymentHistory: [], revisions: [],
|
|
}
|
|
const failure = (message = '测试拒绝操作', code = 'TEST_REJECTED') => new ApiResponseError(message, code, 409, null, 'finance-dialog-test')
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void
|
|
let reject!: (reason: unknown) => void
|
|
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no })
|
|
return { promise, resolve, reject }
|
|
}
|
|
function setFee(overrides: Partial<FinanceFeeSourceDetail> = {}) {
|
|
const current = { ...fee, ...overrides }
|
|
vi.mocked(api.listFinanceFeeSources).mockResolvedValue({ records: [current], total: 1, pages: 1 })
|
|
vi.mocked(api.getFinanceFeeSource).mockResolvedValue(current)
|
|
}
|
|
async function mountPage(kind: 'arap' | 'statements') {
|
|
const router = createRouter({ history: createMemoryHistory(), routes: [
|
|
{ path: '/finance/customer-arap', name: 'finance-customer-arap', component: FinanceCustomerArapView },
|
|
{ path: '/finance/customer-arap/:feeSourceId', name: 'finance-customer-arap-detail', component: FinanceCustomerArapView },
|
|
{ path: '/finance/customer-statements', name: 'finance-customer-statements', component: FinanceCustomerStatementsView },
|
|
{ path: '/finance/customer-statements/:statementId', name: 'finance-customer-statement-detail', component: FinanceCustomerStatementsView },
|
|
] })
|
|
await router.push(kind === 'arap' ? '/finance/customer-arap' : '/finance/customer-statements')
|
|
await router.isReady()
|
|
const wrapper = mount(kind === 'arap' ? FinanceCustomerArapView : FinanceCustomerStatementsView, { attachTo: document.body, global: { plugins: [ElementPlus, router] } })
|
|
await flushPromises()
|
|
return { wrapper, router }
|
|
}
|
|
function activeDialog(wrapper: VueWrapper) {
|
|
const dialog = wrapper.findAllComponents(ElDialog).find(item => item.props('modelValue'))
|
|
expect(dialog).toBeDefined()
|
|
return dialog!
|
|
}
|
|
async function click(wrapper: VueWrapper, text: string, selector = 'button') {
|
|
const button = wrapper.findAll(selector).find(item => item.text().trim() === text)
|
|
expect(button, text).toBeDefined()
|
|
await button!.trigger('click')
|
|
await flushPromises()
|
|
}
|
|
async function rowClick(wrapper: VueWrapper, label: string) { await click(wrapper, label, '.el-table__body-wrapper button') }
|
|
async function openCreate() {
|
|
const { wrapper, router } = await mountPage('statements')
|
|
await click(wrapper, '生成对账单')
|
|
return { wrapper, router, dialog: activeDialog(wrapper) }
|
|
}
|
|
async function fillCreation(dialog: VueWrapper, selected = false) {
|
|
dialog.getComponent({ name: 'ElRadioGroup' }).vm.$emit('update:modelValue', selected ? 'SELECTED' : 'BY_CUTOFF')
|
|
dialog.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', String(customer.id))
|
|
const dates = dialog.findAllComponents({ name: 'ElDatePicker' })
|
|
dates[0]!.vm.$emit('update:modelValue', '2026-10-01')
|
|
dates[1]!.vm.$emit('update:modelValue', '2026-10-20')
|
|
await flushPromises()
|
|
await dialog.get('textarea').setValue('测试备注')
|
|
}
|
|
async function openAdjustment() {
|
|
const { wrapper, router } = await mountPage('statements')
|
|
await rowClick(wrapper, '调整')
|
|
return { wrapper, router, dialog: activeDialog(wrapper) }
|
|
}
|
|
async function fillAdjustment(dialog: VueWrapper) {
|
|
await dialog.get('textarea[placeholder="请输入整体调整原因"]').setValue(' 整体调整 ')
|
|
await dialog.get('.el-table__body-wrapper input[type="checkbox"]').setValue(true)
|
|
dialog.getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 88.12)
|
|
await dialog.get('.el-table__body-wrapper input[placeholder="请输入明细说明"]').setValue(' 行说明 ')
|
|
}
|
|
enableAutoUnmount(afterEach)
|
|
beforeEach(() => {
|
|
vi.resetAllMocks()
|
|
can.mockReturnValue(true)
|
|
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [customer], page: 1, size: 200, total: 1, pages: 1 })
|
|
setFee()
|
|
for (const method of [api.reviewFinanceFeeSource, api.editFinanceFeeSource, api.adjustFinanceFeeSource, api.submitFinanceFeeSource, api.voidFinanceFeeSource]) vi.mocked(method).mockResolvedValue(fee)
|
|
vi.mocked(api.listFinanceStatements).mockResolvedValue([statement])
|
|
vi.mocked(api.getReconciliationStatement).mockResolvedValue(detail)
|
|
vi.mocked(api.createReconciliationStatement).mockResolvedValue({ ...detail, id: '9223372036854775012', billNo: 'REC202610200002', status: 'DRAFT' })
|
|
vi.mocked(api.actionReconciliationStatement).mockResolvedValue(detail)
|
|
})
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.innerHTML = '' })
|
|
|
|
describe('fee source and statement native dialogs', () => {
|
|
it.each(['APPROVE', 'REJECT'] as const)('reviews %s with plain fee facts and the loaded operation version', async action => {
|
|
setFee({ reviewStatus: 'PENDING' })
|
|
const { wrapper } = await mountPage('arap')
|
|
await rowClick(wrapper, '审核')
|
|
const dialog = activeDialog(wrapper)
|
|
expect(dialog.props('title')).toContain('应收费用 · 运费')
|
|
expect(dialog.get('[data-tone="source"]').text()).toContain('IN-TEST-1')
|
|
expect(dialog.find('[data-tone="source"] input').exists()).toBe(false)
|
|
expect(dialog.text()).not.toContain(fee.feeSourceId)
|
|
dialog.getComponent({ name: 'ElSegmented' }).vm.$emit('update:modelValue', action)
|
|
await dialog.get('textarea').setValue(' 审核说明 ')
|
|
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
|
|
expect(dialog.props('fullscreen')).toBe(true)
|
|
await click(dialog, '提交审核')
|
|
expect(api.reviewFinanceFeeSource).toHaveBeenCalledWith(fee.feeSourceId, { action, reason: '审核说明', expectedVersion: 9 })
|
|
})
|
|
it('retains required rejection reasons', async () => {
|
|
setFee({ reviewStatus: 'PENDING' })
|
|
const { wrapper } = await mountPage('arap')
|
|
await rowClick(wrapper, '审核')
|
|
const dialog = activeDialog(wrapper)
|
|
dialog.getComponent({ name: 'ElSegmented' }).vm.$emit('update:modelValue', 'REJECT')
|
|
await flushPromises()
|
|
await click(dialog, '提交审核')
|
|
expect(api.reviewFinanceFeeSource).not.toHaveBeenCalled()
|
|
expect(dialog.get('textarea').attributes('placeholder')).toBe('请填写驳回原因')
|
|
})
|
|
it.each([
|
|
['编辑', 'DRAFT', 'editFinanceFeeSource'], ['调整', 'APPROVED', 'adjustFinanceFeeSource'],
|
|
['提交审核', 'DRAFT', 'submitFinanceFeeSource'], ['作废', 'APPROVED', 'voidFinanceFeeSource'],
|
|
] as const)('keeps %s command, reason, amount applicability and version', async (label, status, method) => {
|
|
setFee({ reviewStatus: status })
|
|
const { wrapper } = await mountPage('arap')
|
|
await rowClick(wrapper, label)
|
|
const dialog = activeDialog(wrapper)
|
|
const hasAmount = label === '编辑' || label === '调整'
|
|
expect(dialog.findComponent({ name: 'ElInputNumber' }).exists()).toBe(hasAmount)
|
|
await dialog.get('textarea').setValue(' 操作说明 ')
|
|
if (hasAmount) dialog.getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 123.45)
|
|
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
|
|
await click(dialog, label === '作废' ? '确认作废' : '确认')
|
|
expect(api[method]).toHaveBeenCalledWith(fee.feeSourceId, { reason: '操作说明', expectedVersion: 9, ...(hasAmount ? { amount: 123.45 } : {}) })
|
|
})
|
|
it('keeps operation reasons mandatory before editing', async () => {
|
|
const { wrapper } = await mountPage('arap')
|
|
await rowClick(wrapper, '编辑')
|
|
await click(activeDialog(wrapper), '确认')
|
|
expect(api.editFinanceFeeSource).not.toHaveBeenCalled()
|
|
})
|
|
it.each(['review', 'edit'] as const)('locks %s forms and keeps failed drafts and diagnostics', async mode => {
|
|
const pending = deferred<FinanceFeeSourceDetail>()
|
|
if (mode === 'review') { setFee({ reviewStatus: 'PENDING' }); vi.mocked(api.reviewFinanceFeeSource).mockReturnValue(pending.promise) }
|
|
else vi.mocked(api.editFinanceFeeSource).mockReturnValue(pending.promise)
|
|
const { wrapper } = await mountPage('arap')
|
|
await rowClick(wrapper, mode === 'review' ? '审核' : '编辑')
|
|
const dialog = activeDialog(wrapper)
|
|
await dialog.get('textarea').setValue('保留操作说明')
|
|
await click(dialog, mode === 'review' ? '提交审核' : '确认')
|
|
expect(dialog.props()).toMatchObject({ showClose: false, closeOnPressEscape: false })
|
|
expect(dialog.get('textarea').attributes('disabled')).toBeDefined()
|
|
pending.reject(failure())
|
|
await flushPromises()
|
|
expect(wrapper.getComponent(OperationErrorToast).props('details')).toMatchObject({ requestId: 'finance-dialog-test' })
|
|
expect((dialog.get('textarea').element as HTMLTextAreaElement).value).toBe('保留操作说明')
|
|
expect(dialog.props('modelValue')).toBe(true)
|
|
})
|
|
it('shows the current mutation failure after a previous review failed', async () => {
|
|
setFee({ reviewStatus: 'PENDING' })
|
|
vi.mocked(api.reviewFinanceFeeSource).mockRejectedValueOnce(failure('审核失败'))
|
|
vi.mocked(api.voidFinanceFeeSource).mockRejectedValueOnce(failure('作废失败'))
|
|
const { wrapper } = await mountPage('arap')
|
|
await rowClick(wrapper, '审核')
|
|
await click(activeDialog(wrapper), '提交审核')
|
|
await click(activeDialog(wrapper), '取消')
|
|
await rowClick(wrapper, '作废')
|
|
await activeDialog(wrapper).get('textarea').setValue('测试作废')
|
|
await click(activeDialog(wrapper), '确认作废')
|
|
expect(wrapper.getComponent(OperationErrorToast).props('details')).toMatchObject({ message: '作废失败' })
|
|
})
|
|
it.each(['FIRSTLEG', 'LASTLEG'])('keeps %s upstream adjustment and void actions unavailable', async system => {
|
|
setFee({ sourceSystem: system, reviewStatus: 'APPROVED' })
|
|
const { wrapper } = await mountPage('arap')
|
|
expect(wrapper.get('.el-table__body-wrapper .density-actions').findAll('button').map(button => button.text())).toEqual(['详情'])
|
|
})
|
|
|
|
it('creates a cutoff statement with the original customer, CNY and period payload', async () => {
|
|
const { dialog, router } = await openCreate()
|
|
expect(dialog.props('width')).toContain('680px')
|
|
await fillCreation(dialog)
|
|
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
|
|
await click(dialog, '确定')
|
|
expect(api.createReconciliationStatement).toHaveBeenCalledWith({ customerId: String(customer.id), currency: 'CNY', generationMode: 'BY_CUTOFF', feeSourceIds: undefined, periodStart: '2026-10-01', periodEnd: '2026-10-20', remark: '测试备注' })
|
|
expect(router.currentRoute.value.name).toBe('finance-customer-statement-detail')
|
|
})
|
|
it('keeps selected-source filtering, summary and complete long IDs through fullscreen', async () => {
|
|
setFee({ reviewStatus: 'APPROVED' })
|
|
const { dialog } = await openCreate()
|
|
await fillCreation(dialog, true)
|
|
expect(api.listFinanceFeeSources).toHaveBeenLastCalledWith(expect.objectContaining({ customerId: String(customer.id), direction: 'RECEIVABLE', status: 'AVAILABLE', reviewStatus: 'APPROVED', statementGenerated: false }))
|
|
const select = dialog.findAllComponents({ name: 'ElSelect' }).find(item => item.props('multiple'))!
|
|
select.vm.$emit('update:modelValue', [fee.feeSourceId])
|
|
await flushPromises()
|
|
expect(dialog.get('.selected-source-summary').text()).toContain('已选 1 笔,合计 100.00 CNY')
|
|
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
|
|
await click(dialog, '确定')
|
|
expect(api.createReconciliationStatement).toHaveBeenCalledWith(expect.objectContaining({ generationMode: 'SELECTED', feeSourceIds: [fee.feeSourceId] }))
|
|
})
|
|
it('preserves customer and period validation before creating', async () => {
|
|
const { dialog } = await openCreate()
|
|
await click(dialog, '确定')
|
|
expect(api.createReconciliationStatement).not.toHaveBeenCalled()
|
|
await fillCreation(dialog)
|
|
dialog.findAllComponents({ name: 'ElDatePicker' })[0]!.vm.$emit('update:modelValue', '2026-11-01')
|
|
await flushPromises()
|
|
await click(dialog, '确定')
|
|
expect(api.createReconciliationStatement).not.toHaveBeenCalled()
|
|
})
|
|
it('keeps selected-source mode disabled without its independent read permission', async () => {
|
|
can.mockImplementation(permission => permission !== 'platform:finance:customer-arap:list')
|
|
const { dialog } = await openCreate()
|
|
expect(dialog.findAllComponents({ name: 'ElRadioButton' })[1]!.props('disabled')).toBe(true)
|
|
})
|
|
it('keeps eligible-source loading errors visible and retries the same selection context', async () => {
|
|
vi.mocked(api.listFinanceFeeSources).mockRejectedValueOnce(failure('费用加载失败'))
|
|
const { dialog } = await openCreate()
|
|
await fillCreation(dialog, true)
|
|
expect(dialog.get('.el-alert').text()).toContain('可生成费用加载失败')
|
|
await click(dialog, '重试')
|
|
expect(dialog.find('.el-alert').exists()).toBe(false)
|
|
expect(api.listFinanceFeeSources).toHaveBeenLastCalledWith(expect.objectContaining({ customerId: String(customer.id), businessDateFrom: '2026-10-01', businessDateTo: '2026-10-20' }))
|
|
})
|
|
it('locks creation then retains the draft and no-eligible-source feedback after failure', async () => {
|
|
const pending = deferred<FinanceReconciliationDetail>()
|
|
vi.mocked(api.createReconciliationStatement).mockReturnValue(pending.promise)
|
|
const { dialog } = await openCreate()
|
|
await fillCreation(dialog)
|
|
await click(dialog, '确定')
|
|
expect(dialog.props()).toMatchObject({ showClose: false, closeOnPressEscape: false })
|
|
expect(dialog.get('textarea').attributes('disabled')).toBeDefined()
|
|
pending.reject(failure('没有可生成的费用', 'FIN_409'))
|
|
await flushPromises()
|
|
expect(dialog.get('.el-alert').text()).toContain('所选账期内没有可生成的应收费用')
|
|
expect((dialog.get('textarea').element as HTMLTextAreaElement).value).toBe('测试备注')
|
|
expect(dialog.props('modelValue')).toBe(true)
|
|
})
|
|
it('keeps all adjustment rows in the main dialog flow with plain statement facts', async () => {
|
|
vi.mocked(api.getReconciliationStatement).mockResolvedValue({ ...detail, lines: Array.from({ length: 13 }, (_, index) => ({ ...detail.lines[0]!, statementLineId: 'line-' + index, feeItemName: '费用' + (index + 1) })) })
|
|
const { dialog } = await openAdjustment()
|
|
expect(dialog.props('width')).toContain('900px')
|
|
expect(dialog.props('title')).toContain(statement.billNo)
|
|
expect(dialog.find('[data-tone="summary"] input').exists()).toBe(false)
|
|
expect(dialog.getComponent({ name: 'ElTable' }).props('maxHeight')).toBeUndefined()
|
|
expect(dialog.findAll('.el-table__body-wrapper tr')).toHaveLength(13)
|
|
expect(dialog.get('.el-table__body-wrapper').text()).toContain('费用13')
|
|
})
|
|
it('saves only checked lines with full IDs and the freshly loaded version', async () => {
|
|
const { dialog } = await openAdjustment()
|
|
await fillAdjustment(dialog)
|
|
await dialog.get('button[aria-label="展开全屏"]').trigger('click')
|
|
await click(dialog, '确定')
|
|
expect(api.actionReconciliationStatement).toHaveBeenCalledWith(statement.id, { action: 'ADJUST', reason: '整体调整', expectedVersion: 8, adjustmentLines: [{ statementLineId: '9223372036854775011', adjustedAmount: 88.12, reason: '行说明' }] })
|
|
})
|
|
it('requires an overall reason, selection and each selected line reason', async () => {
|
|
const { dialog } = await openAdjustment()
|
|
await click(dialog, '确定')
|
|
expect(api.actionReconciliationStatement).not.toHaveBeenCalled()
|
|
await dialog.get('textarea').setValue('整体调整')
|
|
await click(dialog, '确定')
|
|
expect(api.actionReconciliationStatement).not.toHaveBeenCalled()
|
|
await dialog.get('.el-table__body-wrapper input[type="checkbox"]').setValue(true)
|
|
await click(dialog, '确定')
|
|
expect(api.actionReconciliationStatement).not.toHaveBeenCalled()
|
|
})
|
|
it('locks adjustment rows and retains their state with visible failure diagnostics', async () => {
|
|
const pending = deferred<FinanceReconciliationDetail>()
|
|
vi.mocked(api.actionReconciliationStatement).mockReturnValue(pending.promise)
|
|
const { dialog } = await openAdjustment()
|
|
await fillAdjustment(dialog)
|
|
await click(dialog, '确定')
|
|
expect(dialog.props('showClose')).toBe(false)
|
|
expect(dialog.get('.el-table__body-wrapper input[type="checkbox"]').attributes('disabled')).toBeDefined()
|
|
pending.reject(failure())
|
|
await flushPromises()
|
|
expect(dialog.getComponent(OperationErrorToast).props('details')).toMatchObject({ message: '测试拒绝操作', requestId: 'finance-dialog-test' })
|
|
expect((dialog.get('.el-table__body-wrapper input[type="checkbox"]').element as HTMLInputElement).checked).toBe(true)
|
|
expect((dialog.get('.el-table__body-wrapper input[placeholder="请输入明细说明"]').element as HTMLInputElement).value).toBe(' 行说明 ')
|
|
expect(dialog.props('modelValue')).toBe(true)
|
|
})
|
|
it.each([
|
|
['驳回', 'PENDING_REVIEW', 'REJECT'], ['作废', 'DRAFT', 'VOID'], ['重开', 'REJECTED', 'REOPEN'],
|
|
] as const)('identifies %s reason prompts without changing the state command', async (label, status, action) => {
|
|
vi.mocked(api.listFinanceStatements).mockResolvedValue([{ ...statement, status }])
|
|
const prompt = vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue(Object.assign('confirm', { value: '测试原因', action: 'confirm' as const }))
|
|
const { wrapper } = await mountPage('statements')
|
|
await rowClick(wrapper, label)
|
|
expect(prompt).toHaveBeenCalledWith('请输入操作原因', expect.stringContaining(statement.billNo), expect.objectContaining({ customClass: 'admin-density-confirm', inputPattern: /\S+/ }))
|
|
expect(api.actionReconciliationStatement).toHaveBeenCalledWith(statement.id, { action, reason: '测试原因', expectedVersion: 3 })
|
|
})
|
|
it('does not change statement state when its reason prompt is cancelled', async () => {
|
|
vi.mocked(api.listFinanceStatements).mockResolvedValue([{ ...statement, status: 'REJECTED' }])
|
|
vi.spyOn(ElMessageBox, 'prompt').mockRejectedValue('cancel')
|
|
const { wrapper } = await mountPage('statements')
|
|
await rowClick(wrapper, '重开')
|
|
expect(api.actionReconciliationStatement).not.toHaveBeenCalled()
|
|
})
|
|
})
|