242 lines
7.4 KiB
JavaScript
242 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
|
|
const prototypePath = path.resolve(__dirname, '..', '货代SaaS平台原型.html');
|
|
const failures = [];
|
|
let html = '';
|
|
|
|
try {
|
|
html = fs.readFileSync(prototypePath, 'utf8');
|
|
} catch (error) {
|
|
failures.push(`无法读取原型文件:${error.message}`);
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function requireMatch(pattern, label, source = html) {
|
|
if (!pattern.test(source)) failures.push(`missing ${label}`);
|
|
}
|
|
|
|
function findClosingDelimiter(source, openingIndex, opening, closing) {
|
|
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 === opening) depth += 1;
|
|
if (character === closing) {
|
|
depth -= 1;
|
|
if (depth === 0) return index;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
function getPageSource(pageId) {
|
|
const openingTag = new RegExp(
|
|
`<div\\b(?=[^>]*\\bclass=["'][^"']*\\bpage\\b)(?=[^>]*\\bid=["']page-${escapeRegExp(pageId)}["'])[^>]*>`,
|
|
'i',
|
|
).exec(html);
|
|
if (!openingTag) return '';
|
|
|
|
const remainder = html.slice(openingTag.index + openingTag[0].length);
|
|
const nextPage = /<div\b(?=[^>]*\bclass=["'][^"']*\bpage\b)(?=[^>]*\bid=["']page-[^"']+["'])[^>]*>/i.exec(remainder);
|
|
const end = nextPage
|
|
? openingTag.index + openingTag[0].length + nextPage.index
|
|
: html.length;
|
|
return html.slice(openingTag.index, end);
|
|
}
|
|
|
|
function getFunctionSource(functionName) {
|
|
const declaration = new RegExp(
|
|
`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`,
|
|
).exec(html);
|
|
if (!declaration) return '';
|
|
|
|
const openingBrace = html.indexOf('{', declaration.index);
|
|
const closingBrace = findClosingDelimiter(html, openingBrace, '{', '}');
|
|
if (closingBrace < 0) return '';
|
|
return html.slice(declaration.index, closingBrace + 1);
|
|
}
|
|
|
|
function getArrayValue(variableName) {
|
|
const declaration = new RegExp(
|
|
`(?:const|let|var)\\s+${escapeRegExp(variableName)}\\s*=\\s*\\[`,
|
|
).exec(html);
|
|
if (!declaration) return null;
|
|
|
|
const openingBracket = html.indexOf('[', declaration.index);
|
|
const closingBracket = findClosingDelimiter(html, openingBracket, '[', ']');
|
|
if (closingBracket < 0) return null;
|
|
|
|
try {
|
|
return vm.runInNewContext(html.slice(openingBracket, closingBracket + 1), Object.create(null));
|
|
} catch (error) {
|
|
failures.push(`fbaProductData 无法解析:${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const pageSource = getPageSource('fba-product-detail');
|
|
if (!pageSource) failures.push('missing 页面节点 page-fba-product-detail');
|
|
requireMatch(/产品详情/, '产品详情页面标题', pageSource);
|
|
for (const id of ['fbd-product-sku', 'fbd-product-category', 'fbd-product-content']) {
|
|
requireMatch(new RegExp(`\\bid=["']${escapeRegExp(id)}["']`), `产品详情页面节点 ${id}`, pageSource);
|
|
}
|
|
requireMatch(/onclick=["']backToFbaProducts\(\)["']/, '产品详情返回入口 backToFbaProducts()', pageSource);
|
|
|
|
const openSource = getFunctionSource('openFbaProductDetail');
|
|
if (!openSource) failures.push('missing openFbaProductDetail 打开函数');
|
|
requireMatch(
|
|
/\.find\s*\(\s*\(?\s*item\s*\)?\s*=>\s*item\.sku\s*===\s*sku\s*\)/,
|
|
'openFbaProductDetail 按 item.sku===sku 查找产品',
|
|
openSource,
|
|
);
|
|
requireMatch(
|
|
/showToast\s*\(\s*['"]未找到产品记录['"]\s*,\s*['"]error['"]\s*\)/,
|
|
"openFbaProductDetail 未找到时 showToast('未找到产品记录','error')",
|
|
openSource,
|
|
);
|
|
requireMatch(
|
|
/showFbaReadOnlyDetail\s*\(\s*['"]fba-product-detail['"]\s*,/,
|
|
"openFbaProductDetail 调用 showFbaReadOnlyDetail('fba-product-detail', ...)",
|
|
openSource,
|
|
);
|
|
|
|
for (const label of [
|
|
'基础信息',
|
|
'商品SKU',
|
|
'中文品名',
|
|
'英文品名',
|
|
'产品分类',
|
|
'包装信息',
|
|
'长(cm)',
|
|
'宽(cm)',
|
|
'高(cm)',
|
|
'重量(kg)',
|
|
'包装体积(m³)',
|
|
'其他信息',
|
|
'创建时间',
|
|
'更新时间',
|
|
'备注',
|
|
]) {
|
|
requireMatch(new RegExp(escapeRegExp(label)), `openFbaProductDetail 详情文案“${label}”`, openSource);
|
|
}
|
|
|
|
const rendererSource = getFunctionSource('renderFbaProduct');
|
|
requireMatch(
|
|
/openFbaProductDetail\(\$\{lmoEsc\(JSON\.stringify\(d\.sku\)\)\}\)/,
|
|
'renderFbaProduct 使用 JSON.stringify 和 lmoEsc 安全传入 SKU',
|
|
rendererSource,
|
|
);
|
|
|
|
const backSource = getFunctionSource('backToFbaProducts');
|
|
if (!backSource) failures.push('missing backToFbaProducts 返回函数');
|
|
requireMatch(
|
|
/showExistingFbaList\s*\(\s*['"]fba-product['"]\s*\)/,
|
|
"backToFbaProducts 调用 showExistingFbaList('fba-product')",
|
|
backSource,
|
|
);
|
|
if (/\bfbprReset\s*\(/.test(backSource)) {
|
|
failures.push('backToFbaProducts 不应调用 fbprReset');
|
|
}
|
|
if (/\brenderFbaProduct\s*\(/.test(backSource)) {
|
|
failures.push('backToFbaProducts 不应调用 renderFbaProduct');
|
|
}
|
|
|
|
const productData = getArrayValue('fbaProductData');
|
|
if (!Array.isArray(productData) || productData.length === 0) {
|
|
failures.push('missing 非空 fbaProductData 数据');
|
|
} else {
|
|
for (const field of ['createdAt', 'updatedAt', 'remark']) {
|
|
if (!productData.every((item) => Object.prototype.hasOwnProperty.call(item, field))) {
|
|
failures.push(`fbaProductData 每条记录缺少 ${field} 字段`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const volumeSource = getFunctionSource('formatFbaProductVolume');
|
|
if (!volumeSource) {
|
|
failures.push('missing formatFbaProductVolume 函数');
|
|
} else {
|
|
let formatVolume;
|
|
try {
|
|
formatVolume = vm.runInNewContext(`(${volumeSource})`, Object.create(null));
|
|
} catch (error) {
|
|
failures.push(`formatFbaProductVolume 无法在 node:vm 中执行:${error.message}`);
|
|
}
|
|
|
|
if (typeof formatVolume === 'function') {
|
|
try {
|
|
if (formatVolume({ len: 30, wid: 20, height: 15 }) !== '0.0090') {
|
|
failures.push("formatFbaProductVolume({len:30,wid:20,height:15}) 应返回 '0.0090'");
|
|
}
|
|
|
|
const emptyDimensionCases = [
|
|
{ wid: 20, height: 15 },
|
|
{ len: 30, height: 15 },
|
|
{ len: 30, wid: 20 },
|
|
{ len: 0, wid: 20, height: 15 },
|
|
{ len: 30, wid: 0, height: 15 },
|
|
{ len: 30, wid: 20, height: 0 },
|
|
];
|
|
if (emptyDimensionCases.some((product) => formatVolume(product) !== '—')) {
|
|
failures.push("formatFbaProductVolume 缺失或为零的尺寸应返回 '—'");
|
|
}
|
|
} catch (error) {
|
|
failures.push(`formatFbaProductVolume 调用失败:${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (failures.length) {
|
|
console.error('FBA_PRODUCT_DETAIL_CHECK_FAIL');
|
|
failures.forEach((failure) => console.error(`- ${failure}`));
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log('FBA_PRODUCT_DETAIL_CHECK_PASS');
|
|
}
|