176 lines
6.2 KiB
JavaScript
176 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const root = path.resolve(__dirname, '..');
|
|
const financeHtml = fs.readFileSync(path.join(root, 'prototype-src', 'modules', '08-finance.html'), 'utf8');
|
|
const appJs = fs.readFileSync(path.join(root, 'prototype-src', 'app.js'), 'utf8');
|
|
|
|
function assertPattern(source, pattern, message) {
|
|
assert.ok(pattern.test(source), message);
|
|
}
|
|
|
|
function elementSourceById(source, id) {
|
|
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const opening = new RegExp(`<div\\b[^>]*\\bid=["']${escaped}["'][^>]*>`, 'i').exec(source);
|
|
if (!opening) return '';
|
|
|
|
const tagPattern = /<div\b[^>]*>|<\/div\s*>/gi;
|
|
tagPattern.lastIndex = opening.index;
|
|
let depth = 0;
|
|
let match;
|
|
while ((match = tagPattern.exec(source))) {
|
|
if (/^<div\b/i.test(match[0])) depth += 1;
|
|
else if (--depth === 0) return source.slice(opening.index, tagPattern.lastIndex);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function constObjectSource(source, name) {
|
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const declaration = new RegExp(`const\\s+${escaped}\\s*=\\s*\\{`).exec(source);
|
|
if (!declaration) return '';
|
|
|
|
const braceStart = source.indexOf('{', declaration.index);
|
|
let depth = 0;
|
|
let quote = '';
|
|
let escapedChar = false;
|
|
for (let index = braceStart; index < source.length; index += 1) {
|
|
const char = source[index];
|
|
if (quote) {
|
|
if (escapedChar) escapedChar = false;
|
|
else if (char === '\\') escapedChar = true;
|
|
else if (char === quote) quote = '';
|
|
continue;
|
|
}
|
|
if (char === "'" || char === '"' || char === '`') {
|
|
quote = char;
|
|
continue;
|
|
}
|
|
if (char === '{') depth += 1;
|
|
if (char === '}' && --depth === 0) return source.slice(declaration.index, index + 1);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function functionSource(source, name) {
|
|
const start = source.indexOf(`function ${name}(`);
|
|
if (start < 0) return '';
|
|
|
|
const braceStart = source.indexOf('{', start);
|
|
if (braceStart < 0) return '';
|
|
let depth = 0;
|
|
let quote = '';
|
|
let escaped = false;
|
|
|
|
for (let index = braceStart; index < source.length; index += 1) {
|
|
const char = source[index];
|
|
if (quote) {
|
|
if (escaped) escaped = false;
|
|
else if (char === '\\') escaped = true;
|
|
else if (char === quote) quote = '';
|
|
continue;
|
|
}
|
|
if (char === "'" || char === '"' || char === '`') {
|
|
quote = char;
|
|
continue;
|
|
}
|
|
if (char === '{') depth += 1;
|
|
if (char === '}' && --depth === 0) return source.slice(start, index + 1);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
const businessTabs = {
|
|
firstMile: '头程',
|
|
lastMile: '尾程',
|
|
};
|
|
|
|
const businessTabsSource = elementSourceById(financeHtml, 'profit-business-tabs');
|
|
assert.ok(businessTabsSource, '利润表缺少业务类型页签容器');
|
|
Object.entries(businessTabs).forEach(([key, label]) => {
|
|
assertPattern(
|
|
businessTabsSource,
|
|
new RegExp(`data-profit-business=["']${key}["'][^>]*>\\s*${label}`),
|
|
`利润表缺少“${label}”业务页签`,
|
|
);
|
|
});
|
|
|
|
assert.ok(
|
|
!/data-profit-business=["'](warehouseRent|relabel|warehouseTransfer)["']/.test(businessTabsSource),
|
|
'利润表不应继续展示仓租、贴标或仓储转运业务页签',
|
|
);
|
|
|
|
const orderTypeMatch = /<select\b[^>]*\bid=["']profit-order-type["'][^>]*>[\s\S]*?<\/select>/i.exec(financeHtml);
|
|
const orderTypeSource = orderTypeMatch ? orderTypeMatch[0] : '';
|
|
assert.ok(orderTypeSource, '利润表缺少订单类型查询下拉');
|
|
['全部订单类型', '暂扣', '退货', '仓储转运', '换标'].forEach((label) => {
|
|
assertPattern(orderTypeSource, new RegExp('>\\s*' + label + '\\s*</option>'), `订单类型下拉缺少“${label}”`);
|
|
});
|
|
|
|
const profitBusinessDataSource = constObjectSource(appJs, 'profitBusinessData');
|
|
assert.ok(profitBusinessDataSource, '缺少利润表业务数据对象');
|
|
assertPattern(appJs, /let\s+profitBusiness\s*=\s*["']firstMile["']/, '利润表默认业务类型应为头程');
|
|
assertPattern(appJs, /function\s+switchProfitBusiness\s*\(/, '缺少利润表业务类型切换函数');
|
|
assertPattern(appJs, /function\s+getActiveProfitTickets\s*\(/, '缺少当前业务利润单票读取函数');
|
|
|
|
Object.keys(businessTabs).forEach((key) => {
|
|
assertPattern(profitBusinessDataSource, new RegExp(`(?:["']${key}["']|\\b${key})\\s*:\\s*\\[`), `利润表业务数据缺少 ${key} 数组`);
|
|
});
|
|
|
|
assertPattern(appJs, /profit-order-type/, '利润表脚本未读取订单类型筛选');
|
|
assertPattern(appJs, /orderType/, '利润表数据或过滤逻辑缺少订单类型字段');
|
|
|
|
const profitBusinessLabelsSource = constObjectSource(appJs, 'profitBusinessLabels');
|
|
assert.ok(profitBusinessLabelsSource, '缺少利润表业务名称对象');
|
|
Object.entries(businessTabs).forEach(([key, label]) => {
|
|
assertPattern(
|
|
profitBusinessLabelsSource,
|
|
new RegExp(`(?:["']${key}["']|\\b${key})\\s*:\\s*["']${label}["']`),
|
|
`利润表业务名称 ${key} 应映射为“${label}”`,
|
|
);
|
|
});
|
|
|
|
const profitFeeTemplatesSource = constObjectSource(appJs, 'profitFeeTemplates');
|
|
assert.ok(profitFeeTemplatesSource, '缺少利润表费用模板对象');
|
|
Object.keys(businessTabs).forEach((key) => {
|
|
assertPattern(
|
|
profitFeeTemplatesSource,
|
|
new RegExp(`(?:["']${key}["']|\\b${key})\\s*:`),
|
|
`利润表费用模板缺少 ${key} 配置`,
|
|
);
|
|
});
|
|
|
|
for (const feeLabel of ['头程运费收入', '尾程服务收入']) {
|
|
assertPattern(
|
|
profitFeeTemplatesSource,
|
|
new RegExp("[\"']" + feeLabel + "[\"']"),
|
|
'利润表费用模板缺少“' + feeLabel + '”',
|
|
);
|
|
}
|
|
|
|
const profitDetailSource = functionSource(appJs, 'openProfitDetail');
|
|
assert.ok(profitDetailSource, '缺少利润明细函数');
|
|
assertPattern(
|
|
profitDetailSource,
|
|
/profitBusinessLabels\s*\[\s*profitBusiness\s*\]/,
|
|
'利润单票明细应展示当前业务类型',
|
|
);
|
|
|
|
const profitResetSource = functionSource(appJs, 'profitReset');
|
|
assert.ok(profitResetSource, '缺少利润表重置函数');
|
|
assertPattern(
|
|
profitResetSource,
|
|
/profitBusiness\s*=\s*["']firstMile["']/,
|
|
'利润表重置时应恢复头程业务类型',
|
|
);
|
|
assertPattern(
|
|
profitResetSource,
|
|
/profit-order-type[\s\S]*value\s*=\s*["']?["']?/,
|
|
'利润表重置时应清空订单类型',
|
|
);
|
|
|
|
console.log('利润表业务类型页签回归校验通过');
|