79 lines
3.6 KiB
JavaScript
79 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* run-regression.mjs — headless 回归 runner(CI / 本地均可)
|
||
*
|
||
* 前置:仓库根目录起 dev-server(node site/dev-server.js)
|
||
* 依赖:npm i playwright(或全局已装)
|
||
* 运行:node tools/run-regression.mjs
|
||
* 产出:tests/report.json(首页「测试通过率」数据源)+ tests/report-junit.xml
|
||
*/
|
||
import { writeFileSync } from 'fs';
|
||
|
||
const BASE = process.env.REG_BASE || 'http://127.0.0.1:3311';
|
||
const CONC = 6;
|
||
|
||
const { chromium } = await import('playwright');
|
||
const browser = await chromium.launch();
|
||
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
||
|
||
const data = await (await fetch(`${BASE}/site/data.json`)).json();
|
||
const slugs = data.components.map(c => c.slug);
|
||
const results = [];
|
||
|
||
async function runOne(browser, slug) {
|
||
const p = await browser.newPage({ viewport: { width: 1200, height: 800 } });
|
||
try {
|
||
await p.goto(`${BASE}/tests/${slug}.html`, { waitUntil: 'load', timeout: 15000 });
|
||
const btn = p.locator('#run-asserts');
|
||
if (await btn.count()) await btn.click();
|
||
await p.waitForTimeout(200);
|
||
const r = await p.evaluate(() => {
|
||
const num = s => parseInt(String(s || '0').replace(/\D/g, '')) || 0;
|
||
const t = id => { const n = document.getElementById(id); return n ? n.textContent : '0'; };
|
||
const failures = [...document.querySelectorAll('#result-list .t-row')]
|
||
.filter(li => li.querySelector('.t-pill')?.classList.contains('fail'))
|
||
.map(li => li.querySelector('.id')?.textContent || '?')
|
||
.slice(0, 8);
|
||
return { total: num(t('sum-total')), pass: num(t('sum-pass')), fail: num(t('sum-fail')), skip: num(t('sum-skip')), failures };
|
||
});
|
||
results.push({ slug, ...r });
|
||
} catch (e) {
|
||
results.push({ slug, total: 0, pass: 0, fail: 1, skip: 0, failures: ['page-error'] });
|
||
} finally {
|
||
await p.close();
|
||
}
|
||
}
|
||
|
||
for (let i = 0; i < slugs.length; i += CONC) {
|
||
await Promise.all(slugs.slice(i, i + CONC).map(s => runOne(browser, s)));
|
||
}
|
||
await browser.close();
|
||
|
||
const totalAssert = results.reduce((s, r) => s + r.total, 0);
|
||
const passAssert = results.reduce((s, r) => s + r.pass, 0);
|
||
const failAssert = results.reduce((s, r) => s + r.fail, 0);
|
||
const skipAssert = results.reduce((s, r) => s + r.skip, 0);
|
||
const pagesAllPass = results.filter(r => r.fail === 0 && r.total > 0).length;
|
||
const report = {
|
||
generated: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
||
version: data.meta.version,
|
||
pages: results.length,
|
||
pagesAllPass,
|
||
passRate: totalAssert ? Math.round(passAssert / totalAssert * 1000) / 10 : 0,
|
||
assertions: { total: totalAssert, pass: passAssert, fail: failAssert, skip: skipAssert },
|
||
failedPages: results.filter(r => r.fail > 0).map(r => ({ slug: r.slug, fail: r.fail, failures: r.failures }))
|
||
};
|
||
writeFileSync('tests/report.json', JSON.stringify(report, null, 2) + '\n');
|
||
|
||
/* JUnit XML(供 CI 展示) */
|
||
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
||
const cases = results.map(r =>
|
||
` <testcase name="tests/${r.slug}" time="0">` +
|
||
(r.fail > 0 ? `<failure message="${esc((r.failures || []).join('; '))}">${r.fail} failed</failure>` : '') +
|
||
`</testcase>`).join('\n');
|
||
writeFileSync('tests/report-junit.xml',
|
||
`<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="aurora-admin tests" tests="${results.length}" failures="${results.filter(r => r.fail > 0).length}">\n${cases}\n</testsuite>\n`);
|
||
|
||
console.log(`passRate ${report.passRate}% | pages ${results.length} (all-pass ${pagesAllPass}) | assertions ${passAssert}/${totalAssert}`);
|
||
console.log('written: tests/report.json, tests/report-junit.xml');
|