This commit is contained in:
233
scripts/tests/employeeCollectionActing.test.mjs
Normal file
233
scripts/tests/employeeCollectionActing.test.mjs
Normal file
@@ -0,0 +1,233 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
import { compileScript, parse } from '@vue/compiler-sfc'
|
||||
|
||||
function evaluate(source, imports = {}) {
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||||
}).outputText
|
||||
const module = { exports: {} }
|
||||
new Function('require', 'module', 'exports', compiled)(
|
||||
(name) => {
|
||||
assert.ok(imports[name], `Unexpected dependency: ${name}`)
|
||||
return imports[name]
|
||||
},
|
||||
module,
|
||||
module.exports
|
||||
)
|
||||
return module.exports
|
||||
}
|
||||
|
||||
const acting = evaluate(readFileSync('src/utils/business/employeeCollectionActing.ts', 'utf8'))
|
||||
const { descriptor } = parse(
|
||||
readFileSync(
|
||||
'src/views/finance/employee-collection/applications/components/ApplicationFormDialog.vue',
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
const componentSource = compileScript(descriptor, { id: 'collection-acting-test' }).content
|
||||
|
||||
const employee = { id: 10, name: '测试员工', user_type: 1 }
|
||||
const bill = (id, name = employee.name, accountId = 99) => ({
|
||||
id,
|
||||
remaining_amount: 10000,
|
||||
status: 0,
|
||||
debtor_account_id: accountId,
|
||||
debtor_snapshot: { account_id: accountId, account_name: name }
|
||||
})
|
||||
|
||||
function formHarness(props = {}, candidates = [], details = {}) {
|
||||
const calls = []
|
||||
const detailCalls = []
|
||||
const cleared = []
|
||||
const user = vue.reactive({ info: { ...employee }, isSuperAdmin: true })
|
||||
const service = {
|
||||
getPaymentMethods: async () => ({
|
||||
code: 0,
|
||||
data: [{ id: 1, name: '银行转账', enabled: true }]
|
||||
}),
|
||||
getBills: async () => ({ code: 0, data: { items: candidates, total: candidates.length } }),
|
||||
getApplicationById: async () => ({
|
||||
code: 0,
|
||||
data: {
|
||||
allocations: Object.keys(details).map((id) => ({ bill_id: Number(id), amount: 5000 }))
|
||||
}
|
||||
}),
|
||||
getBillById: async (id) => {
|
||||
detailCalls.push(id)
|
||||
return { code: 0, data: { bill: details[id] } }
|
||||
},
|
||||
createApplication: async (data) => {
|
||||
calls.push({ method: 'create', data })
|
||||
return { code: 0 }
|
||||
},
|
||||
updateApplication: async (id, data) => {
|
||||
calls.push({ method: 'update', id, data })
|
||||
return { code: 0 }
|
||||
}
|
||||
}
|
||||
const display = {
|
||||
BILL_SOURCE_OPTIONS: [],
|
||||
BILL_STATUS_OPTIONS: [],
|
||||
canCreateApplication: () => true,
|
||||
formatCollectionCurrency: String,
|
||||
normalizeCollectionList: (data) => (Array.isArray(data) ? data : data.items || []),
|
||||
normalizeCollectionPage: (data) => ({ list: data.items || [], total: data.total || 0 })
|
||||
}
|
||||
const component = evaluate(componentSource, {
|
||||
vue,
|
||||
'element-plus': { ElMessage: { success() {}, error() {}, warning() {} } },
|
||||
'@/api/modules': {
|
||||
EmployeeCollectionService: service,
|
||||
AgentRechargeService: {},
|
||||
ShopService: {}
|
||||
},
|
||||
'@/utils/business/format': {
|
||||
fenToYuan: (value) => Number(value || 0) / 100,
|
||||
yuanToFen: (value) => Math.round(Number(value || 0) * 100)
|
||||
},
|
||||
'@/utils/business': {
|
||||
getErrorMessage: String,
|
||||
toVoucherKeyList: (value) => (Array.isArray(value) ? value : value ? [value] : [])
|
||||
},
|
||||
'@/store/modules/user': { useUserStore: () => user },
|
||||
'@/utils/business/employeeCollectionActing': acting,
|
||||
'@/components/business/VoucherUpload.vue': { default: {} },
|
||||
'../../employeeCollectionDisplay': display
|
||||
}).default
|
||||
const reactiveProps = vue.reactive({
|
||||
modelValue: true,
|
||||
application: null,
|
||||
presetBill: null,
|
||||
...props
|
||||
})
|
||||
const state = component.setup(reactiveProps, { expose() {}, emit() {} })
|
||||
state.formRef.value = {
|
||||
clearValidate: (field) => cleared.push(field),
|
||||
validate: async () => {
|
||||
if (state.rules.value.acting_reason?.[0]?.required && !state.form.acting_reason.trim()) {
|
||||
throw new Error('请填写代办原因')
|
||||
}
|
||||
}
|
||||
}
|
||||
const fillPayment = () =>
|
||||
Object.assign(state.form, {
|
||||
payment_method_id: 1,
|
||||
paid_amount: 100,
|
||||
paid_at: '2026-10-14T15:00:00+08:00',
|
||||
payer_name: '付款方',
|
||||
external_transaction_no: 'LOCAL_TEST',
|
||||
payment_voucher_keys: ['test-voucher']
|
||||
})
|
||||
return { state, user, calls, cleared, detailCalls, fillPayment }
|
||||
}
|
||||
|
||||
test('matching nonempty me.name makes a bill self-handled even when account IDs differ', () => {
|
||||
assert.equal(acting.isEmployeeCollectionBillSelf(bill(1), employee), true)
|
||||
assert.equal(acting.isEmployeeCollectionBillSelf(bill(1, ' 测试员工 '), employee), true)
|
||||
assert.equal(acting.isEmployeeCollectionBillSelf(bill(1, '其他员工'), employee), false)
|
||||
assert.equal(acting.isEmployeeCollectionBillSelf(bill(1, '', 10), employee), true)
|
||||
assert.equal(
|
||||
acting.isEmployeeCollectionBillSelf(bill(1, '', null), { name: '', user_type: 1 }),
|
||||
false
|
||||
)
|
||||
assert.equal(acting.requiresEmployeeCollectionActingReason([bill(1), bill(2)], employee), false)
|
||||
assert.equal(
|
||||
acting.requiresEmployeeCollectionActingReason([bill(1), bill(2, '其他员工')], employee),
|
||||
true
|
||||
)
|
||||
assert.equal(acting.requiresEmployeeCollectionActingReason([undefined], employee), true)
|
||||
assert.equal(acting.requiresEmployeeCollectionActingReason([], employee), true)
|
||||
assert.equal(
|
||||
acting.requiresEmployeeCollectionActingReason([bill(2, '其他员工')], {
|
||||
...employee,
|
||||
user_type: 2
|
||||
}),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
test('creating a self-handled application has no acting reason rule and omits the field entirely', async () => {
|
||||
const { state, calls, fillPayment } = formHarness({ presetBill: bill(1) })
|
||||
await state.initialize()
|
||||
await vue.nextTick()
|
||||
fillPayment()
|
||||
assert.equal(state.isActing.value, false)
|
||||
assert.equal(state.rules.value.acting_reason, undefined)
|
||||
state.form.acting_reason = '不得发送的残留值'
|
||||
await state.handleSubmit()
|
||||
assert.equal(calls[0].method, 'create')
|
||||
assert.equal(Object.hasOwn(calls[0].data, 'acting_reason'), false)
|
||||
assert.deepEqual(calls[0].data.allocations, [{ bill_id: 1, amount: 10000 }])
|
||||
})
|
||||
|
||||
test('acting for another employee remains required; switching back to self clears the old value and validation', async () => {
|
||||
const { state, calls, cleared, fillPayment } = formHarness()
|
||||
fillPayment()
|
||||
state.toggleBill(bill(2, '其他员工'))
|
||||
await vue.nextTick()
|
||||
assert.equal(state.rules.value.acting_reason[0].required, true)
|
||||
await assert.rejects(state.handleSubmit(), /代办原因/)
|
||||
assert.equal(calls.length, 0)
|
||||
state.form.acting_reason = '代办核销'
|
||||
state.toggleBill(bill(2, '其他员工'))
|
||||
state.toggleBill(bill(1))
|
||||
await vue.nextTick()
|
||||
assert.equal(state.isActing.value, false)
|
||||
assert.equal(state.form.acting_reason, '')
|
||||
assert.ok(cleared.includes('acting_reason'))
|
||||
await state.handleSubmit()
|
||||
assert.equal(Object.hasOwn(calls[0].data, 'acting_reason'), false)
|
||||
})
|
||||
|
||||
test('resubmission loads responsibility for bills outside the current candidate page and omits a historical acting reason', async () => {
|
||||
const application = {
|
||||
id: 8,
|
||||
acting_reason: '历史代办原因',
|
||||
paid_amount: 5000,
|
||||
payment_method_id: 1
|
||||
}
|
||||
const { state, calls, detailCalls, fillPayment } = formHarness({ application }, [], {
|
||||
21: bill(21)
|
||||
})
|
||||
await state.initialize()
|
||||
await vue.nextTick()
|
||||
fillPayment()
|
||||
assert.deepEqual(detailCalls, [21])
|
||||
assert.equal(state.displayBills.value[0].debtor_snapshot.account_name, employee.name)
|
||||
assert.equal(state.isActing.value, false)
|
||||
assert.equal(state.rules.value.acting_reason, undefined)
|
||||
assert.equal(state.form.acting_reason, '')
|
||||
await state.handleSubmit()
|
||||
assert.equal(calls[0].method, 'update')
|
||||
assert.equal(calls[0].id, 8)
|
||||
assert.equal(Object.hasOwn(calls[0].data, 'acting_reason'), false)
|
||||
assert.deepEqual(calls[0].data.allocations, [{ bill_id: 21, amount: 5000 }])
|
||||
})
|
||||
|
||||
test('resubmission reuses responsibility from the candidate page and cannot be shadowed by an incomplete allocation cache', async () => {
|
||||
const { state, detailCalls } = formHarness({ application: { id: 9 } }, [bill(1)], { 1: bill(1) })
|
||||
await state.initialize()
|
||||
await vue.nextTick()
|
||||
assert.deepEqual(detailCalls, [])
|
||||
state.selectedBillOptions.value[1] = { id: 1, remaining_amount: 10000 }
|
||||
assert.equal(state.isActing.value, false)
|
||||
})
|
||||
|
||||
test('mixed-responsibility resubmission still sends the required acting reason', async () => {
|
||||
const { state, calls, fillPayment } = formHarness({ application: { id: 10 } }, [], {
|
||||
1: bill(1),
|
||||
2: bill(2, '其他员工')
|
||||
})
|
||||
await state.initialize()
|
||||
await vue.nextTick()
|
||||
fillPayment()
|
||||
assert.equal(state.isActing.value, true)
|
||||
assert.equal(state.rules.value.acting_reason[0].required, true)
|
||||
state.form.acting_reason = ' 帮员工提交核销 '
|
||||
await state.handleSubmit()
|
||||
assert.equal(calls[0].data.acting_reason, '帮员工提交核销')
|
||||
})
|
||||
Reference in New Issue
Block a user