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

314 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 read = file => fs.readFileSync(path.join(root, file), 'utf8');
const general = read('prototype-src/modules/10-general.html');
const overlays = read('prototype-src/overlays.html');
const shell = read('prototype-src/shell.html');
const app = read('prototype-src/app.js');
const agents = read('AGENTS.md');
const manifest = JSON.parse(read('prototype-src/manifest.json'));
function count(source, pattern) {
return (source.match(pattern) || []).length;
}
function pageFragment(source, pageId) {
const marker = `<div class="page" id="page-${pageId}">`;
const start = source.indexOf(marker);
assert.ok(start >= 0, `缺少页面 page-${pageId}`);
const next = source.indexOf('<div class="page" id="page-', start + marker.length);
return source.slice(start, next < 0 ? source.length : next);
}
function headers(page) {
return Array.from(page.matchAll(/<th>([^<]+)<\/th>/g), match => match[1]);
}
function tabMarkup(page, targetPage) {
const match = new RegExp(`<button\\b[^>]*switchWarehousePage\\('${targetPage}',\\s*this\\)[^>]*>`, 'g').exec(page);
assert.ok(match, `缺少跳转至 ${targetPage} 的仓库页签`);
return match[0];
}
function extractBalanced(source, start, open, close) {
let depth = 0;
let quote = '';
for (let index = start; index < source.length; index += 1) {
const char = source[index];
if (quote) {
if (char === '\\') index += 1;
else if (char === quote) quote = '';
continue;
}
if (char === '\'' || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === open) depth += 1;
if (char === close) {
depth -= 1;
if (depth === 0) return index;
}
}
throw new Error(`无法闭合 ${open}${close} 片段`);
}
function extractArrayDeclaration(source, name) {
const match = new RegExp(`(?:const|let|var)\\s+${name}\\s*=`).exec(source);
assert.ok(match, `app.js 缺少 ${name} 数据数组`);
const open = source.indexOf('[', match.index + match[0].length);
const end = extractBalanced(source, open, '[', ']');
return source.slice(match.index, source.indexOf(';', end) + 1);
}
function extractVariableDeclaration(source, name) {
const match = new RegExp(`(?:const|let|var)\\s+${name}\\s*=`).exec(source);
assert.ok(match, `app.js 缺少 ${name} 声明`);
const open = source.indexOf('{', match.index + match[0].length);
const end = extractBalanced(source, open, '{', '}');
return source.slice(match.index, source.indexOf(';', end) + 1);
}
function extractFunction(source, name) {
const match = new RegExp(`function\\s+${name}\\s*\\(`).exec(source);
assert.ok(match, `app.js 缺少函数 ${name}`);
const open = source.indexOf('{', match.index + match[0].length);
const end = extractBalanced(source, open, '{', '}');
return source.slice(match.index, end + 1);
}
function makeTbody(eventLog, name) {
let html = '';
const element = {};
Object.defineProperty(element, 'innerHTML', {
get: () => html,
set: value => {
html = String(value);
eventLog.push(['render', name, html]);
}
});
return element;
}
const ownedPage = pageFragment(general, 'gen-owned-warehouse');
const fbaPage = pageFragment(general, 'gen-fba');
const partnerPage = pageFragment(general, 'gen-partner-warehouse');
const pageCases = [
['自有仓', ownedPage, 'gen-owned-warehouse'],
['FBA仓库', fbaPage, 'gen-fba'],
['合作仓', partnerPage, 'gen-partner-warehouse']
];
for (const [name, page, activePage] of pageCases) {
for (const target of ['gen-owned-warehouse', 'gen-fba', 'gen-partner-warehouse']) {
assert.match(page, new RegExp(`switchWarehousePage\\('${target}',\\s*this\\)`), `${name}缺少 ${target} 页签`);
const tab = tabMarkup(page, target);
assert.match(tab, new RegExp(`aria-selected="${target === activePage ? 'true' : 'false'}"`), `${name}页签 aria-selected 错误`);
if (target === activePage) assert.match(tab, /class="[^"]*active[^"]*"/, `${name}当前页签未高亮`);
else assert.doesNotMatch(tab, /class="[^"]*active[^"]*"/, `${name}非当前页签错误高亮`);
}
}
assert.deepEqual(headers(ownedPage), ['自有仓名称', '仓库地址', '城市', '州/省', '邮编', '国家', '状态', '操作']);
assert.doesNotMatch(ownedPage, /距离(MILES)|data-owned-warehouse-select/, '自有仓页面不得维护距离');
assert.match(ownedPage, /openWarehouseAdd\('owned'\)/, '自有仓新增入口未指定 owned 目标');
const targetHeaders = ['仓库代码', '仓库地址', '城市', '州/省', '邮编', '国家', '距离(MILES)', '状态', '操作'];
assert.deepEqual(headers(fbaPage), targetHeaders);
assert.deepEqual(headers(partnerPage), targetHeaders);
assert.match(fbaPage, /id="gen-fba-owned-warehouse"[^>]*data-owned-warehouse-select/);
assert.match(partnerPage, /id="gen-partner-warehouse-owned-warehouse"[^>]*data-owned-warehouse-select/);
const sidebarStart = shell.indexOf('<aside');
const sidebarEnd = shell.indexOf('</aside>', sidebarStart);
const sidebar = shell.slice(sidebarStart, sidebarEnd);
assert.equal(count(sidebar, /switchPage\('gen-owned-warehouse',this\)">仓库管理<\/div>/g), 1, '侧栏仓库管理必须默认进入自有仓');
assert.doesNotMatch(sidebar, /sub-menu-item[^>]+gen-(?:fba|partner-warehouse)/, '侧栏不得为 FBA 或合作仓增加独立入口');
assert.match(shell, /#page-gen-owned-warehouse \.warehouse-table-wrap[^{}]*,[^{}]*#page-gen-fba \.warehouse-table-wrap[^{}]*,[^{}]*#page-gen-partner-warehouse \.warehouse-table-wrap/);
const generalModule = manifest.modules.find(module => module.file === 'modules/10-general.html');
assert.ok(generalModule, 'manifest 缺少通用资料模块');
for (const list of [manifest.pageOrder, generalModule.pages]) {
assert.equal(list.filter(id => id === 'gen-owned-warehouse').length, 1, 'gen-owned-warehouse 必须只登记一次');
const ownedIndex = list.indexOf('gen-owned-warehouse');
assert.equal(list[ownedIndex + 1], 'gen-fba', '自有仓后必须紧接 FBA仓库');
assert.equal(list[ownedIndex + 2], 'gen-partner-warehouse', 'FBA仓库后必须紧接合作仓');
}
assert.match(agents, /仓库(`gen-owned-warehouse`、`gen-fba`、`gen-partner-warehouse`)/);
const modalStart = overlays.indexOf('<div class="modal-overlay" id="modal-fba-add">');
assert.ok(modalStart >= 0, '共享浮层中缺少仓库表单弹窗');
const nextModalStart = overlays.indexOf('<div class="modal-overlay"', modalStart + 1);
const warehouseModal = overlays.slice(modalStart, nextModalStart < 0 ? overlays.length : nextModalStart);
assert.match(warehouseModal, /id="warehouse-form-title"/);
assert.match(warehouseModal, /id="warehouse-owned-name-row"/);
assert.match(warehouseModal, /id="warehouse-code-row"/);
assert.match(warehouseModal, /id="warehouse-distance-row"[^>]*style="display:none"/);
assert.match(warehouseModal, /id="warehouse-distance-origin"/);
assert.match(warehouseModal, /id="warehouse-distance-miles"[^>]*type="number"[^>]*min="0"/);
assert.match(warehouseModal, /onclick="saveWarehouseForm\(\)"/);
const eventLog = [];
const showModalCalls = [];
const hideModalCalls = [];
const toastCalls = [];
const ownedTbody = makeTbody(eventLog, 'owned');
const fbaTbody = makeTbody(eventLog, 'fba');
const partnerTbody = makeTbody(eventLog, 'partner');
const fbaOwnedSelect = {value: '洛杉矶仓', innerHTML: ''};
const partnerOwnedSelect = {value: '洛杉矶仓', innerHTML: ''};
const elements = {
'gen-owned-warehouse-keyword': {value: ''},
'gen-owned-warehouse-country': {value: ''},
'tbody-gen-owned-warehouse': ownedTbody,
'gen-fba-owned-warehouse': fbaOwnedSelect,
'gen-fba-keyword': {value: ''},
'gen-fba-country': {value: ''},
'tbody-gen-fba': fbaTbody,
'gen-partner-warehouse-owned-warehouse': partnerOwnedSelect,
'gen-partner-warehouse-keyword': {value: ''},
'gen-partner-warehouse-country': {value: ''},
'tbody-gen-partner-warehouse': partnerTbody,
'warehouse-form-title': {textContent: ''},
'warehouse-owned-name-row': {style: {display: 'none'}},
'warehouse-code-row': {style: {display: ''}},
'warehouse-distance-row': {style: {display: 'none'}},
'warehouse-distance-origin': {textContent: ''},
'warehouse-owned-name': {value: ''},
'fba-add-country': {value: ''},
'fba-add-code': {value: ''},
'fba-add-city': {value: ''},
'fba-add-state': {value: ''},
'fba-add-addr': {value: ''},
'fba-add-zip': {value: ''},
'fba-add-tax-rate': {value: ''},
'fba-add-no-tax-rate': {value: ''},
'fba-add-inner-code': {value: ''},
'warehouse-distance-miles': {value: ''}
};
const context = {
document: {
getElementById: id => elements[id] || null,
querySelectorAll: selector => selector === '[data-owned-warehouse-select]' ? [fbaOwnedSelect, partnerOwnedSelect] : []
},
showModal: id => { showModalCalls.push(id); eventLog.push(['show', id]); },
hideModal: id => { hideModalCalls.push(id); eventLog.push(['hide', id]); },
showToast: (message, type) => { toastCalls.push([message, type]); eventLog.push(['toast', message, type]); }
};
const functionNames = [
'normalizeWarehouseTarget', 'getWarehousePageConfig', 'getNextWarehouseId', 'getNextOwnedWarehouseId',
'resetWarehouseForm', 'syncOwnedWarehouseOptions', 'setWarehouseFormFieldVisibility',
'filterWarehouseRows', 'formatGenFbaDistance', 'renderWarehousePage', 'renderOwnedWarehouse',
'renderGenFba', 'renderPartnerWarehouse', 'openWarehouseAdd', 'openWarehouseEdit', 'saveWarehouseForm'
];
const vmSource = [
extractArrayDeclaration(app, 'genOwnedWarehouseData'),
extractArrayDeclaration(app, 'genFbaData'),
extractArrayDeclaration(app, 'genPartnerWarehouseData'),
extractVariableDeclaration(app, 'warehouseFormState'),
...functionNames.map(name => extractFunction(app, name)),
'this.api={genOwnedWarehouseData,genFbaData,genPartnerWarehouseData,warehouseFormState,renderOwnedWarehouse,renderGenFba,renderPartnerWarehouse,openWarehouseAdd,openWarehouseEdit,saveWarehouseForm,syncOwnedWarehouseOptions};'
].join('\n');
vm.runInNewContext(vmSource, context);
assert.deepEqual(Array.from(context.api.genOwnedWarehouseData, row => row.name), ['洛杉矶仓', '纽约仓']);
for (const row of context.api.genOwnedWarehouseData) {
assert.equal(Object.hasOwn(row, 'distanceMiles'), false, '自有仓记录不得包含距离数据');
}
context.api.syncOwnedWarehouseOptions();
assert.match(fbaOwnedSelect.innerHTML, /洛杉矶仓/);
assert.match(fbaOwnedSelect.innerHTML, /纽约仓/);
context.api.renderOwnedWarehouse();
assert.match(ownedTbody.innerHTML, /洛杉矶仓/);
assert.doesNotMatch(ownedTbody.innerHTML, /MILES|distanceMiles/);
context.api.openWarehouseEdit('owned', 'OWN001');
assert.equal(elements['warehouse-owned-name-row'].style.display, '');
assert.equal(elements['warehouse-code-row'].style.display, 'none');
assert.equal(elements['warehouse-distance-row'].style.display, 'none');
assert.equal(elements['warehouse-owned-name'].value, '洛杉矶仓');
fbaOwnedSelect.value = '洛杉矶仓';
context.api.openWarehouseEdit('fba', 'FBA001');
assert.equal(elements['warehouse-distance-row'].style.display, '');
assert.equal(elements['warehouse-distance-origin'].textContent, '洛杉矶仓');
assert.equal(elements['warehouse-distance-miles'].value, '22');
elements['warehouse-distance-miles'].value = '31.5';
context.api.saveWarehouseForm();
assert.equal(context.api.genFbaData[0].distanceMiles['洛杉矶仓'], 31.5);
assert.equal(context.api.genFbaData[0].distanceMiles['纽约仓'], 2800);
assert.equal(context.api.genFbaData[1].distanceMiles['洛杉矶仓'], 73);
partnerOwnedSelect.value = '纽约仓';
context.api.openWarehouseEdit('partner', 'FBA008');
assert.equal(elements['warehouse-distance-origin'].textContent, '纽约仓');
assert.equal(elements['warehouse-distance-miles'].value, '2770');
elements['warehouse-distance-miles'].value = '';
context.api.saveWarehouseForm();
assert.equal(Object.hasOwn(context.api.genPartnerWarehouseData[0].distanceMiles, '纽约仓'), false);
assert.equal(context.api.genPartnerWarehouseData[0].distanceMiles['洛杉矶仓'], 48);
partnerOwnedSelect.value = '洛杉矶仓';
context.api.openWarehouseEdit('partner', 'FBA008');
const hideCountBeforeInvalid = hideModalCalls.length;
elements['warehouse-distance-miles'].value = '-1';
context.api.saveWarehouseForm();
assert.equal(context.api.genPartnerWarehouseData[0].distanceMiles['洛杉矶仓'], 48);
assert.equal(hideModalCalls.length, hideCountBeforeInvalid, '非法距离不得关闭弹窗');
assert.deepEqual(toastCalls.at(-1), ['距离必须为非负数', 'warning']);
fbaOwnedSelect.value = '洛杉矶仓';
context.api.openWarehouseAdd('fba');
assert.equal(elements['warehouse-distance-row'].style.display, '', 'FBA 新增必须显示距离字段');
assert.equal(elements['warehouse-distance-origin'].textContent, '洛杉矶仓');
elements['fba-add-country'].value = '美国';
elements['fba-add-code'].value = 'SEA8';
elements['fba-add-city'].value = 'Seattle';
elements['fba-add-state'].value = 'WA';
elements['fba-add-addr'].value = '100 Demo St';
elements['fba-add-zip'].value = '98101';
elements['warehouse-distance-miles'].value = '1135.5';
context.api.saveWarehouseForm();
const addedFba = context.api.genFbaData.at(-1);
assert.equal(addedFba.code, 'SEA8');
assert.deepEqual(Object.keys(addedFba.distanceMiles), ['洛杉矶仓']);
assert.equal(addedFba.distanceMiles['洛杉矶仓'], 1135.5);
partnerOwnedSelect.value = '纽约仓';
context.api.openWarehouseAdd('partner');
assert.equal(elements['warehouse-distance-row'].style.display, '', '合作仓新增必须显示距离字段');
assert.equal(elements['warehouse-distance-origin'].textContent, '纽约仓');
elements['fba-add-country'].value = '美国';
elements['fba-add-code'].value = 'EWR-DEMO';
elements['fba-add-city'].value = 'Newark';
elements['fba-add-state'].value = 'NJ';
elements['fba-add-addr'].value = '200 Demo Ave';
elements['fba-add-zip'].value = '07101';
elements['warehouse-distance-miles'].value = '18';
context.api.saveWarehouseForm();
const addedPartner = context.api.genPartnerWarehouseData.at(-1);
assert.equal(addedPartner.code, 'EWR-DEMO');
assert.deepEqual(Object.keys(addedPartner.distanceMiles), ['纽约仓']);
assert.equal(addedPartner.distanceMiles['纽约仓'], 18);
context.api.openWarehouseAdd('owned');
elements['warehouse-owned-name'].value = '芝加哥仓';
elements['fba-add-country'].value = '美国';
elements['fba-add-city'].value = 'Chicago';
elements['fba-add-state'].value = 'IL';
elements['fba-add-addr'].value = '100 Demo Warehouse Rd';
elements['fba-add-zip'].value = '60601';
context.api.saveWarehouseForm();
assert.equal(context.api.genOwnedWarehouseData.length, 3);
assert.equal(context.api.genOwnedWarehouseData.at(-1).name, '芝加哥仓');
assert.equal(Object.hasOwn(context.api.genOwnedWarehouseData.at(-1), 'distanceMiles'), false);
assert.match(fbaOwnedSelect.innerHTML, /芝加哥仓/);
assert.ok(showModalCalls.length >= 4, '新增编辑流程必须打开共享弹窗');
console.log('GEN_FBA_OWNED_DISTANCE_CHECK_PASS');