Files
docs/悦/货代SAAS平台后台/tests/verify-outbound-vehicle-edit-delivery.cjs
T

433 lines
15 KiB
JavaScript
Raw Normal View History

2026-08-21 09:20:26 +08:00
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const root = path.resolve(__dirname, '..');
const moduleHtml = fs.readFileSync(
path.join(root, 'prototype-src', 'modules', '04-overseas-delivery.html'),
'utf8',
);
const overlays = fs.readFileSync(path.join(root, 'prototype-src', 'overlays.html'), 'utf8');
const tail = fs.readFileSync(path.join(root, 'prototype-src', 'tail.html'), 'utf8');
const app = fs.readFileSync(path.join(root, 'prototype-src', 'app.js'), 'utf8');
const manifest = JSON.parse(
fs.readFileSync(path.join(root, 'prototype-src', 'manifest.json'), 'utf8'),
);
const failures = [];
function expect(condition, message) {
if (!condition) failures.push(message);
}
function codeOnly(source) {
let output = '';
let state = 'code';
let escaped = false;
const regexCanStartAfter = /[([{,:;=!?&|+\-*%^~<>]/;
const controlKeywords = new Set(['if', 'while', 'for', 'with', 'switch', 'catch']);
let previousSignificant = '';
let previousWord = '';
let regexAllowedAfterControlParen = false;
const parenthesisStack = [];
for (let index = 0; index < source.length; index += 1) {
const char = source[index];
const next = source[index + 1];
if (state === 'line-comment') {
output += char === '\n' || char === '\r' ? char : ' ';
if (char === '\n' || char === '\r') state = 'code';
continue;
}
if (state === 'block-comment') {
output += ' ';
if (char === '*' && next === '/') {
output += ' ';
index += 1;
state = 'code';
}
continue;
}
if (state === 'regex' || state === 'regex-class') {
output += char === '\n' || char === '\r' ? char : ' ';
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (state === 'regex' && char === '[') {
state = 'regex-class';
} else if (state === 'regex-class' && char === ']') {
state = 'regex';
} else if (state === 'regex' && char === '/') {
state = 'code';
previousSignificant = '/';
previousWord = '';
regexAllowedAfterControlParen = false;
}
continue;
}
if (state !== 'code') {
output += char === '\n' || char === '\r' ? char : ' ';
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (
(state === 'single-quote' && char === "'")
|| (state === 'double-quote' && char === '"')
|| (state === 'template' && char === '`')
) {
state = 'code';
}
continue;
}
if (char === '/' && next === '/') {
output += ' ';
index += 1;
state = 'line-comment';
} else if (char === '/' && next === '*') {
output += ' ';
index += 1;
state = 'block-comment';
} else if (char === "'") {
output += ' ';
state = 'single-quote';
} else if (char === '"') {
output += ' ';
state = 'double-quote';
} else if (char === '`') {
output += ' ';
state = 'template';
} else if (char === '(') {
output += char;
parenthesisStack.push(controlKeywords.has(previousWord));
previousSignificant = char;
previousWord = '';
regexAllowedAfterControlParen = false;
} else if (char === ')') {
output += char;
regexAllowedAfterControlParen = parenthesisStack.pop() === true;
previousSignificant = char;
previousWord = '';
} else if (char === '/') {
const startsRegex = !previousSignificant
|| regexCanStartAfter.test(previousSignificant)
|| /^(return|case|throw|typeof|void|delete|yield|await)$/.test(previousWord)
|| regexAllowedAfterControlParen;
output += startsRegex ? ' ' : char;
if (startsRegex) state = 'regex';
else {
previousSignificant = char;
previousWord = '';
}
regexAllowedAfterControlParen = false;
} else {
output += char;
if (!/\s/.test(char)) {
previousSignificant = char;
previousWord = /[A-Za-z0-9_$]/.test(char) ? `${previousWord}${char}` : '';
regexAllowedAfterControlParen = false;
}
}
}
return output;
}
function extractFunction(name, source, code = codeOnly(source)) {
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const declaration = new RegExp(`function\\s+${escapedName}\\s*\\(`).exec(code);
if (!declaration) return '';
const bodyStart = code.indexOf('{', declaration.index + declaration[0].length);
if (bodyStart < 0) return '';
let depth = 0;
for (let index = bodyStart; index < code.length; index += 1) {
const char = code[index];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) return source.slice(declaration.index, index + 1);
}
}
return '';
}
const extractorProbe = [
'// function ignoredLine() {}',
'/* function ignoredBlock() {} */',
'const ignoredString = "function ignoredString() {}";',
'const ignoredTemplate = `function ignoredTemplate() {}`;',
'const quotePattern = /["\']/;',
'if (ready) /function ignoredControlRegex() {}/.test(value);',
'function liveProbe() { return {ok: true}; }',
].join('\n');
expect(!extractFunction('ignoredLine', extractorProbe), '函数提取器误识别行注释声明');
expect(!extractFunction('ignoredBlock', extractorProbe), '函数提取器误识别块注释声明');
expect(!extractFunction('ignoredString', extractorProbe), '函数提取器误识别字符串声明');
expect(!extractFunction('ignoredTemplate', extractorProbe), '函数提取器误识别模板文本声明');
expect(!extractFunction('ignoredControlRegex', extractorProbe), '函数提取器误识别控制语句后的正则文本');
expect(Boolean(extractFunction('liveProbe', extractorProbe)), '函数提取器无法识别真实声明');
function extractModal(source, modalId) {
const escapedId = modalId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const opening = new RegExp(`<div\\b[^>]*\\bid=["']${escapedId}["'][^>]*>`, 'i').exec(source);
if (!opening) return '';
const tagPattern = /<\/?div\b[^>]*>/gi;
tagPattern.lastIndex = opening.index;
let depth = 0;
let tag;
while ((tag = tagPattern.exec(source))) {
if (/^<\/div\b/i.test(tag[0])) depth -= 1;
else depth += 1;
if (depth === 0) return source.slice(opening.index, tagPattern.lastIndex);
}
return '';
}
function getSelectOptionValues(source, selectId) {
const escapedId = selectId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const select = new RegExp(
`<select\\b(?=[^>]*\\bid=["']${escapedId}["'])[^>]*>([\\s\\S]*?)<\\/select>`,
'i',
).exec(source);
if (!select) return null;
const values = [];
const optionPattern = /<option\b([^>]*)>([\s\S]*?)<\/option>/gi;
let option;
while ((option = optionPattern.exec(select[1]))) {
const value = /\bvalue=["']([^"']*)["']/i.exec(option[1]);
const text = option[2].replace(/<[^>]*>/g, '').trim();
const optionValue = value ? value[1] : text;
if (optionValue) values.push(optionValue);
}
return values;
}
function expectExactOptionSet(source, selectId, expectedValues, label) {
const actualValues = getSelectOptionValues(source, selectId);
expect(Boolean(actualValues), `缺少 ${label}选择框`);
if (!actualValues) return;
const actual = [...actualValues].sort();
const expected = [...expectedValues].sort();
expect(
actual.length === expected.length && actual.every((value, index) => value === expected[index]),
`${label}选项应为“${expectedValues.join('、')}”`,
);
}
expect(!moduleHtml.includes('id="page-outbound-vehicle-edit"'), '独立编辑页应移除');
expect(!manifest.pageOrder.includes('outbound-vehicle-edit'), 'pageOrder 应移除独立编辑页');
const deliveryModule = manifest.modules.find(
(item) => item.file === 'modules/04-overseas-delivery.html',
);
expect(Boolean(deliveryModule), 'manifest 缺少海外派送模块');
if (deliveryModule) {
expect(
!deliveryModule.pages.includes('outbound-vehicle-edit'),
'海外派送模块 pages 应移除独立编辑页',
);
}
expect(overlays.includes('id="modal-outbound-vehicle-edit"'), '缺少独立编辑排车弹框');
expect(
overlays.includes('id="modal-outbound-vehicle-order-select"'),
'缺少关联订单选择弹框',
);
const bolModal = extractModal(tail, 'modal-bol');
expect(Boolean(bolModal), '缺少独立转派送弹框');
expectExactOptionSet(
bolModal,
'bol-deliveryMethod',
['FBA自提', '客户自提', '卡派打托', '卡派地板', '快递', '外配'],
'派送方式',
);
expectExactOptionSet(bolModal, 'bol-pickupMethod', ['一提一卸', '一提多卸'], '提货方式');
expectExactOptionSet(bolModal, 'bol-loadType', ['托盘', '地板', '混装'], '装车类型');
expectExactOptionSet(bolModal, 'bol-bookAccount', ['4', '8'], '预约账号');
expectExactOptionSet(
bolModal,
'bol-vehicleSize',
['大车(30托/15托)', '小车(14托/12托)'],
'车辆大小',
);
expect(!bolModal.includes('卡派地版'), '转派送弹框不应保留错误文案“卡派地版”');
expect(bolModal.includes('<label>预约号</label>'), 'ISA号码应改为预约号');
expect(bolModal.includes('id="bol-appointmentNo"'), '预约号应使用独立字段');
expect(bolModal.includes('id="bol-courierCompanyId"'), '快递方式应保留快递公司选择');
expect(!bolModal.includes('车队邮箱'), '转派送弹框应移除车队邮箱');
expect(!bolModal.includes('ISA号码'), '转派送弹框不应保留 ISA号码 文案');
expect(!bolModal.includes('是否发送派送单'), '转派送弹框应移除是否发送派送单');
expect(!bolModal.includes('预估派送单费用'), '费用区应移除预估派送单费用');
expect(!bolModal.includes('快递费用'), '费用区应移除快递费用');
expect(!bolModal.includes('id="bol-driverEmail"'), '转派送弹框不应保留车队邮箱字段');
expect(!bolModal.includes('id="bol-sendDispatch"'), '转派送弹框不应保留发送派送单字段');
expect(!bolModal.includes('id="bol-estimatedDispatchFee"'), '转派送弹框不应保留预估派送费用字段');
expect(!bolModal.includes('id="bol-courierFee"'), '转派送弹框不应保留快递费字段');
expect(
/id="bol-dispatchFee"[^>]*placeholder="请输入派送费用"/.test(bolModal),
'派送费用输入框应提示“请输入派送费用”',
);
const vehicleTypeIndex = bolModal.indexOf('id="bol-vehicleType"');
const vehicleSizeIndex = bolModal.indexOf('id="bol-vehicleSize"');
const appointmentAccountIndex = bolModal.indexOf('id="bol-bookAccount"');
expect(
vehicleTypeIndex >= 0 && vehicleSizeIndex > vehicleTypeIndex && appointmentAccountIndex > vehicleSizeIndex,
'车辆大小应排在车辆类型后、预约账号前',
);
const functionNames = [
'editVehicle',
'switchOutboundVehicleEditTab',
'renderOutboundVehicleEditOrders',
'removeOutboundVehicleEditOrder',
'getAvailableOutboundVehicleOrders',
'openOutboundVehicleOrderSelect',
'renderOutboundVehicleOrderOptions',
'confirmOutboundVehicleOrderSelection',
'saveVehicleEdit',
'openBolModal',
'handleOutboundVehicleLoadTypeChange',
'validateOutboundVehicleDeliveryDraft',
'nextOutboundVehicleDeliveryNo',
'saveBol',
'openOutboundVehicleDelivery',
'getOutboundVehicleCourierFeeDetails',
];
const scriptSource = `${app}\n${tail}`;
const scriptCode = codeOnly(scriptSource);
const functionSources = Object.fromEntries(
functionNames.map((name) => [name, extractFunction(name, scriptSource, scriptCode)]),
);
functionNames.forEach((name) => {
expect(Boolean(functionSources[name]), `缺少 ${name} 函数`);
});
const saveBolSource = functionSources.saveBol || '';
expect(saveBolSource.includes("appointmentAccount:value('bol-bookAccount')"), '保存时应读取预约账号');
expect(saveBolSource.includes("appointmentNo:value('bol-appointmentNo')"), '保存时应读取预约号');
expect(saveBolSource.includes("vehicleSize:value('bol-vehicleSize')"), '保存时应读取车辆大小');
expect(saveBolSource.includes('appointmentAccount:draft.appointmentAccount'), '派送记录应保存预约账号');
expect(saveBolSource.includes('appointmentNo:draft.appointmentNo'), '派送记录应保存预约号');
expect(saveBolSource.includes('vehicleSize:draft.vehicleSize'), '派送记录应保存车辆大小');
function runPureFunctionCheck(name, callback) {
if (!functionSources[name]) return;
try {
const pureFunction = new Function(`${functionSources[name]}\nreturn ${name};`)();
callback(pureFunction);
} catch (error) {
failures.push(`${name} 运行时验证失败:${error.message}`);
}
}
runPureFunctionCheck('getAvailableOutboundVehicleOrders', (getAvailableOutboundVehicleOrders) => {
const orders = [
{orderNo: 'ORDER-A', status: '待派送', destFba: 'ONT8'},
{orderNo: 'ORDER-B', status: '待派送', destFba: 'LAX9'},
{orderNo: 'ORDER-C', status: '派送中', destFba: 'ONT8'},
];
const vehicles = [
{vehicleNo: 'CAR-1', orders: [{orderNo: 'ORDER-A'}]},
{vehicleNo: 'CAR-2', orders: [{orderNo: 'ORDER-B'}]},
];
assert.deepEqual(
getAvailableOutboundVehicleOrders('CAR-1', [], orders, vehicles).map((item) => item.orderNo),
['ORDER-A'],
);
});
runPureFunctionCheck('validateOutboundVehicleDeliveryDraft', (validateOutboundVehicleDeliveryDraft) => {
assert.equal(
validateOutboundVehicleDeliveryDraft({
deliveryDate: '2026-08-13',
deliveryMethod: '卡派地板',
pickupMethod: '一提一卸',
loadType: '地板',
dispatchFee: 200,
loadingFee: 150,
}),
'',
);
assert.match(
validateOutboundVehicleDeliveryDraft({
deliveryDate: '2026-08-13',
deliveryMethod: '卡派地板',
pickupMethod: '一提一卸',
loadType: '地板',
dispatchFee: -1,
loadingFee: 150,
}),
/派送费用/,
);
assert.match(
validateOutboundVehicleDeliveryDraft({
deliveryDate: '2026-08-13',
deliveryMethod: '快递',
pickupMethod: '一提一卸',
loadType: '托盘',
dispatchFee: 200,
courierCompanyId: '',
}, [{id: 'EXP-1', status: '合作中'}]),
/快递公司/,
);
assert.equal(
validateOutboundVehicleDeliveryDraft({
deliveryDate: '2026-08-13',
deliveryMethod: '快递',
pickupMethod: '一提一卸',
loadType: '托盘',
dispatchFee: 200,
courierCompanyId: 'EXP-1',
}, [{id: 'EXP-1', status: '合作中'}]),
'',
);
});
runPureFunctionCheck('getOutboundVehicleCourierFeeDetails', (getOutboundVehicleCourierFeeDetails) => {
assert.deepEqual(
getOutboundVehicleCourierFeeDetails([
{
source: 'outboundVehicle',
deliveryMethod: '快递',
dispatchNo: 'PSD20260818001',
courierCompanyName: 'UPS',
appointmentDate: '2026-08-18',
dispatchFee: 88,
},
]),
[{
dispatchNo: 'PSD20260818001',
company: 'UPS',
businessDate: '2026-08-18',
currency: 'USD',
amount: 88,
}],
);
});
runPureFunctionCheck('nextOutboundVehicleDeliveryNo', (nextOutboundVehicleDeliveryNo) => {
assert.equal(
nextOutboundVehicleDeliveryNo('2026-08-13', [
{dispatchNo: 'PSD20260813001'},
{dispatchNo: 'PSD20260813003'},
]),
'PSD20260813004',
);
});
if (failures.length) {
console.error('OUTBOUND_VEHICLE_EDIT_DELIVERY_CHECK_FAIL');
failures.forEach((failure) => console.error(`- ${failure}`));
process.exitCode = 1;
} else {
console.log('OUTBOUND_VEHICLE_EDIT_DELIVERY_CHECK_PASS');
}