Files
docs/悦/货代SAAS平台后台/tests/verify-fba-carton-management.cjs
T
2026-08-19 14:28:55 +08:00

308 lines
13 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 moduleHtml = fs.readFileSync(path.join(root, 'prototype-src/modules/03-overseas-warehouse.html'), 'utf8');
const overlaysHtml = fs.readFileSync(path.join(root, 'prototype-src/overlays.html'), 'utf8');
const appJs = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
const failures = [];
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Find the matching brace while treating strings and comments as opaque text.
function findClosingBrace(source, openingIndex) {
let depth = 0;
let quote = '';
let lineComment = false;
let blockComment = false;
for (let index = openingIndex; index < source.length; index += 1) {
const character = source[index];
const next = source[index + 1];
if (lineComment) {
if (character === '\n' || character === '\r') lineComment = false;
continue;
}
if (blockComment) {
if (character === '*' && next === '/') {
blockComment = false;
index += 1;
}
continue;
}
if (quote) {
if (character === '\\') index += 1;
else if (character === quote) quote = '';
continue;
}
if (character === '/' && next === '/') {
lineComment = true;
index += 1;
continue;
}
if (character === '/' && next === '*') {
blockComment = true;
index += 1;
continue;
}
if (character === "'" || character === '"' || character === '`') {
quote = character;
continue;
}
if (character === '{') depth += 1;
if (character === '}') {
depth -= 1;
if (depth === 0) return index;
}
}
return -1;
}
function findStatementEnd(source, startIndex) {
let quote = '';
let lineComment = false;
let blockComment = false;
let parenDepth = 0;
let bracketDepth = 0;
let braceDepth = 0;
for (let index = startIndex; index < source.length; index += 1) {
const character = source[index];
const next = source[index + 1];
if (lineComment) {
if (character === '\n' || character === '\r') lineComment = false;
continue;
}
if (blockComment) {
if (character === '*' && next === '/') {
blockComment = false;
index += 1;
}
continue;
}
if (quote) {
if (character === '\\') index += 1;
else if (character === quote) quote = '';
continue;
}
if (character === '/' && next === '/') {
lineComment = true;
index += 1;
continue;
}
if (character === '/' && next === '*') {
blockComment = true;
index += 1;
continue;
}
if (character === "'" || character === '"' || character === '`') {
quote = character;
continue;
}
if (character === '(') parenDepth += 1;
else if (character === ')') parenDepth -= 1;
else if (character === '[') bracketDepth += 1;
else if (character === ']') bracketDepth -= 1;
else if (character === '{') braceDepth += 1;
else if (character === '}') braceDepth -= 1;
else if (character === ';' && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) return index;
}
return -1;
}
function getFunctionSource(name, source = appJs) {
const declaration = new RegExp(`(?:async\\s+)?function\\s+${escapeRegExp(name)}\\s*\\([^)]*\\)\\s*\\{`).exec(source);
if (!declaration) return '';
const openingBrace = source.indexOf('{', declaration.index);
const closingBrace = findClosingBrace(source, openingBrace);
return closingBrace < 0 ? '' : source.slice(declaration.index, closingBrace + 1);
}
function getDeclarationSource(name, source = appJs) {
const declaration = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(name)}\\s*=`).exec(source);
if (!declaration) return '';
const semicolon = findStatementEnd(source, declaration.index);
return semicolon < 0 ? '' : source.slice(declaration.index, semicolon + 1);
}
function requireMatch(pattern, label, source) {
try {
assert.match(source, pattern, label);
} catch (error) {
failures.push(error.message);
}
}
requireMatch(/onclick="openFbaCartonForm\(\)"/, '箱号新增按钮应打开表单', moduleHtml);
requireMatch(/onclick="exportFbaCartons\(\)"/, '箱号导出按钮应调用导出方法', moduleHtml);
requireMatch(/id="fbc-wh"[^>]*>[\s\S]*?<option>FBA IND9<\/option>/, '箱号仓库筛选应覆盖 FBA IND9', moduleHtml);
const modalStart = overlaysHtml.indexOf('<div class="modal-overlay" id="modal-fba-carton-form"');
const nextModalStart = modalStart < 0 ? -1 : overlaysHtml.indexOf('<div class="modal-overlay"', modalStart + 1);
const cartonModalHtml = modalStart < 0 ? '' : overlaysHtml.slice(modalStart, nextModalStart < 0 ? overlaysHtml.length : nextModalStart);
requireMatch(/id="modal-fba-carton-form"/, '应提供箱号新增编辑弹框', cartonModalHtml);
[
'fbc-form-title', 'fbc-form-carton', 'fbc-form-wh', 'fbc-form-sku',
'fbc-form-cn-name', 'fbc-form-qty', 'fbc-form-weight', 'fbc-form-pallet',
'fbc-form-inbound', 'fbc-form-outbound', 'fbc-form-status', 'fbc-form-remark',
].forEach((id) => requireMatch(new RegExp(`id="${id}"`), `表单缺少 ${id}`, cartonModalHtml));
requireMatch(/saveFbaCartonForm\(\)/, '应提供箱号保存入口', cartonModalHtml);
const functionNames = [
'getFbaCartonStatus', 'getFilteredFbaCartons', 'openFbaCartonForm',
'saveFbaCartonForm', 'exportFbaCartons',
];
const functionSources = {};
functionNames.forEach((name) => {
functionSources[name] = getFunctionSource(name);
if (!functionSources[name]) failures.push(`missing ${name} 函数`);
});
// Keep the edit-state declaration in the same lexical scope as the extracted functions.
const stateNames = ['fbaCartonEditingNo', 'fbaCartonEditingCode', 'fbaCartonEditingId'];
const stateSources = stateNames.map((name) => getDeclarationSource(name)).filter(Boolean);
const declaredStateNames = new Set(stateSources.map((source) => {
const match = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)/.exec(source);
return match && match[1];
}).filter(Boolean));
stateNames.forEach((name) => {
if (!declaredStateNames.has(name)) stateSources.push(`let ${name} = '';`);
});
// Exercise the parser against braces in strings and comments before using app.js.
const parserFixture = "function fixture(){const text='}'; /* } */ return {ok:true}; // }\n}";
assert.match(getFunctionSource('fixture', parserFixture), /return \{ok:true\};/);
assert.equal(getDeclarationSource('fixtureValue', "const fixtureValue = [';'];"), "const fixtureValue = [';'];");
if (!failures.length) {
try {
const elementIds = [
'fbc-form-title', 'fbc-form-carton', 'fbc-form-wh', 'fbc-form-sku',
'fbc-form-cn-name', 'fbc-form-qty', 'fbc-form-weight', 'fbc-form-pallet',
'fbc-form-inbound', 'fbc-form-outbound', 'fbc-form-status', 'fbc-form-remark',
'fbc-carton', 'fbc-wh', 'fbc-status',
];
const elements = Object.fromEntries(elementIds.map((id) => [id, {value: '', textContent: '', readOnly: false}]));
const cartons = [
{cartonNo: 'CTN-IN-001', fbaWh: 'FBA ONT8', sku: 'SKU-A1001', cnName: '在库商品', qty: 24, weight: 12, relatedOutboundNo: '—', createdAt: '2026-07-20 09:20', updatedAt: '2026-08-01 13:50', remark: '保留备注'},
{cartonNo: 'CTN-OUT-002', fbaWh: 'FBA CLT2', sku: 'SKU-B2002', cnName: '已出库商品', qty: 50, weight: 25, relatedOutboundNo: 'FBO20260722001', createdAt: '2026-07-21 09:20', updatedAt: '2026-08-02 13:50', remark: '—'},
{cartonNo: 'CTN-IN-003', fbaWh: 'FBA MEM1', sku: 'SKU-C3003', cnName: '另一在库商品', qty: 40, weight: 30, relatedOutboundNo: '', createdAt: '2026-07-22 09:20', updatedAt: '2026-08-03 13:50', remark: '—'},
];
const modalCalls = [];
const hideCalls = [];
const toasts = [];
const renderCalls = [];
const downloads = [];
const documentStub = {getElementById: (id) => elements[id] || null};
const runtime = new Function(
'document', 'showModal', 'hideModal', 'showToast', 'renderFbaCarton',
'downloadCsvFile', 'csvEscape', 'fbaCartonData',
`${stateSources.join('\n')}\n${Object.values(functionSources).join('\n')}\nreturn {${functionNames.join(',')}};`,
)(
documentStub,
(id) => modalCalls.push(id),
(id) => hideCalls.push(id),
(message, type) => toasts.push([message, type]),
(list) => renderCalls.push((list || []).slice()),
(filename, csv) => downloads.push({filename, csv}),
(value) => `"${String(value == null ? '' : value).replace(/"/g, '""')}"`,
cartons,
);
assert.equal(runtime.getFbaCartonStatus({relatedOutboundNo: '—'}), '在库');
assert.equal(runtime.getFbaCartonStatus({relatedOutboundNo: ''}), '在库');
assert.equal(runtime.getFbaCartonStatus({relatedOutboundNo: 'FBO-001'}), '已出库');
runtime.openFbaCartonForm('CTN-OUT-002');
assert.equal(toasts.at(-1)[1], 'warning');
assert.match(toasts.at(-1)[0], /出库|编辑/);
assert.deepEqual(modalCalls, []);
runtime.openFbaCartonForm('CTN-IN-001');
assert.equal(elements['fbc-form-title'].textContent, '编辑箱号');
assert.equal(elements['fbc-form-carton'].value, 'CTN-IN-001');
assert.equal(elements['fbc-form-carton'].readOnly, true);
assert.deepEqual(modalCalls, ['modal-fba-carton-form']);
const original = cartons[0];
const originalCreatedAt = original.createdAt;
const previousUpdatedAt = original.updatedAt;
const renderCountBeforeSave = renderCalls.length;
elements['fbc-form-cn-name'].value = '更新后的商品';
elements['fbc-form-qty'].value = '88';
elements['fbc-form-weight'].value = '44.5';
runtime.saveFbaCartonForm();
assert.equal(original.cnName, '更新后的商品');
assert.equal(original.qty, 88);
assert.equal(original.weight, 44.5);
assert.equal(original.createdAt, originalCreatedAt);
assert.match(original.updatedAt, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
assert.notEqual(original.updatedAt, previousUpdatedAt);
assert.deepEqual(hideCalls, ['modal-fba-carton-form']);
assert.ok(renderCalls.length > renderCountBeforeSave, '保存后应重绘箱号列表');
runtime.openFbaCartonForm();
elements['fbc-form-carton'].value = 'CTN-IN-001';
elements['fbc-form-wh'].value = 'FBA ONT8';
elements['fbc-form-sku'].value = 'SKU-A1001';
elements['fbc-form-cn-name'].value = '重复箱号';
elements['fbc-form-qty'].value = '1';
elements['fbc-form-weight'].value = '1';
const cartonCountBeforeDuplicate = cartons.length;
const renderCountBeforeDuplicate = renderCalls.length;
runtime.saveFbaCartonForm();
assert.equal(toasts.at(-1)[1], 'warning');
assert.match(toasts.at(-1)[0], /存在|重复/);
assert.equal(hideCalls.length, 1);
assert.equal(cartons.length, cartonCountBeforeDuplicate);
assert.equal(renderCalls.length, renderCountBeforeDuplicate);
elements['fbc-carton'].value = 'OUT-002';
elements['fbc-wh'].value = '';
elements['fbc-status'].value = '';
assert.deepEqual(runtime.getFilteredFbaCartons().map((item) => item.cartonNo), ['CTN-OUT-002']);
elements['fbc-carton'].value = '';
elements['fbc-wh'].value = 'FBA MEM1';
assert.deepEqual(runtime.getFilteredFbaCartons().map((item) => item.cartonNo), ['CTN-IN-003']);
elements['fbc-wh'].value = '';
elements['fbc-status'].value = '已出库';
assert.deepEqual(runtime.getFilteredFbaCartons().map((item) => item.cartonNo), ['CTN-OUT-002']);
elements['fbc-status'].value = '在库';
assert.deepEqual(runtime.getFilteredFbaCartons().map((item) => item.cartonNo), ['CTN-IN-001', 'CTN-IN-003']);
elements['fbc-carton'].value = 'IN';
elements['fbc-wh'].value = 'FBA ONT8';
elements['fbc-status'].value = '在库';
runtime.exportFbaCartons();
assert.equal(downloads.length, 1);
assert.match(downloads[0].filename, /^箱号管理-\d{8}\.csv$/);
assert.equal(downloads[0].csv.charCodeAt(0), 0xfeff);
assert.match(downloads[0].csv, /CTN-IN-001/);
assert.doesNotMatch(downloads[0].csv, /CTN-IN-003/);
assert.doesNotMatch(downloads[0].csv, /CTN-OUT-002/);
elements['fbc-carton'].value = 'NOT-FOUND';
elements['fbc-wh'].value = '';
elements['fbc-status'].value = '';
runtime.exportFbaCartons();
assert.deepEqual(toasts.at(-1), ['暂无可导出数据', 'warning']);
assert.equal(downloads.length, 1);
} catch (error) {
failures.push(`箱号管理运行时行为错误:${error.message}`);
}
}
if (failures.length) {
console.error('FBA_CARTON_MANAGEMENT_CHECK_FAIL');
failures.forEach((failure) => console.error(`- ${failure}`));
process.exitCode = 1;
} else {
console.log('FBA_CARTON_MANAGEMENT_CHECK_PASS');
}