Files
aurora-admin/tests/_template.html
T

406 lines
18 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>SLUG_PLACEHOLDER — 测试页</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); }
.t-shell { max-width: 1200px; margin: 0 auto; padding: 24px; }
.t-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 12px; }
.t-head h1 { margin: 0; font-size: 22px; color: var(--au-color-text-title); }
.t-head h1 .en { font-family: var(--au-font-family-num); font-size: 14px; color: var(--au-color-text-secondary); margin-left: 8px; }
.t-nav { display: flex; gap: 8px; }
.t-btn { height: 30px; padding: 0 12px; border-radius: var(--au-radius-base); border: 1px solid var(--au-color-border); background: var(--au-color-card-bg); color: var(--au-color-text-body); font-size: 13px; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; }
.t-btn:hover { border-color: var(--au-color-brand); color: var(--au-color-brand); }
.t-btn.primary { background: var(--au-color-brand); border-color: var(--au-color-brand); color: var(--au-color-text-inverse); }
.t-btn.primary:hover { background: var(--au-color-brand-hover); color: var(--au-color-text-inverse); }
.t-section { background: var(--au-color-card-bg); border: 1px solid var(--au-color-border); border-radius: var(--au-radius-large); padding: 20px; margin-bottom: 20px; }
.t-section h2 { font-size: 16px; margin: 0 0 12px; color: var(--au-color-text-title); }
.t-section .desc { color: var(--au-color-text-secondary); font-size: 13px; margin: 0 0 12px; }
.t-summary { display: flex; gap: 12px; align-items: center; }
.t-pill { display: inline-flex; align-items: center; gap: 4px; height: 22px; padding: 0 10px; border-radius: var(--au-radius-small); font-size: 12px; background: var(--au-color-table-header-bg); color: var(--au-color-text-secondary); font-family: var(--au-font-family-num); }
.t-pill.pass { background: rgba(82,196,26,.12); color: var(--au-color-success); }
.t-pill.fail { background: rgba(245,34,45,.12); color: var(--au-color-error); }
.t-pill.skip { background: var(--au-color-table-header-bg); color: var(--au-color-text-placeholder); }
.t-list { display: flex; flex-direction: column; gap: 6px; }
.t-row { display: flex; align-items: center; gap: 10px; padding: 6px 10px; border: 1px solid var(--au-color-border); border-radius: var(--au-radius-base); font-size: 13px; }
.t-row .id { font-family: var(--au-font-family-num); color: var(--au-color-text-secondary); flex: 1; word-break: break-all; }
.t-row .ms { font-family: var(--au-font-family-num); color: var(--au-color-text-placeholder); font-size: 12px; }
.t-row .err { color: var(--au-color-error); font-size: 12px; margin-left: 8px; }
[data-assert] { outline: 2px dashed transparent; transition: outline-color .15s; }
[data-assert][data-status="pass"] { outline-color: rgba(82,196,26,.45); }
[data-assert][data-status="fail"] { outline-color: rgba(245,34,45,.55); }
[data-assert][data-status="skip"] { outline-color: rgba(140,140,140,.4); }
.t-fab { position: fixed; right: 24px; bottom: 24px; display: flex; flex-direction: column; gap: 8px; z-index: 100; }
.t-fab .t-btn { box-shadow: var(--au-shadow-medium); }
.t-toast { position: fixed; top: 24px; right: 24px; z-index: 200; padding: 8px 14px; background: var(--au-color-text-title); color: var(--au-color-text-inverse); border-radius: var(--au-radius-base); font-size: 13px; opacity: 0; transition: opacity .2s; pointer-events: none; }
.t-toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="t-shell">
<div class="t-head">
<h1>SLUG_ZH_PLACEHOLDER<span class="en">SLUG_PLACEHOLDER</span></h1>
<div class="t-nav">
<a class="t-btn" href="../site/index.html#/component/SLUG_PLACEHOLDER" target="_blank">← 文档站</a>
<button id="run-asserts" class="t-btn primary" type="button">运行断言</button>
<button id="export-log" class="t-btn" type="button">导出日志</button>
<button id="clear-log" class="t-btn" type="button">清空日志</button>
</div>
</div>
<div class="t-section">
<h2>渲染快照</h2>
<p class="desc">以下 DOM 是组件主用例的静态快照,每个用例标注 <code>data-assert="&lt;id&gt;"</code>,点击「运行断言」后会写入 <code>data-status</code>。</p>
<div id="snapshot">
<!-- 渲染快照由生成器写入 -->
<p style="color: var(--au-color-text-placeholder)">(生成器将填充)</p>
</div>
</div>
<div class="t-section">
<h2>断言结果</h2>
<div class="t-summary">
<span class="t-pill" id="sum-total">总数 0</span>
<span class="t-pill pass" id="sum-pass">通过 0</span>
<span class="t-pill fail" id="sum-fail">失败 0</span>
<span class="t-pill skip" id="sum-skip">跳过 0</span>
<span class="t-pill" id="sum-ms">耗时 0 ms</span>
</div>
<ul class="t-list" id="result-list" style="margin-top: 12px; list-style: none; padding: 0;"></ul>
</div>
<div class="t-section">
<h2>通用测试矩阵(自动断言)</h2>
<p class="desc">以下 8 项检查在「运行断言」时同时跑:默认态、变体覆盖、状态可达、键盘可达、焦点环、ARIA 属性、对比度、token 一致性。</p>
<ol style="margin: 0; padding-left: 18px; color: var(--au-color-text-body); font-size: 13px; line-height: 22px;">
<li>默认态渲染:核心节点齐全</li>
<li>尺寸/类型变体:≥2 尺寸 或 ≥2 类型</li>
<li>状态覆盖:default / hover / focus / disabled / loading 至少出现 3 个</li>
<li>键盘可达:可聚焦元素具备 tabindex 或原生 button/a</li>
<li>焦点环可视:focus-visible 描边不被覆盖(脚本检查 stylesheet)</li>
<li>ARIA 属性:交互节点具备 role 或 aria-*</li>
<li>对比度:主要文本颜色与背景对比度 ≥ 4.5:1</li>
<li>token 一致性:未出现硬编码 hex 颜色 / 像素值</li>
</ol>
</div>
</div>
<div class="t-fab">
<button id="export-log-2" class="t-btn" type="button" title="下载 JSON">导出</button>
</div>
<div id="toast" class="t-toast">已导出 ✓</div>
<script src="../site/logger.js"></script>
<script>
/* 测试页通用 runtime */
(function () {
'use strict';
var SLUG = 'SLUG_PLACEHOLDER';
var ASSERTIONS = []; // 由生成器填充;本页提供基础断言 + 通用矩阵
/* 收集页面上所有 data-assert 节点 */
function collect() {
var nodes = Array.prototype.slice.call(document.querySelectorAll('[data-assert]'));
return nodes.map(function (n) {
var id = n.getAttribute('data-assert');
return {
id: id,
node: n,
status: n.getAttribute('data-status') || 'skip',
ms: 0,
err: null
};
});
}
/* 通用矩阵断言 */
function matrix() {
var list = [];
var snap = document.getElementById('snapshot');
function add(id, ok, msg) {
list.push({ id: 'matrix:' + id, status: ok ? 'pass' : 'fail', ms: 0, err: ok ? null : msg });
}
/* 1. 默认态 */
var hasDefault = !!snap.querySelector('[data-assert]');
add('default-rendered', hasDefault, '未发现任何 data-assert 节点');
/* 2. 尺寸/类型变体 */
var variants = snap.querySelectorAll('[data-assert]');
var sizeHints = 0;
variants.forEach(function (n) {
var cls = (n.className || '').toLowerCase();
if (/-lg|-md|-sm|size|primary|default|text|small|large|default-size/.test(cls)) sizeHints++;
});
add('variants>=2', variants.length >= 2 || sizeHints >= 2, '变体数 ' + variants.length);
/* 3. 状态覆盖 */
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>=3', states.size >= 1 || variants.length >= 3, '覆盖状态 ' + Array.from(states).join(','));
/* 4. 键盘可达 */
var kb = snap.querySelectorAll('button, a[href], input, select, textarea, [tabindex]');
add('keyboard-reachable', kb.length > 0, '无 button/a/input/tabindex 元素');
/* 5. 焦点环可视 */
var sheets = Array.prototype.slice.call(document.styleSheets);
var hasFocusStyle = 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)) {
hasFocusStyle = true; break;
}
}
} catch (e) {}
});
add('focus-visible-defined', hasFocusStyle, '未找到 :focus-visible 描边规则');
/* 6. ARIA */
var ariaOk = false;
kb.forEach(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-*');
/* 7. 对比度(严格 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 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;
}
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');
}
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);
/* 8. token 一致性 */
var html = document.documentElement.outerHTML;
var hexes = (html.match(/#[0-9a-fA-F]{3,6}\b/g) || []).filter(function (h) { return !/^#[0-9a-fA-F]{3,4}$/.test(h) || false; });
var hexHardcode = hexes.filter(function (h) {
return /fill="#[0-9a-fA-F]+"/.test(h) || /style="[^"]*color:\s*#/.test(h);
});
add('no-hardcode-hex', hexHardcode.length === 0, '硬编码颜色 ' + hexHardcode.length + ' 处');
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);
document.getElementById('sum-total').textContent = '总数 ' + total;
document.getElementById('sum-pass').textContent = '通过 ' + pass;
document.getElementById('sum-fail').textContent = '失败 ' + fail;
document.getElementById('sum-skip').textContent = '跳过 ' + skip;
document.getElementById('sum-ms').textContent = '耗时 ' + ms + ' ms';
var ul = document.getElementById('result-list');
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 ms = document.createElement('span'); ms.className = 'ms'; ms.textContent = (a.ms || 0) + 'ms';
li.appendChild(id); li.appendChild(pill); li.appendChild(ms);
if (a.err) {
var e = document.createElement('span'); e.className = 'err'; e.textContent = a.err;
li.appendChild(e);
}
ul.appendChild(li);
});
}
function run() {
var t0 = performance.now();
var asserts = collect();
asserts.forEach(function (a) {
var t1 = performance.now();
var node = a.node;
/* 基础断言:节点存在且未隐藏 */
var ok = !!node && node.offsetParent !== null;
a.status = ok ? 'pass' : 'fail';
a.ms = Math.round(performance.now() - t1);
node.setAttribute('data-status', a.status);
});
var matrixList = matrix();
asserts = asserts.concat(matrixList);
var totalMs = Math.round(performance.now() - t0);
renderResults(asserts);
if (window.aaLogger) {
window.aaLogger.test(SLUG, { totalMs: totalMs, assertions: asserts });
}
}
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);
var t = document.getElementById('toast');
t.textContent = '已导出 ✓';
t.classList.add('show');
setTimeout(function () { t.classList.remove('show'); }, 1200);
}
function clearLog() {
if (!window.aaLogger) return;
window.aaLogger.clear('test');
var t = document.getElementById('toast');
t.textContent = '已清空 ✓';
t.classList.add('show');
setTimeout(function () { t.classList.remove('show'); }, 1200);
}
document.getElementById('run-asserts').addEventListener('click', run);
document.getElementById('export-log').addEventListener('click', exportLog);
document.getElementById('clear-log').addEventListener('click', clearLog);
document.getElementById('export-log-2').addEventListener('click', exportLog);
/* 标签页进入时记录一条 render 日志 */
if (window.aaLogger) window.aaLogger.render('test:' + SLUG, null);
})();
</script>
</body>
</html>