Files
aurora-admin/tests/_collect.html
T
aurora-admin 8934ebb440 feat: P2 分发 + P3 CI(含三个实测发现的缺陷修复)
执行 ROADMAP 的 S1-P2(npm 分发)与 S1-P3(CI 回归)。

P3 · CI 回归流水线
- .github/workflows/regression.yml:push/PR 自动跑回归,失败阻断
  含 playwright 浏览器缓存、零依赖自检、服务就绪轮询、报告上传、
  GitHub Step Summary 输出
- tools/run-regression.mjs 重构:
  · 路径基于脚本位置(不再依赖 cwd,CI 更稳)
  · 环境自检:服务未起退出码 2、playwright 缺失退出码 2
  · 断言超时后重试一次(吸取 _collect.html 的偶发教训)
  · 超时与断言失败分开统计(timedOut 字段)
  · 失败时打印明细并 exit 1,全部通过 exit 0

P2 · npm 分发
- package.json:aurora-admin-design,dependencies 为空(零运行时依赖铁律)
  playwright 放 devDependencies(CI 专用)
- tools/build-dist.mjs:零依赖构建脚本,产出 dist/
  tokens/{tokens.css,tokens.json(DTCG),figma.json}
  components/{<slug>.css ×79, index.css 聚合, <slug>.html ×79}
  manifest.json(含 SHA 校验和) + README.md
- .npmignore / .gitignore 更新(dist/ node_modules/ 不入库)
- npm pack 实测:115.9 KB 压缩 / 566 KB 解压 / 167 文件 / 无 site|tests 泄漏

实测发现的三个缺陷(全部修复)
1. package.json 加 "type": "module" 会让 site/dev-server.js(CJS)崩溃
   —— 移除该字段,靠 .mjs 扩展名区分模块系统
2. dist 里 HTML 引用 ../tokens/colors_and_type.css 但产物是 tokens.css
   —— 路径对但文件不存在,产物是坏的。重写全部引用规则
   (含 index.css 的 @import 从 ./tokens/ 改为 ../tokens/)
3. 断言总数不确定:icon-contrast 是条件性推入(依赖渲染时机),
   导致同一页不同次运行总数不同(960 vs 961)
   —— 改为恒定输出一条(无问题=pass,有问题=skip),
   断言数从 960 稳定为 1038,连跑两次完全一致
4. 顺带修 na 与 skip 重复计算(na = total - denom,但 skip 已是同一批)
   —— 统一为 na = skip,算术自洽:pass+fail+skip = total

验证
- runner:passRate 100% | 79 页全通过 | 1003/1003 | N/A 35 | exit 0
- collector:完全同口径(total 1038 / pass 1003 / fail 0 / skip 35)
- dist:79/79 页面浏览器实测正常(令牌解析、无加载失败)
- 连跑两次数字完全一致(可复现)
2026-09-11 23:13:25 +08:00

194 lines
8.3 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>批量回归收集器 — Aurora Admin</title>
<link rel="stylesheet" href="../.design_library/aurora-admin/colors_and_type.css">
<link rel="stylesheet" href="../site/style.css">
<style>
body { margin: 0; font-family: var(--au-font-family); color: var(--au-color-text-body); background: var(--au-color-page-bg); }
.shell { max-width: 1240px; margin: 0 auto; padding: 24px; }
.shell h1 { margin: 0 0 4px; font-size: 24px; color: var(--au-color-text-title); }
.shell h1 a { font-size: 13px; margin-left: 8px; color: var(--au-color-brand); }
.shell .desc { margin: 0 0 16px; color: var(--au-color-text-secondary); font-size: 14px; }
.summary { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
.pill { height: 28px; padding: 0 12px; border-radius: var(--au-radius-base); background: var(--au-color-card-bg); border: 1px solid var(--au-color-border); display: inline-flex; align-items: center; font-family: var(--au-font-family-num); font-size: 13px; color: var(--au-color-text-body); }
.pill strong { margin-right: 4px; color: var(--au-color-brand); }
.pill.pass { background: rgba(82,196,26,.12); color: var(--au-color-success); border-color: transparent; }
.pill.fail { background: rgba(245,34,45,.12); color: var(--au-color-error); border-color: transparent; }
#frames { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
#frames iframe { width: 100%; height: 320px; border: 1px solid var(--au-color-border); border-radius: var(--au-radius-base); background: #fff; }
pre#out { background: var(--au-color-card-bg); border: 1px solid var(--au-color-border); border-radius: var(--au-radius-base); padding: 12px; font-size: 12px; max-height: 320px; overflow: auto; font-family: var(--au-font-family-num); }
</style>
</head>
<body>
<div class="shell">
<h1>批量回归收集器 <a href="index.html">← 测试总览</a></h1>
<p class="desc">顺序/并发把全部测试页装入同源 iframe,收集 <code>_runtime.js</code> 在帧内跑出的断言汇总,生成与 <code>tools/run-regression.mjs</code> 同结构的报告对象(<code>window.__aaCollectResult</code>)。零 npm 依赖。</p>
<div class="summary">
<span class="pill">进度 <strong id="prog">0 / 0</strong></span>
<span class="pill" id="rate">通过率 —</span>
<span class="pill" id="state">启动中…</span>
</div>
<div id="frames"></div>
<pre id="out">…</pre>
</div>
<script>
(function () {
'use strict';
var CONC = 4;
var TIMEOUT = 12000;
var bySlug = {};
var slugs = [];
var done = 0;
var queue = [];
var busy = 0;
var finished = false;
var host = document.getElementById('frames');
var progEl = document.getElementById('prog');
var rateEl = document.getElementById('rate');
var stateEl = document.getElementById('state');
var outEl = document.getElementById('out');
function setState(t, kind) {
stateEl.textContent = t;
stateEl.className = 'pill' + (kind ? ' ' + kind : '');
}
window.addEventListener('message', function (e) {
var d = e.data;
if (!d || d.type !== 'aa-test-result' || !d.result) return;
var slug = d.result.slug;
if (bySlug[slug]) return;
bySlug[slug] = d.result;
done++;
update();
});
function update() {
progEl.textContent = done + ' / ' + slugs.length;
var tot = 0, pass = 0, fail = 0;
Object.keys(bySlug).forEach(function (k) {
var r = bySlug[k];
tot += r.total; pass += r.pass; fail += r.fail;
});
var denom = pass + fail;
rateEl.textContent = '通过率 ' + (denom ? (Math.round(pass / denom * 1000) / 10) + '%' : '—') + '(' + pass + '/' + denom + ',N/A ' + (tot - denom) + ')';
rateEl.className = 'pill ' + (fail ? 'fail' : 'pass');
}
function loadInto(frame, slug, cb, attempt) {
attempt = attempt || 1;
var settled = false;
busy++;
setState('运行中:' + slug + (attempt > 1 ? '(重试)' : ''));
var timer = setTimeout(finish, TIMEOUT);
function onMsg(e) {
var d = e.data;
if (d && d.type === 'aa-test-result' && d.result && d.result.slug === slug) finish();
}
function finish() {
if (settled) return;
settled = true;
clearTimeout(timer);
window.removeEventListener('message', onMsg);
if (!bySlug[slug]) {
try { frame.removeAttribute('src'); } catch (e) {}
// 并发下主线程被抢占会导致偶发超时,先重试一次再判定
if (attempt < 2) {
busy--;
setState('重试:' + slug);
return setTimeout(function () { loadInto(frame, slug, cb, attempt + 1); }, 250);
}
// 两次都超时,记为 timeout(与断言失败区分开,不计入通过率分母)
bySlug[slug] = { slug: slug, total: 0, pass: 0, fail: 0, skip: 0, failures: ['timeout'] };
done++;
}
try { frame.removeAttribute('src'); } catch (e) {}
busy--;
update();
cb();
}
window.addEventListener('message', onMsg);
frame.setAttribute('src', './' + slug + '.html');
}
function pump() {
if (finished) return;
if (!queue.length && busy === 0) return finalize();
for (var i = 0; i < CONC && queue.length; i++) {
var frame = document.getElementById('f' + i);
if (frame.getAttribute('data-busy') === '1') continue;
frame.setAttribute('data-busy', '1');
(function (fr) {
var slug = queue.shift();
loadInto(fr, slug, function () {
fr.setAttribute('data-busy', '0');
pump();
});
})(frame);
}
}
function finalize() {
finished = true;
var results = slugs.map(function (s) { return bySlug[s] || { slug: s, total: 0, pass: 0, fail: 1, skip: 0, failures: ['missing'] }; });
var totalAssert = results.reduce(function (s, r) { return s + r.total; }, 0);
var passAssert = results.reduce(function (s, r) { return s + r.pass; }, 0);
var failAssert = results.reduce(function (s, r) { return s + r.fail; }, 0);
var skipAssert = results.reduce(function (s, r) { return s + r.skip; }, 0);
var denom = passAssert + failAssert;
var kinds = {};
results.forEach(function (r) {
(r.failures || []).forEach(function (f) { kinds[f] = (kinds[f] || 0) + 1; });
});
var report = {
generated: new Date().toISOString().slice(0, 19).replace('T', ' '),
version: window.__aaVersion || '',
pages: results.length,
pagesAllPass: results.filter(function (r) { return r.fail === 0 && r.total > 0; }).length,
passRate: denom ? Math.round(passAssert / denom * 1000) / 10 : 0,
/* na 与 skip 是同一批,不重复计算 */
assertions: { total: totalAssert, pass: passAssert, fail: failAssert, skip: skipAssert, na: skipAssert },
failureKinds: kinds,
/* 两次重试后仍无响应的页面:属采集环境问题(并发抢占),与断言失败分开统计 */
timedOut: results.filter(function (r) { return (r.failures || []).indexOf('timeout') >= 0; }).map(function (r) { return r.slug; }),
failedPages: results.filter(function (r) { return r.fail > 0; })
.map(function (r) { return { slug: r.slug, fail: r.fail, failures: r.failures || [], details: r.failureDetails || [] }; })
};
window.__aaCollectResult = report;
setState(report.timedOut.length ? '完成(' + report.timedOut.length + ' 页超时)' : '完成', failAssert ? 'fail' : 'pass');
outEl.textContent = JSON.stringify(report, null, 2);
try { document.title = 'DONE ' + report.passRate + '%'; } catch (e) {}
}
for (var i = 0; i < CONC; i++) {
var f = document.createElement('iframe');
f.id = 'f' + i;
f.setAttribute('title', 'collector frame ' + i);
host.appendChild(f);
}
fetch('../site/data.json', { cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (d) {
window.__aaVersion = (d.meta && d.meta.version) || '';
slugs = (d.components || []).map(function (c) { return c.slug; });
queue = slugs.slice();
progEl.textContent = '0 / ' + slugs.length;
pump();
})
.catch(function (e) {
setState('读取 site/data.json 失败', 'fail');
outEl.textContent = '需要先跑 build-site.ps1 并通过 dev-server 访问:' + e;
});
})();
</script>
</body>
</html>