Files
airz-uniapp/scripts/test-zero-balances.cjs
hujun e5406c23a6 fix: 修复匹配详情提示及公积金可选路径问题
修复匹配详情中“用户自填未经核验”等来源说明,清理未参与项的橙色注意提示,修正贷款统计口径文案;补充公积金完整可选路径提取逻辑,确保明确无额外时长要求的公积金分支可正确识别并通过。
2026-10-19 16:11:59 +08:00

212 lines
14 KiB
JavaScript

// Execute the actual UTS services and page scripts in memory. No app build, network or storage.
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const assert = require('node:assert/strict');
const ts = require('D:/Develop/HBuilderX/plugins/uniapp-uts-v1/node_modules/@dcloudio/uni-uts-v1/lib/typescript/lib/typescript.js');
class Json {
constructor(values = {}) { Object.assign(this, values); }
getString(k) { return typeof this[k] === 'string' ? this[k] : null; }
getNumber(k) { return typeof this[k] === 'number' ? this[k] : null; }
getBoolean(k) { return typeof this[k] === 'boolean' ? this[k] : null; }
getArray(k) { return Array.isArray(this[k]) ? this[k] : null; }
}
const root = path.join(__dirname, '..');
const modules = {};
const calls = [];
const activePages = [];
let response = new Json({ assessmentId: '1', revisionId: '2', submissionStatus: 'accepted' });
const session = new Json({ token: 'synthetic-zero-balance-owner' });
const api = {
getSession: () => session,
requestJson: async (...args) => { calls.push(args); return response; }
};
function compile(source, filename, extras = {}) {
const exports = {};
const compiled = ts.transpileModule(source, { reportDiagnostics: true, compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } });
assert.equal((compiled.diagnostics || []).filter(d => d.category === ts.DiagnosticCategory.Error).length, 0, 'UTS transpiles: ' + filename);
vm.runInNewContext(compiled.outputText, {
exports, UTSJSONObject: Json, Error, Number, Date, Math, Promise,
require: p => p.endsWith('.uvue') ? {} : p === '@/services/api.uts' || p === './api.uts' ? api :
p === '@/services/layout.uts' ? { usePageLayout: () => ({ pageStyle: () => '', contentStyle: () => '', bottomStyle: () => '' }) } :
p === '@/services/documents.uts' ? { fetchOwnedDocuments: async () => [], releaseDocumentTemp: () => {} } :
load(p.startsWith('@/') ? p.slice(2) : path.posix.normalize(path.posix.dirname(filename) + '/' + p)),
...extras
}, { filename });
return exports;
}
function load(filename) {
if (modules[filename]) return modules[filename];
const result = compile(fs.readFileSync(path.join(root, filename), 'utf8'), filename);
modules[filename] = result;
return result;
}
const service = load('services/assessment.uts');
const records = load('packages/finance/services/records.uts');
const flow = load('services/assessment-flow.uts');
function form(overrides = {}) {
return Object.assign(service.emptyAssessmentForm(), {
name: '合成用户', idCard: '110101199001010000', phone: '19900000009', city: '广东省', maritalStatus: '未婚',
occupation: '企业职员', company: '合成单位', monthlyIncome: '1', socialSecurity: '未缴纳', housingFund: '未缴纳',
house: '无', car: '无', creditLoanCount: '0', creditCardCount: '0', loanCountScope: 'all_loans'
}, overrides);
}
function save(value) { service.setAssessmentInput('personal', 2, value, [], [], false, false); }
function snapshot(value) { return JSON.stringify(value); }
function detail(formValues) {
return new Json({ assessmentId: '1', revisionId: '2', revisionNo: 1, type: 'personal', status: 'succeeded', moneyUnit: 'yuan', form: new Json(formValues) });
}
function page(kind) {
const filename = `packages/finance/${kind}/${kind}.uvue`;
const hooks = {};
const events = { toasts: [], navigation: [] };
const exposure = kind === 'assessment' ? '{form,step,restore,next,validateStep,confirmLoanCountScope:typeof confirmLoanCountScope === "undefined" ? null : confirmLoanCountScope}' : '{payload,sections,submit,loadInput}';
const source = fs.readFileSync(path.join(root, filename), 'utf8').match(/<script setup lang="uts">([\s\S]*?)<\/script>/)[1];
const result = compile(source + '\nexports.page=' + exposure, filename, {
ref: value => ({ value }), computed: fn => ({ get value() { return fn(); } }),
onLoad: fn => { hooks.load = fn; }, onShow: fn => { hooks.show = fn; },
onHide: fn => { hooks.hide = fn; }, onUnload: fn => { hooks.unload = fn; }, onBackPress: () => {},
getCurrentPages: () => [], setInterval: () => 1, clearInterval: () => {},
uni: { showToast: value => events.toasts.push(value.title), navigateTo: value => events.navigation.push(value.url),
redirectTo: value => events.navigation.push(value.url), reLaunch: value => events.navigation.push(value.url) }
});
if (kind === 'assessment') { hooks.load({}); hooks.show(); }
else hooks.show();
activePages.push(hooks);
return { ...result.page, events };
}
let checks = 0, failures = 0;
async function test(name, callback) {
activePages.splice(0).forEach(hooks => { hooks.hide?.(); });
flow.clearAssessmentFlow(); calls.length = 0;
try { await callback(); checks++; console.log('PASS ' + name); }
catch (error) { failures++; console.error('FAIL ' + name + '\n' + error.stack); }
}
(async () => {
await test('zero counts make only empty related amounts not applicable without changing input', () => {
const value = form(); const before = snapshot(value); save(value);
const progress = service.assessmentProgress('personal');
assert.equal(progress.assetsDone, true, 'empty balances for explicitly absent loans/cards are complete');
assert.equal(progress.percent, 100);
assert.equal(snapshot(value), before);
for (const key of ['totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit'])
assert.equal(service.getAssessmentInput('personal')[key], '');
});
await test('missing counts and positive counts never infer zero balances', () => {
for (const value of [form({ creditLoanCount: '', creditCardCount: '' }), form({ creditLoanCount: '1', creditCardCount: '1' })]) {
save(value); const progress = service.assessmentProgress('personal');
assert.equal(progress.assetsDone, false); assert.ok(progress.percent < 100);
}
});
await test('all accepted zero-count spellings share the same non-applicable behavior', () => {
for (const count of ['0', '00', '000', '0000']) {
const value = form({ creditLoanCount: count, creditCardCount: count }); save(value);
assert.equal(service.assessmentProgress('personal').assetsDone, true);
assert.equal(service.assessmentProgress('personal').percent, 100);
assert.equal(service.getAssessmentInput('personal').creditLoanCount, count);
}
});
await test('legacy credit-loan zero never receives the new loan scope during restoration', () => {
const saved = new Json({ ...form(), loanCountScope: '' });
const restored = service.formFromInput(saved); save(restored);
assert.equal(restored.creditLoanCount, '0'); assert.equal(restored.loanCountScope, '');
assert.equal(service.assessmentProgress('personal').assetsDone, false);
assert.ok(service.assetValidationMessage(restored).includes('重新确认'));
});
await test('zero count and positive related amounts are rejected without overwriting', () => {
for (const [key, expected] of [['totalDebtBalance', '贷款笔数为 0 时,欠款总余额和贷款月还款金额不能大于 0'],
['monthlyLoanPayment', '贷款笔数为 0 时,欠款总余额和贷款月还款金额不能大于 0'],
['creditCardTotalLimit', '信用卡张数为 0 时,总额度和已使用总额度不能大于 0'],
['creditCardUsedLimit', '信用卡张数为 0 时,总额度和已使用总额度不能大于 0']]) {
const value = form({ [key]: '0.000001' }); const before = snapshot(value);
assert.equal(service.assetValidationMessage(value), expected); save(value);
assert.equal(service.assessmentProgress('personal').assetsDone, false);
assert.equal(snapshot(value), before); assert.equal(service.getAssessmentInput('personal')[key], '0.000001');
}
});
await test('invalid amounts cannot grant a zero-count exemption to sibling empty amounts', () => {
for (const key of ['totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']) {
const value = form({ [key]: 'bad' }); save(value);
assert.ok(service.assetValidationMessage(value).length > 0);
assert.equal(service.assessmentProgress('personal').assetsDone, false);
assert.equal(service.getAssessmentInput('personal')[key], 'bad');
}
});
await test('entered zero money stays entered and legacy loan scope is not inferred from zero values', () => {
const value = form({ totalDebtBalance: '0.000000', monthlyLoanPayment: '0.00', creditCardTotalLimit: '0', creditCardUsedLimit: '0.000' });
const before = snapshot(value); save(value); assert.equal(service.assessmentProgress('personal').assetsDone, true);
assert.equal(service.assetValidationMessage(value), ''); assert.equal(snapshot(value), before);
const old = service.formFromInput(new Json({ ...value, loanCountScope: '' }));
assert.equal(old.loanCountScope, ''); assert.ok(service.assetValidationMessage(old).length > 0);
});
await test('new submission carries all-loan scope while empty monetary fields remain null', async () => {
const value = new Json({ ...form(), type: 'personal', documents: [], authorizations: [] });
const before = snapshot(value); await records.createAssessment(value);
const body = calls.at(-1)[2]; assert.equal(body.form.loanCountScope, 'all_loans');
assert.equal(body.form.creditLoanCount, 0); assert.equal(body.form.creditCardCount, 0);
for (const key of ['totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']) assert.equal(body.form[key], null);
assert.equal(snapshot(value), before);
});
await test('serialization preserves entered amounts and never fabricates scope for legacy callers', async () => {
const value = new Json({ ...form({ loanCountScope: '', creditLoanCount: '2', creditCardCount: '1', totalDebtBalance: '1.234567', monthlyLoanPayment: '0', creditCardTotalLimit: '2', creditCardUsedLimit: '0.123456' }), type: 'personal' });
const before = snapshot(value); await records.createAssessment(value);
const body = calls.at(-1)[2]; assert.equal(body.form.loanCountScope, undefined);
assert.equal(body.form.totalDebtBalance, '12345.67'); assert.equal(body.form.monthlyLoanPayment, '0.00');
assert.equal(body.form.creditCardTotalLimit, '20000.00'); assert.equal(body.form.creditCardUsedLimit, '1234.56');
assert.equal(snapshot(value), before);
});
await test('edit restoration preserves old and new scope and every existing amount', async () => {
for (const scope of [undefined, 'all_loans']) {
response = detail({ creditLoanCount: 0, loanCountScope: scope, creditCardCount: 0, totalDebtBalance: '12000.00', monthlyLoanPayment: null, creditCardTotalLimit: '0.00', creditCardUsedLimit: null });
const restored = await records.fetchAssessmentEdit('1');
assert.equal(restored.loanCountScope || '', scope || ''); assert.equal(restored.creditLoanCount, '0');
assert.equal(restored.totalDebtBalance, '1.2'); assert.equal(restored.monthlyLoanPayment, '');
assert.equal(restored.creditCardTotalLimit, '0'); assert.equal(restored.creditCardUsedLimit, '');
}
});
await test('archive labels retain historical credit-loan meaning and name new scope correctly', async () => {
for (const [scope, label] of [[undefined, '信用贷款笔数(旧口径)'], ['all_loans', '贷款笔数']]) {
response = detail({ creditLoanCount: 0, loanCountScope: scope });
const archived = await records.fetchAssessmentDetail('1', '2');
assert.ok(archived.fields.some(item => item.label === label && item.value === '0'));
}
});
await test('historical count requires an explicit page confirmation without clearing any values', () => {
const current = page('assessment');
const old = form({ loanCountScope: '', totalDebtBalance: '1.2', monthlyLoanPayment: '0.03' });
save(old); current.restore('personal'); current.step.value = 2;
const before = snapshot(current.form.value); current.next();
assert.equal(current.step.value, 2); assert.ok(current.events.toasts.at(-1).includes('重新确认'));
assert.equal(typeof current.confirmLoanCountScope, 'function'); current.confirmLoanCountScope();
const after = JSON.parse(snapshot(current.form.value)); after.loanCountScope = '';
assert.equal(JSON.stringify(after), before); assert.equal(current.form.value.loanCountScope, 'all_loans');
current.next(); assert.equal(current.step.value, 2, 'positive money conflict remains after confirming zero loans');
});
await test('page confirmation enables blank balances for old zero counts and remembers scope', () => {
const current = page('assessment'); save(form({ loanCountScope: '' })); current.restore('personal'); current.step.value = 2;
current.next(); assert.equal(current.step.value, 2);
current.confirmLoanCountScope(); current.next(); assert.equal(current.step.value, 3);
assert.equal(service.getAssessmentInput('personal').loanCountScope, 'all_loans');
assert.equal(service.getAssessmentInput('personal').totalDebtBalance, '');
});
await test('confirmation page refuses legacy scope and displays submitted scope without reinterpreting', async () => {
save(form({ loanCountScope: '' })); let current = page('confirm');
const before = calls.length; await current.submit(); assert.equal(calls.length, before);
assert.ok(current.events.toasts.at(-1).includes('重新确认'));
assert.equal(service.getAssessmentInput('personal').step, 2);
save(form()); current = page('confirm');
assert.ok(current.sections.value[2].rows.some(row => row.label === '贷款笔数'));
assert.ok(current.sections.value[2].rows.some(row => row.value.includes('房贷') && row.value.includes('车贷')));
});
await test('confirmation distinguishes exempt empty amounts from entered zero amounts', () => {
save(form()); let current = page('confirm'); let rows = current.sections.value[2].rows;
assert.equal(rows.find(row => row.label === '欠款总余额').value, '不适用(无贷款)');
assert.equal(rows.find(row => row.label === '总额度').value, '不适用(无信用卡)');
save(form({ totalDebtBalance: '0.000000', creditCardTotalLimit: '0.00' })); current = page('confirm'); rows = current.sections.value[2].rows;
assert.equal(rows.find(row => row.label === '欠款总余额').value, '0.000000 万元');
assert.equal(rows.find(row => row.label === '总额度').value, '0.00 万元');
});
console.log((failures ? 'FAIL' : 'PASS') + ': ' + checks + ' passed, ' + failures + ' failed zero-balance UTS/page scenarios; modelCalls=0 businessWrites=0 networkCalls=0');
if (failures) process.exitCode = 1;
})().catch(error => { console.error(error.stack); process.exitCode = 1; });