Files
aurora-admin/tests/_runtime.js
T

308 lines
12 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 */
(function () {
'use strict';
function slug() {
if (window.__SLUG__) return String(window.__SLUG__).toLowerCase();
var p = location.pathname.split('/').pop() || '';
return p.replace(/\.html$/, '').toLowerCase();
}
var SLUG = slug();
function collect() {
var nodes = Array.prototype.slice.call(document.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 snap = document.getElementById('snapshot') || document.body;
function add(id, ok, msg) { list.push({ id: 'matrix:' + id, status: ok ? 'pass' : 'fail', ms: 0, err: ok ? null : msg }); }
var variants = snap.querySelectorAll('[data-assert]');
add('default-rendered', variants.length > 0, '未发现任何 data-assert 节点');
var sizeHints = 0;
variants.forEach(function (n) {
var cls = (n.className || '').toLowerCase();
if (/-lg|-md|-sm|size|primary|default|text|small|large/.test(cls)) sizeHints++;
});
add('variants>=2', variants.length >= 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.contains(s) || n.hasAttribute(s)) states.add(s);
});
});
add('states>=1', states.size >= 1 || variants.length >= 3, '覆盖状态 ' + Array.from(states).join(','));
var kb = snap.querySelectorAll('button, a[href], input, select, textarea, [tabindex]');
add('keyboard-reachable', kb.length > 0, '无 button/a/input/tabindex 元素');
var sheets = Array.prototype.slice.call(document.styleSheets);
var hasFocus = false;
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 描边规则');
var ariaOk = false;
kb.forEach(function (n) {
if (n.getAttribute('role') || n.getAttribute('aria-label') || n.getAttribute('aria-disabled') || /^(button|a)$/.test(n.tagName.toLowerCase())) ariaOk = true;
});
add('aria-or-role', ariaOk, '交互元素缺少 role / aria-*');
/* 严格 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);
}
/* 解析浏览器计算色值:支持 rgb(...)、rgba(...)、#hex (3/4/6/8 位) */
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;
}
/* 将带 alpha 的 src 叠加到不透明 dst 上(alpha compositing) */
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
];
}
/* 递归/向上遍历祖先层级,计算真实渲染底色(处理多层 rgba 叠加,兜底白色画布) */
function resolveBg(el) {
var layers = [];
var cur = el;
while (cur && cur.nodeType === 1) {
var cs = window.getComputedStyle(cur);
var bg = parseColor(cs.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 isElementVisible(n) {
if (!n || n.nodeType !== 1) return false;
var cs = window.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;
}
/* WCAG 1.4.3 禁用态/非激活态豁免 */
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;
}
/* 排除测试运行器自带 UI(断言汇总、日志按钮、断言结果列表等) */
function isHarnessUI(n) {
if (!n || !n.closest) return false;
return !!n.closest('#result-list, .t-summary, .t-head, .t-fab, .t-toast, .t-btn, .t-list');
}
/* 大字号判定:>= 24px (18pt) 或 (>= 18.66px (14pt) 且粗体 >= 700) */
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 = snap.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) || isHarnessUI(n)) return;
var cs = window.getComputedStyle(n);
var fg = parseColor(cs.color);
if (!fg) return;
var bg = resolveBg(n);
var effFg = composite(fg, bg);
var r = ratio(effFg, 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);
var html = document.documentElement.outerHTML;
var hexHardcode = (html.match(/style="[^"]*color:\s*#[0-9a-fA-F]+/g) || []).length;
add('no-hardcode-hex', hexHardcode === 0, '硬编码颜色 ' + hexHardcode + ' 处');
return list;
}
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);
}
/* 快照节点自动标注:生成器未写入 data-assert 时,按结构启发式补标注
(.row 子元素 → .demo 子元素 → .demo-block → #snapshot 首层),
保证「DOM 带 data-assert / data-status」协议对全部 79 页成立 */
function annotate() {
var snap = document.getElementById('snapshot') || document.body;
if (snap.querySelector('[data-assert]')) return;
var mark = function (els) {
var i = 0;
Array.prototype.forEach.call(els, function (el) {
if (!el.hasAttribute('data-assert')) {
el.setAttribute('data-assert', 'variant-' + (++i));
el.setAttribute('data-status', 'skip');
}
});
return i > 0;
};
var rows = snap.querySelectorAll('.row');
for (var r = 0; r < rows.length; r++) {
if (mark(rows[r].children)) return;
}
var demos = snap.querySelectorAll('.demo');
for (var d = 0; d < demos.length; d++) {
if (mark(demos[d].children)) return;
}
if (mark(snap.querySelectorAll('.demo-block'))) return;
mark(snap.children);
}
function run() {
var t0 = performance.now();
annotate();
var asserts = collect();
asserts.forEach(function (a) {
var t1 = performance.now();
var ok = !!a.node && (a.node.offsetParent !== null || a.node.getClientRects().length > 0);
a.status = ok ? 'pass' : 'fail';
a.ms = Math.round(performance.now() - t1);
a.node.setAttribute('data-status', a.status);
});
asserts = asserts.concat(matrix());
var totalMs = Math.round(performance.now() - t0);
renderResults(asserts);
if (window.aaLogger) window.aaLogger.test(SLUG, { totalMs: totalMs, assertions: asserts });
toast('已运行 ' + asserts.length + ' 条断言');
}
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('已清空 ✓');
}
document.getElementById('run-asserts') && document.getElementById('run-asserts').addEventListener('click', 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);
if (window.aaLogger) window.aaLogger.render('test:' + SLUG, null);
})();