首次提交: OnebotCatalog 项目代码与文档(含 NX 按需生成服务二期、后台、一键启动)
This commit is contained in:
130
tools/e2e-parametric.js
Normal file
130
tools/e2e-parametric.js
Normal file
@@ -0,0 +1,130 @@
|
||||
// 无头对拍: 参数化引擎 (一期 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); });
|
||||
Reference in New Issue
Block a user