Files
OnebotCatalog/tools/e2e-parametric.js

131 lines
7.2 KiB
JavaScript

// 无头对拍: 参数化引擎 (一期 2026-08-25) — 网页 UI 与桌面 selftest 读同一份 parametric-vectors.json
// 用法: node tools\e2e-parametric.js [siteUrl] [vectorsPath]
// 断言: 标准档位编码/下载可用; 非标档位编码+徽标+下载置灰+最近标准档位提示; 非法组合无型号
const fs = require('fs');
let chromium;
try { chromium = require('playwright-core').chromium; }
catch (e) { chromium = require('C:/Users/ruigu/AppData/Local/Temp/nds3dtest/node_modules/playwright-core').chromium; }
const edge = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe';
(async () => {
const url = process.argv[2] || 'http://localhost:8080/';
const vecPath = process.argv[3] ||
'w:/团队文件-TeamCenter/airtac_0602_2026_v11sp5_world_version/airtac_0602_2026_v11sp5_world_version/OnebotCatalog/onebot-data/catalog/parametric-vectors.json';
const vec = JSON.parse(fs.readFileSync(vecPath, 'utf8'));
const browser = await chromium.launch({ executablePath: edge, headless: true });
const page = await browser.newPage({ viewport: { width: 1600, height: 900 } });
const errs = [];
page.on('pageerror', e => errs.push(e.message));
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.tree .series', { timeout: 20000 }); // 等目录树渲染 (服务器冷启动首请求慢)
await page.waitForTimeout(800);
let pass = 0, fail = 0;
const out = [];
const check = (name, ok) => { out.push((ok ? 'PASS ' : 'FAIL ') + name); if (ok) pass++; else fail++; };
// 选中 KC 系列 (首个参数化系列)
const names = await page.$$eval('.tree .series', els => els.map(e => e.textContent.trim()));
const kcIdx = names.findIndex(n => n.indexOf('KC') === 0);
if (kcIdx < 0) { console.log('FAIL: 树中未找到 KC 系列'); await browser.close(); process.exit(1); }
const seriesEls = await page.$$('.tree .series');
await seriesEls[kcIdx].click();
await page.waitForTimeout(500);
for (let ci = 0; ci < vec.cases.length; ci++) {
const c = vec.cases[ci];
const tag = 'case' + (ci + 1) + ' ' + c.series;
const keys = Object.keys(c.params);
const setParam = async (p, v) => {
const sel = await page.$('#sel-' + p);
if (!sel) { check(tag + ' 参数控件缺失 ' + p, false); return; }
const isSelect = (await sel.evaluate(el => el.tagName)) === 'SELECT';
console.log(' [' + tag + '] 设置 ' + p + '=' + JSON.stringify(v) + ' (' + (isSelect ? 'select' : 'input') + ')');
if (isSelect) {
const val = String(v) === '' ? '__EMPTY__' : String(v);
await sel.selectOption(val);
} else {
await sel.fill(String(v));
await sel.dispatchEvent('change');
}
await page.waitForTimeout(200);
};
// 错误用例: 引擎层拒绝已由桌面向量对拍覆盖; 网页端断言 UI 联动拦截行为
if (c.expectError === 'rule') {
// 末参数 = 违背规则的那个: 先设置其余参数, 该选项应已被联动剔除
for (let k = 0; k < keys.length - 1; k++) await setParam(keys[k], c.params[keys[k]]);
await page.waitForTimeout(300);
const lastP = keys[keys.length - 1];
const lastV = String(c.params[lastP]);
const opts = await page.$$eval('#sel-' + lastP + ' option', os => os.map(o => o.value));
check(tag + ' 规则联动剔除非法选项 ' + lastP + '=' + lastV + ' (实际选项: ' + opts.join(',') + ')',
opts.indexOf(lastV) < 0);
continue;
}
if (c.expectError === 'domain') {
for (let k = 0; k < keys.length - 1; k++) await setParam(keys[k], c.params[keys[k]]);
await page.waitForTimeout(300);
const lastP = keys[keys.length - 1];
await setParam(lastP, c.params[lastP]); // 输入越界值 → 处理函数拒绝
await page.waitForTimeout(300);
const title = await page.$eval('#modelTitle', el => el.textContent);
const status = await page.$eval('#status', el => el.textContent);
check(tag + ' 域外输入被拒绝 (标题: ' + title.trim() + ' | 状态: ' + status.trim() + ')',
title.indexOf('还缺') >= 0 || status.indexOf('超出范围') >= 0);
continue;
}
// 依次设置各参数 (枚举用 select; range 用输入框 + change)
for (const p of keys) await setParam(p, c.params[p]);
await page.waitForTimeout(400);
const title = await page.$eval('#modelTitle', el => el.textContent);
if (c.expectError) {
const noModel = title.indexOf('KC') < 0 || title.indexOf('还缺') >= 0 || title.indexOf('无对应型号') >= 0;
check(tag + ' 非法组合不生成型号 (标题: ' + title.trim() + ')', noModel);
continue;
}
const hasCode = title.indexOf(c.expectCode) >= 0;
check(tag + ' 型号编码 ' + c.expectCode + ' (标题: ' + title.trim() + ')', hasCode);
if (c.expectStandard) {
const dlOk = await page.$eval('#btnDownload', el => !el.classList.contains('hidden') && !el.disabled);
check(tag + ' 标准档位下载可用', dlOk);
} else {
const badge = await page.$eval('#modelTitle', el => !!el.querySelector('.nonstd-badge'));
// 生成服务未配置 → 置灰(一期过渡态); 已配置 → 在线生成按钮可用 (两种都是正确状态)
const dl = await page.$eval('#btnDownload', el => ({ hidden: el.classList.contains('hidden'), disabled: el.disabled, text: el.textContent }));
const dlOk = !dl.hidden && ((dl.disabled && dl.text.indexOf('下载 STEP') >= 0) ||
(!dl.disabled && dl.text.indexOf('在线生成') >= 0));
check(tag + ' 非标徽标显示 + 下载状态正确 (按钮: ' + dl.text + (dl.disabled ? '/disabled' : '/enabled') + ')', badge && dlOk);
if (c.expectNearest) {
const hint = await page.$eval('#viewer3dHint', el => el.textContent);
check(tag + ' 最近标准档位提示 ' + c.expectNearest + ' (提示: ' + hint.trim() + ')',
hint.indexOf(c.expectNearest) >= 0);
}
}
}
// 非标加入购物车 → 角标 + 抽屉标注 (与桌面语义一致)
await page.fill('#sel-stroke', '87');
await page.dispatchEvent('#sel-stroke', 'change');
await page.waitForTimeout(400);
await page.click('#btnAddCart');
await page.waitForTimeout(300);
await page.click('#btnCart');
await page.waitForTimeout(300);
const sub = await page.$eval('#cartList .cart-item .ci-sub', el => el.textContent);
check('购物车非标条目标注 (内容: ' + sub.trim() + ')', sub.indexOf('非标') >= 0);
await page.click('#cartClose');
await page.waitForTimeout(200);
// 标准档位回归: 换回 stroke=75 → 下载恢复可用
await page.fill('#sel-stroke', '75');
await page.dispatchEvent('#sel-stroke', 'change');
await page.waitForTimeout(400);
const dlRe = await page.$eval('#btnDownload', el => !el.disabled && el.getAttribute('href').indexOf('.step') >= 0);
check('标准档位回归: 下载恢复可用 (href=' + (await page.$eval('#btnDownload', el => el.getAttribute('href'))) + ')', dlRe);
console.log(out.join('\n'));
console.log('结果: ' + pass + ' 通过, ' + fail + ' 失败 | 页面错误: ' + (errs.length ? errs.join(' | ') : '无'));
await browser.close();
process.exit(fail ? 1 : 0);
})().catch(e => { console.error('FAIL:', e.message); process.exit(1); });