Files
docs/悦/货代SAAS平台后台/tests/verify-finance-ar-detail-report.cjs
T
2026-08-22 09:50:50 +08:00

199 lines
11 KiB
JavaScript

#!/usr/bin/env node
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const sourceRoot = path.join(root, 'prototype-src');
const manifest = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'manifest.json'), 'utf8'));
const financeHtml = fs.readFileSync(path.join(sourceRoot, 'modules', '08-finance.html'), 'utf8');
const shellHtml = fs.readFileSync(path.join(sourceRoot, 'shell.html'), 'utf8');
const overlaysHtml = fs.readFileSync(path.join(sourceRoot, 'overlays.html'), 'utf8');
const appJs = fs.readFileSync(path.join(sourceRoot, 'app.js'), 'utf8');
function countMatches(source, pattern) {
return [...source.matchAll(pattern)].length;
}
function extractPage(source, pageId) {
const escaped = pageId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const opening = new RegExp(`<div\\b[^>]*\\bid=["']page-${escaped}["'][^>]*>`, 'i').exec(source);
if (!opening) return '';
const rest = source.slice(opening.index + opening[0].length);
const nextPage = /<div\b[^>]*\bclass=["'][^"']*\bpage\b[^"']*["'][^>]*\bid=["']page-/i.exec(rest);
return source.slice(opening.index, nextPage
? opening.index + opening[0].length + nextPage.index
: source.length);
}
const pageId = 'finance-ar-detail-report';
const financeModule = manifest.modules.find(item => item.file === 'modules/08-finance.html');
assert.ok(financeModule, '缺少财务模块 manifest 登记');
assert.equal(financeModule.pages.filter(page => page === pageId).length, 1, '财务模块必须唯一登记报表页面');
assert.equal(manifest.pageOrder.filter(page => page === pageId).length, 1, '根 pageOrder 必须唯一登记报表页面');
assert.equal(countMatches(financeHtml, /id=["']page-finance-ar-detail-report["']/g), 1, '财务模块必须唯一包含报表页面');
const navEntry = `switchPage('${pageId}',this)`;
const navIndex = shellHtml.indexOf(navEntry);
const arapIndex = shellHtml.indexOf("switchPage('customer-arap',this)");
const reconIndex = shellHtml.indexOf("switchPage('customer-recon-stmt',this)");
assert.ok(navIndex >= 0, '财务导航缺少应收款统计表入口');
assert.ok(arapIndex >= 0 && navIndex > arapIndex, '应收款统计表入口应位于客户应收应付之后');
assert.ok(reconIndex >= 0 && navIndex < reconIndex, '应收款统计表入口应位于客户对账单之前');
assert.match(
shellHtml,
/<div\b[^>]*\bonclick=["']switchPage\('finance-ar-detail-report',this\)["'][^>]*>\s*应收款统计表\s*<\/div>/,
'财务导航入口必须使用“应收款统计表”文案',
);
const reportPage = extractPage(financeHtml, pageId);
assert.ok(reportPage, '财务模块缺少应收款统计表页面');
assert.ok(reportPage.includes('应收款统计表'), '页面标题和面包屑必须使用“应收款统计表”');
assert.match(reportPage, /<label\b[^>]*for=["']ardr-month["'][^>]*>统计月份<\/label>/, '筛选项必须改为统计月份');
assert.match(reportPage, /<input\b[^>]*\bid=["']ardr-month["'][^>]*\btype=["']month["'][^>]*\bvalue=["']2026-07["']/, '统计月份必须使用 month 控件并默认 2026-07');
assert.doesNotMatch(reportPage, /\bid=["']ardr-year["']/, '页面不应继续保留统计年度控件');
[
'ardr-entity',
'ardr-currency',
'ardr-total-receivable',
'ardr-total-received',
'ardr-total-deduction',
'ardr-total-balance',
'thead-receivable-detail-report',
'tbody-receivable-detail-report',
].forEach(id => assert.match(reportPage, new RegExp(`\\bid=["']${id}["']`), `页面缺少字段 ${id}`));
['应收总金额', '已收合计', '零头减免', '余额未收', '客户', '备注']
.forEach(label => assert.ok(reportPage.includes(label), `页面缺少文案 ${label}`));
assert.match(reportPage, /<span\b[^>]*\bid=["']ardr-table-title["'][^>]*>应收款明细<\/span>/, '列表标题不应显示统计月份');
assert.doesNotMatch(reportPage, /position\s*:\s*sticky/i, '标准列表不应使用页面专属冻结列');
assert.doesNotMatch(reportPage, /ardr-month-group/, '标准列表不应使用月份分组表头');
assert.match(overlaysHtml, /<div\b[^>]*\bid=["']modal-receivable-report-edit["']/, '缺少应收款明细编辑弹窗');
assert.match(overlaysHtml, /<input\b[^>]*\bid=["']ardr-edit-entity["'][^>]*\breadonly\b/, '应收单位必须只读');
assert.match(overlaysHtml, /<input\b[^>]*\bid=["']ardr-edit-customer["'][^>]*\brequired\b[^>]*\bmaxlength=["']50["']/, '客户必须必填且最多 50 字');
assert.match(overlaysHtml, /<textarea\b[^>]*\bid=["']ardr-edit-remark["'][^>]*\bmaxlength=["']200["']/, '备注必须最多 200 字');
assert.match(overlaysHtml, /onclick=["']saveReceivableReportEdit\(\)["']/, '编辑弹窗保存按钮未接入保存函数');
const calculateStart = appJs.indexOf('function calculateReceivableReportRow');
const renderStart = appJs.indexOf('function renderReceivableDetailReport');
assert.ok(calculateStart >= 0 && renderStart > calculateStart, '缺少应收款统计表连续函数区间');
const reportFunctions = appJs.slice(calculateStart, renderStart);
['calculateReceivableReportRow', 'summarizeReceivableReportByCurrency', 'buildReceivableReportHeader', 'updateReceivableReportEditableFields']
.forEach(name => assert.match(reportFunctions, new RegExp(`function\\s+${name}\\s*\\(`), `函数区间缺少 ${name}`));
const sandbox = {};
vm.runInNewContext(
`${reportFunctions}\nthis.api={calculateReceivableReportRow,summarizeReceivableReportByCurrency,buildReceivableReportHeader,updateReceivableReportEditableFields};`,
sandbox,
{filename: 'receivable-report-functions.js', timeout: 1000},
);
const api = sandbox.api;
const julyRow = api.calculateReceivableReportRow({
year: 2026,
customer: '客户甲',
receivableEntity: '主体A',
currency: 'CNY',
monthlyAmounts: {
6: {direct: 50, special: 10, received: 40, deduction: 0},
7: {direct: 100, special: 20, received: 90, deduction: 5},
},
}, '2026-07');
assert.equal(julyRow.monthKey, '2026-07', '行数据必须记录所选月份');
assert.deepEqual(
JSON.parse(JSON.stringify(julyRow.month)),
{
firstLeg: 100,
relabel: 20,
warehouseTransit: 0,
goodsDisposal: 0,
returnRelabel: 0,
usDomesticTruck: 0,
usDomesticLooseBoard: 0,
},
'行数据只能读取所选月份的 7 项业务金额',
);
assert.equal(julyRow.receivableAmount, 120, '应收总金额必须为所选月份两项业务金额之和');
assert.equal(julyRow.receivedAmount, 90, '已收合计必须读取所选月份金额');
assert.equal(julyRow.deductionAmount, 5, '零头减免必须读取所选月份金额');
assert.equal(julyRow.balanceAmount, 25, '余额未收必须按所选月份计算');
const missingRow = api.calculateReceivableReportRow({
year: 2026,
currency: 'CNY',
monthlyAmounts: {7: {direct: 100, special: 20, received: 90, deduction: 5}},
}, '2026-08');
assert.deepEqual(
JSON.parse(JSON.stringify(missingRow.month)),
{
firstLeg: null,
relabel: null,
warehouseTransit: null,
goodsDisposal: null,
returnRelabel: null,
usDomesticTruck: null,
usDomesticLooseBoard: null,
},
'缺失月份必须返回空业务金额',
);
assert.equal(missingRow.receivableAmount, 0, '缺失月份应收金额必须为 0');
assert.equal(missingRow.receivedAmount, 0, '缺失月份已收金额必须为 0');
assert.equal(missingRow.balanceAmount, 0, '缺失月份余额必须为 0');
const usdRow = api.calculateReceivableReportRow({
year: 2026,
currency: 'USD',
monthlyAmounts: {7: {direct: 10, special: 5, received: 3, deduction: 2}},
}, '2026-07');
const summary = api.summarizeReceivableReportByCurrency([julyRow, usdRow]);
assert.deepEqual(
JSON.parse(JSON.stringify(summary)),
{
CNY: {receivable: 120, received: 90, deduction: 5, balance: 25},
USD: {receivable: 15, received: 3, deduction: 2, balance: 10},
},
'应按币种汇总所选月份金额',
);
const editableRecord = {customer: '原客户', remark: '原备注'};
assert.equal(api.updateReceivableReportEditableFields(editableRecord, ' 新客户 ', ' 新备注 '), true, '有效编辑应返回 true');
assert.deepEqual(editableRecord, {customer: '新客户', remark: '新备注'}, '客户和备注应去除首尾空格后更新');
const invalidRecord = {customer: '保留客户', remark: '保留备注'};
assert.equal(api.updateReceivableReportEditableFields(invalidRecord, ' ', '不应写入'), false, '空客户应返回 false');
assert.deepEqual(invalidRecord, {customer: '保留客户', remark: '保留备注'}, '校验失败时不应修改原数据');
const header = api.buildReceivableReportHeader('2026-07');
assert.equal(typeof header, 'string', '单月表头必须生成 HTML 字符串');
assert.equal(countMatches(header, /<tr>/g), 1, '标准列表必须使用单层表头');
assert.equal(countMatches(header, /<th\b/g), 17, '标准列表必须固定 17 列');
assert.match(header, /<th>操作<\/th><\/tr>$/, '操作必须是最后一列');
assert.equal(countMatches(header, /ardr-month-group/g), 0, '表头不应生成月份分组');
['头程', '换标', '仓储转运', '货物废弃', '退货换标', '美国本土卡', '美本土散板']
.forEach(label => assert.equal(countMatches(header, new RegExp(`<th>${label}<\/th>`, 'g')), 1, `表头必须包含一个 ${label}`));
assert.doesNotMatch(header, /FBA直发|FBA专线/, '表头不应继续使用旧 FBA 业务列');
assert.ok(!header.includes('2026.07'), '表头不应显示所选月份');
assert.match(appJs, /if\(page==='finance-ar-detail-report'\)renderReceivableDetailReport\(\)/, 'switchPage 未初始化应收款统计表');
assert.ok(appJs.includes("getElementById('ardr-month')"), '渲染逻辑必须读取统计月份');
assert.ok(appJs.includes("month.value='2026-07'"), '重置必须恢复默认月份 2026-07');
assert.ok(appJs.includes('暂无匹配的应收款明细'), '缺少单月统计空态文案');
assert.match(appJs, /function\s+openReceivableReportEdit\s*\(/, '缺少打开单条编辑弹窗函数');
assert.match(appJs, /function\s+saveReceivableReportEdit\s*\(/, '缺少保存单条编辑函数');
assert.ok(appJs.includes('请输入客户'), '缺少客户必填校验提示');
assert.ok(appJs.includes('应收款明细已更新'), '缺少编辑成功提示');
assert.ok(appJs.includes('应收款统计表已导出'), '缺少新报表名称的导出反馈');
assert.ok(appJs.includes('正在打开打印预览'), '缺少打印反馈');
assert.match(
appJs,
/<button\b[^>]*\btype=["']button["'][^>]*\bclass=["']btn btn-sm btn-primary["'][^>]*\bonclick="openReceivableReportEdit\([^\"]*\)"[^>]*>编辑<\/button>/,
'应收款明细行编辑按钮必须使用 btn btn-sm btn-primary 样式并保留编辑行为',
);
assert.doesNotMatch(
appJs,
/<button\b[^>]*\bclass=["'][^"']*\bbtn-link\b[^"']*["'][^>]*\bonclick=["']openReceivableReportEdit\(/,
'应收款明细行编辑按钮不应使用未定义的 btn-link 样式',
);
console.log('应收款统计表校验通过');