Files
aurora-admin/tests/_runtime.js
T
aurora-admin 6fec4ba6f8 v1.3.0: 修复验收链路(iframe 真实渲染+跨帧断言)、基座焦点环、契约第二批 15 个
回归通过率此前不可信:run-tests.ps1 抽取演示页 body 时剥离全部 <script>,
而 79 个 H5 演示页的 DOM 全由内联脚本渲染,且测试页从未引用 frameworks/<组件>.css
—— 断言跑在空 DOM + 无组件样式上,v1.2.0 的 75.1% 属结构性误报。

Fixed — 验收链路
- 测试页改为 iframe 加载真实演示页(_template.html),_runtime.js 跨帧在真实
  渲染结果上断言:帧内 getComputedStyle/styleSheets/outerHTML,用例自动标注 +
  帧内描边,结果 postMessage 上报
- 标注改为容器下钻(.row/.demo-block → 首个含 ≥2 有效子元素的容器,最多 6 层)
- 新增 N/A 语义:静态组件键盘/ARIA 记 skip 而非失败;对比度与 token 检查限定
  在被测组件区,页头文档文字不计
- 通过率分母改为 pass/(pass+fail),tools/run-regression.mjs 与收集器同口径

Added — 设计系统
- 基座 :focus-visible 统一焦点环(colors_and_type.css)。--au-color-focus-ring
  此前只定义未消费,79 个组件样式仅 20 个自带 focus 规则 —— 一次性消除 66 页失败
- tests/_collect.html:零依赖浏览器批量回归收集器(并发 iframe + 汇总)
- tests/_index_template.html:测试总览页模板外置,读数来自 data.json

Added — 契约第二批(展示类 15 个)
- tree/collapse/calendar/carousel/imagepreview/qrcode/countdown/watermark/
  cardlist/treetable/timelinelist/steplist/chartpanel/dashboardcard/employeecard
- 逐份从规格原文(组件2/4/7/9.txt)提炼,含 dims/variants/anatomy/structure/
  usageHints/doNotInvent/unknowns;契约化 21 → 36/79

Changed
- run-tests.ps1 重写:删除失效的快照抽取(占位符替换早已不匹配模板),改生成
  iframe 版测试页;输出 WriteAllText 无 BOM;保持 ASCII-only,中文移入 UTF-8 模板
- 回归基线:960 断言 / 794 通过 / 85.1% / 11 页全通过 / N/A 27
- CHANGELOG/TESTING/改造清单同步;测试总览页与首页通过率联动

验证:浏览器全量 79 页收集器跑通两次,基线一致;详情页契约章节
(含 doNotInvent/unknowns)、chip、首页「通过率 85.1%」、changelog v1.3.0 实测通过
2026-09-10 21:19:09 +08:00

475 lines
18 KiB
JavaScript
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.
/* Aurora Admin 测试页 — 共享 runtime
v1.3.0:断言目标改为 iframe 内真实演示页(frameworks/<Prefix>.html)。
演示页的 DOM 由内联脚本渲染,静态抄快照拿不到用例,因此必须跨帧在真实渲染结果上断言。
- 同源 iframe 可直接访问 contentDocument;失败时回退到本页 document(兼容旧页)。 */
(function () {
'use strict';
function slugFromPath() {
if (window.__SLUG__) return String(window.__SLUG__).toLowerCase();
var p = location.pathname.split('/').pop() || '';
return p.replace(/\.html$/, '').toLowerCase();
}
var SLUG = slugFromPath();
var FRAME_ID = 'snapshot-frame';
var fDoc = null;
var fWin = null;
/* ---------- 帧绑定 ---------- */
function bindFrame() {
var f = document.getElementById(FRAME_ID);
if (!f) return false;
var d, w;
try {
d = f.contentDocument;
w = f.contentWindow;
} catch (e) { return false; }
if (!d || !d.body || !w) return false;
var href = '';
try { href = String(w.location.href || ''); } catch (e) { href = ''; }
/* about:blank / 尚未开始加载时不接受 */
if (!/\.html(\?|#|$)/.test(href)) return false;
fDoc = d;
fWin = w;
injectFrameStyle();
setFrameState('已就绪 · ' + document.title.split(' ')[0], 'pass');
return true;
}
function doc() { return fDoc || document; }
function win() { return fWin || window; }
/* 被测范围:演示页的 .demo 容器(文档页头 h1/.sub 不属于组件交付物) */
function scope() {
var d = doc();
return d.querySelector('.demo') || d.body || document.body;
}
/* 页头文档性文字:被测范围之外的说明文字 */
function isDocChrome(n) {
if (!n || !n.tagName) return false;
if (/^h1$/i.test(n.tagName)) return true;
var cls = (n.className || '').toString().toLowerCase();
if (/\b(sub|desc|hint|note)\b/.test(cls)) return true;
return !!(n.closest && n.closest('h1, .sub, .desc, .hint, .note'));
}
function injectFrameStyle() {
if (!fDoc || fDoc.getElementById('aa-assert-style')) return;
var s = fDoc.createElement('style');
s.id = 'aa-assert-style';
s.textContent =
'[data-assert]{outline:2px dashed transparent;outline-offset:2px;transition:outline-color .15s}' +
'[data-assert][data-status="pass"]{outline-color:rgba(82,196,26,.5)}' +
'[data-assert][data-status="fail"]{outline-color:rgba(245,34,45,.65)}' +
'[data-assert][data-status="skip"]{outline-color:rgba(140,140,140,.45)}';
(fDoc.head || fDoc.documentElement).appendChild(s);
}
function setFrameState(text, kind) {
var el = document.getElementById('frame-state');
if (!el) return;
el.textContent = text;
el.className = 't-pill' + (kind ? ' ' + kind : '');
}
/* ---------- 用例标注 ---------- */
/* 用例标注:覆盖 79 个演示页的四种结构形态
演示页把内容包在 .demo / .aa-page 里,包裹层数不等,因此从被测范围向下
寻找「第一个有 ≥2 个有效子元素的容器」作为用例层;找不到就逐层下钻。
页头文档文字(h1 / .sub 等)不标为用例。 */
function annotate() {
var root = scope();
if (!root || root.querySelector('[data-assert]')) return;
var cnt = 0;
function usable(el) {
if (!el || !el.tagName) return false;
if (/^(SCRIPT|STYLE|BR|HR|LINK|META)$/.test(el.tagName)) return false;
return !isDocChrome(el);
}
function mark(els) {
Array.prototype.forEach.call(els, function (el) {
if (!usable(el) || el.hasAttribute('data-assert')) return;
el.setAttribute('data-assert', 'variant-' + (++cnt));
el.setAttribute('data-status', 'skip');
});
}
var groups = root.querySelectorAll('.row, .demo-block, .demo-row');
if (groups.length) {
Array.prototype.forEach.call(groups, function (g) { mark(g.children); });
}
if (cnt === 0) {
/* 下钻:容器有效子元素 < 2 时继续进入其后代,最多 6 层 */
var queue = [{ el: root, depth: 0 }];
while (queue.length && cnt === 0) {
var cur = queue.shift();
var kids = Array.prototype.filter.call(cur.el.children || [], usable);
if (kids.length >= 2) {
mark(kids);
break;
}
if (cur.depth < 6) {
kids.forEach(function (k) {
if (k.children && k.children.length) queue.push({ el: k, depth: cur.depth + 1 });
});
}
}
}
if (cnt === 0) mark(root.children);
}
function collect() {
var nodes = Array.prototype.slice.call(doc().querySelectorAll('[data-assert]'));
return nodes.map(function (n) {
return {
id: n.getAttribute('data-assert'),
node: n,
status: n.getAttribute('data-status') || 'skip',
ms: 0,
err: null
};
});
}
/* ---------- 通用矩阵 ---------- */
function matrix() {
var list = [];
var root = scope();
var W = win();
function add(id, ok, msg) {
list.push({ id: 'matrix:' + id, status: ok ? 'pass' : 'fail', ms: 0, err: ok ? null : msg });
}
var variants = root.querySelectorAll('[data-assert]');
add('default-rendered', variants.length > 0, '未发现任何用例节点(演示页帧内为空)');
var sizeHints = 0;
variants.forEach(function (n) {
var cls = (n.className || '').toString().toLowerCase();
if (/-lg|-md|-sm|size|primary|default|text|small|large/.test(cls)) sizeHints++;
});
add('variants>=2', variants.length >= 2 || sizeHints >= 2, '用例数 ' + variants.length);
var states = new Set();
variants.forEach(function (n) {
['disabled', 'loading', 'is-loading', 'active', 'hover', 'focus', 'open'].forEach(function (s) {
if (n.classList && (n.classList.contains(s) || n.hasAttribute(s))) states.add(s);
});
});
add('states>=1', states.size >= 1 || variants.length >= 3, '覆盖状态 ' + Array.from(states).join(','));
var visible = 0;
variants.forEach(function (n) { if (isElementVisible(n)) visible++; });
add('visible-asserts>=1', visible >= 1, '默认态可见用例 0 / ' + variants.length + '(全部隐藏)');
var kb = root.querySelectorAll('button, a[href], input, select, textarea, [tabindex]');
/* 键盘可达 / ARIA 的真实意图是「点击类交互不能只有鼠标路径」。
纯静态组件(水印、指标卡、图表)本就没有交互面,记 N/A(skip)而不算失败;
一旦存在 cursor:pointer 或 onclick 的非原生可聚焦元素,就是真缺口。 */
var fake = [];
Array.prototype.forEach.call(root.querySelectorAll('[onclick], [role="button"], div, span'), function (n) {
if (kb.length && Array.prototype.indexOf.call(kb, n) >= 0) return;
if (/^(BUTTON|A|INPUT|SELECT|TEXTAREA)$/.test(n.tagName)) return;
if (n.hasAttribute('tabindex')) return;
var onclick = n.hasAttribute('onclick');
var ptr = false;
try { ptr = W.getComputedStyle(n).cursor === 'pointer'; } catch (e) {}
if (onclick || ptr) {
var label = n.tagName.toLowerCase() + (n.className ? '.' + String(n.className).trim().split(/\s+/)[0] : '');
if (fake.indexOf(label) < 0 && fake.length < 3) fake.push(label);
}
});
if (kb.length === 0 && fake.length === 0) {
list.push({ id: 'matrix:keyboard-reachable', status: 'skip', ms: 0, err: '静态组件无交互面' });
list.push({ id: 'matrix:aria-or-role', status: 'skip', ms: 0, err: '静态组件无交互面' });
} else {
add('keyboard-reachable', kb.length > 0, fake.length ? '存在鼠标专用交互:' + fake.join('、') : '无 button/a/input/tabindex 元素');
var ariaOk = false;
Array.prototype.forEach.call(kb, function (n) {
if (n.getAttribute('role') || n.getAttribute('aria-label') || n.getAttribute('aria-disabled') ||
n.getAttribute('aria-expanded') || /^(button|a)$/.test(n.tagName.toLowerCase())) ariaOk = true;
});
add('aria-or-role', ariaOk, '交互元素缺少 role / aria-*');
}
var hasFocus = false;
var sheets = Array.prototype.slice.call(doc().styleSheets || []);
sheets.forEach(function (s) {
try {
var rules = s.cssRules || [];
for (var i = 0; i < rules.length; i++) {
if (rules[i].selectorText && /:focus(-visible)?/.test(rules[i].selectorText)) { hasFocus = true; break; }
}
} catch (e) {}
});
add('focus-visible-defined', hasFocus, '未找到 :focus(-visible) 描边规则');
/* 交互元素检查见下方 keyboard / aria 分支(静态组件记 N/A) */
/* 对比度:WCAG 2.1 相对亮度 */
function sRGBtoLin(v) {
v = v / 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}
function lum(rgb) {
return 0.2126 * sRGBtoLin(rgb[0]) + 0.7152 * sRGBtoLin(rgb[1]) + 0.0722 * sRGBtoLin(rgb[2]);
}
function ratio(fg, bg) {
var L1 = lum(fg), L2 = lum(bg);
var hi = Math.max(L1, L2), lo = Math.min(L1, L2);
return (hi + 0.05) / (lo + 0.05);
}
function parseColor(c) {
if (!c || typeof c !== 'string') return null;
c = c.trim();
var m = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s/]+([\d.]+%?))?\s*\)$/i.exec(c);
if (m) {
var a = 1;
if (m[4] !== undefined) a = m[4].slice(-1) === '%' ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
return [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3]), Math.max(0, Math.min(1, a))];
}
m = /^#([0-9a-f]{3,8})$/i.exec(c);
if (m) {
var h = m[1];
if (h.length === 3 || h.length === 4) h = h.split('').map(function (ch) { return ch + ch; }).join('');
if (h.length === 6) h += 'ff';
if (h.length === 8) {
return [
parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16),
parseInt(h.slice(4, 6), 16), parseInt(h.slice(6, 8), 16) / 255
];
}
}
return null;
}
function composite(src, dst) {
var a = src[3];
if (a >= 1) return [src[0], src[1], src[2], 1];
return [
Math.round(src[0] * a + dst[0] * (1 - a)),
Math.round(src[1] * a + dst[1] * (1 - a)),
Math.round(src[2] * a + dst[2] * (1 - a)),
1
];
}
function resolveBg(el) {
var layers = [];
var cur = el;
while (cur && cur.nodeType === 1) {
var bg = parseColor(W.getComputedStyle(cur).backgroundColor);
if (bg && bg[3] > 0) {
layers.push(bg);
if (bg[3] >= 1) break;
}
cur = cur.parentElement;
}
var out = [255, 255, 255, 1];
for (var i = layers.length - 1; i >= 0; i--) out = composite(layers[i], out);
return out;
}
function hasDirectText(n) {
for (var i = 0; i < n.childNodes.length; i++) {
var cn = n.childNodes[i];
if (cn.nodeType === 3 && /\S/.test(cn.nodeValue)) return true;
}
return false;
}
function isInactive(n) {
var cur = n;
while (cur && cur.nodeType === 1) {
if (cur.hasAttribute('disabled') || cur.getAttribute('aria-disabled') === 'true') return true;
var cls = (cur.className || '').toString().toLowerCase();
if (/\b(disabled|is-disabled)\b/.test(cls)) return true;
cur = cur.parentElement;
}
return false;
}
function isLargeText(cs) {
var fs = parseFloat(cs.fontSize) || 14;
var fw = parseInt(cs.fontWeight, 10) || (cs.fontWeight === 'bold' ? 700 : 400);
return fs >= 24 || (fs >= 18.66 && fw >= 700);
}
var contrastFailures = [];
var textNodes = root.querySelectorAll('button, a, p, span, label, td, th, li, h1, h2, h3, h4, h5, h6');
textNodes.forEach(function (n) {
if (contrastFailures.length >= 3) return;
if (!hasDirectText(n) || !isElementVisible(n) || isInactive(n) || isDocChrome(n)) return;
var cs = W.getComputedStyle(n);
var fg = parseColor(cs.color);
if (!fg) return;
var bg = resolveBg(n);
var r = ratio(composite(fg, bg), bg);
var minRatio = isLargeText(cs) ? 3.0 : 4.5;
if (r < minRatio) {
var label = n.tagName.toLowerCase() + (n.className ? '.' + String(n.className).trim().split(/\s+/)[0] : '') +
' "' + (n.textContent || '').trim().slice(0, 10) + '"';
contrastFailures.push(label + ' (' + r.toFixed(2) + ':1 < ' + minRatio + ':1)');
}
});
add('contrast>=4.5', contrastFailures.length === 0, contrastFailures.length ? '低于 WCAG AA:' + contrastFailures.join(';') : null);
/* token 一致性:用例区内联 hex 颜色 */
var hexHardcode = 0;
var styled = root.querySelectorAll('[style]');
Array.prototype.forEach.call(styled, function (n) {
var st = n.getAttribute('style') || '';
if (/(^|;)\s*(background|background-color|color|border|border-color|fill|stroke)\s*:[^;]*#[0-9a-fA-F]{3,8}/.test(st)) hexHardcode++;
});
add('no-hardcode-hex', hexHardcode === 0, '用例区内联硬编码颜色 ' + hexHardcode + ' 处');
return list;
}
function isElementVisible(n) {
if (!n || n.nodeType !== 1) return false;
var cs = win().getComputedStyle(n);
if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) return false;
var rect = n.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) return false;
return true;
}
/* ---------- 结果渲染与上报 ---------- */
function renderResults(assertions) {
var total = assertions.length;
var pass = assertions.filter(function (a) { return a.status === 'pass'; }).length;
var fail = assertions.filter(function (a) { return a.status === 'fail'; }).length;
var skip = assertions.filter(function (a) { return a.status === 'skip'; }).length;
var ms = assertions.reduce(function (s, a) { return s + (a.ms || 0); }, 0);
function setText(id, txt) { var n = document.getElementById(id); if (n) n.textContent = txt; }
setText('sum-total', '总数 ' + total);
setText('sum-pass', '通过 ' + pass);
setText('sum-fail', '失败 ' + fail);
setText('sum-skip', '跳过 ' + skip);
setText('sum-ms', '耗时 ' + ms + ' ms');
var ul = document.getElementById('result-list');
if (!ul) return;
ul.innerHTML = '';
assertions.forEach(function (a) {
var li = document.createElement('li'); li.className = 't-row';
var id = document.createElement('span'); id.className = 'id'; id.textContent = a.id;
var pill = document.createElement('span');
pill.className = 't-pill ' + (a.status === 'pass' ? 'pass' : a.status === 'fail' ? 'fail' : 'skip');
pill.textContent = a.status.toUpperCase();
var m = document.createElement('span'); m.className = 'ms'; m.textContent = (a.ms || 0) + 'ms';
li.appendChild(id); li.appendChild(pill); li.appendChild(m);
if (a.err) { var e = document.createElement('span'); e.className = 'err'; e.textContent = a.err; li.appendChild(e); }
ul.appendChild(li);
});
}
function toast(msg) {
var t = document.getElementById('toast'); if (!t) return;
t.textContent = msg; t.classList.add('show');
setTimeout(function () { t.classList.remove('show'); }, 1200);
}
function run() {
var t0 = performance.now();
annotate();
var asserts = collect();
asserts.forEach(function (a) {
var t1 = performance.now();
var visible = !!a.node && isElementVisible(a.node);
var rects = a.node && a.node.getClientRects ? a.node.getClientRects().length : 0;
if (visible || rects > 0) {
a.status = 'pass';
} else {
a.status = 'skip';
a.err = '默认隐藏(交互后可见)';
}
a.ms = Math.round(performance.now() - t1);
try { a.node.setAttribute('data-status', a.status); } catch (e) {}
});
asserts = asserts.concat(matrix());
var totalMs = Math.round(performance.now() - t0);
renderResults(asserts);
var res = {
slug: SLUG,
total: asserts.length,
pass: asserts.filter(function (a) { return a.status === 'pass'; }).length,
fail: asserts.filter(function (a) { return a.status === 'fail'; }).length,
skip: asserts.filter(function (a) { return a.status === 'skip'; }).length,
failures: asserts.filter(function (a) { return a.status === 'fail'; }).map(function (a) { return a.id; }).slice(0, 8),
ms: totalMs
};
window.__aaResult = res;
setFrameState('断言 ' + res.pass + '/' + res.total + ' 通过', res.fail ? 'fail' : 'pass');
if (window.aaLogger) window.aaLogger.test(SLUG, { totalMs: totalMs, assertions: asserts });
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: 'aa-test-result', result: res }, '*');
}
} catch (e) {}
return res;
}
function exportLog() {
if (!window.aaLogger) return;
var data = window.aaLogger.export('test');
var blob = new Blob([data], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = 'aa-test-' + SLUG + '-' + Date.now() + '.json';
document.body.appendChild(a); a.click();
setTimeout(function () { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
toast('已导出 ✓');
}
function clearLog() {
if (!window.aaLogger) return;
window.aaLogger.clear('test');
toast('已清空 ✓');
}
/* ---------- 启动 ---------- */
var runBtn = document.getElementById('run-asserts');
if (runBtn) runBtn.addEventListener('click', function () { if (!fDoc) bindFrame(); run(); });
['export-log', 'export-log-2'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.addEventListener('click', exportLog);
});
var clr = document.getElementById('clear-log');
if (clr) clr.addEventListener('click', clearLog);
var frame = document.getElementById(FRAME_ID);
if (frame) {
var auto = function () {
if (!bindFrame()) { setFrameState('演示页加载失败', 'fail'); return; }
setTimeout(run, 60);
};
frame.addEventListener('load', auto);
if (bindFrame()) setTimeout(run, 60); else setFrameState('加载中…');
} else {
setTimeout(run, 0);
}
var reload = document.getElementById('reload-frame');
if (reload) reload.addEventListener('click', function () {
if (!frame) return;
fDoc = null; fWin = null;
setFrameState('加载中…');
frame.setAttribute('src', frame.getAttribute('src'));
});
if (window.aaLogger) window.aaLogger.render('test:' + SLUG, null);
window.__aaRun = run;
})();