Files
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

294 lines
14 KiB
JavaScript
Raw Permalink 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.
/* Kole UI 测试页 — 行为断言库(S2-P7)
*
* 目标:把测试从"元素存在"升级到"交互后发生什么",验证点击/输入/键盘的真实行为。
* 运行环境:tests/<slug>.html 的帧内(与 _runtime.js 同作用域),直接操作演示页 DOM。
* 协议:在演示页 HTML 里用 data-behavior 声明行为,格式:
* <button data-behavior="click-toggles-class:.kole-mask|is-open">打开</button>
* 支持的动作(动词:参数,参数用 | 分隔):
* click-toggles-class:<selector>|<class> — 点击后 selector 出现/消失该类(再点一次应还原)
* click-adds-node:<selector> — 点击后 selector 匹配节点数增加
* click-removes-node:<selector> — 点击后 selector 匹配节点数减少
* click-sets-attr:<selector>|<attr>|<val> — 点击后 selector 的属性变为期望值
* input-clears:<selector> — 点击后 selector(input)的 value 被清空
* input-filters:<selector>|<min> — 输入过滤后 selector 可见数减少(min=最少保留)
* keyboard-activates:<key> — 聚焦元素按 key 后有状态变化(is-active/is-open/aria 变化)
* tab-switches:<panel-attr> — 点击 tab 后对应面板切换(hidden 翻转)
* layout-menu-rows — 菜单布局守卫:可见行文本不叠字 / 可见子级不越出容
* 器横向边界 / 折叠态不露子级(nav-menu 族,S6-P43)
* 无 data-behavior 声明的页面:返回 [](不产出噪音断言)。
* 每个断言 id 形如 matrix:behavior:<slug>:<index>-<verb>,失败时 err 写清期望与实际。
*/
(function () {
'use strict';
function q(root, sel) {
try { return root.querySelector(sel); } catch (e) { return null; }
}
function qa(root, sel) {
try { return Array.prototype.slice.call(root.querySelectorAll(sel)); } catch (e) { return []; }
}
/* @doc 前缀:在整个演示文档查(弹窗挂载点常在 .demo 作用域外,如 modal 的 #mount) */
function qaScope(ctx, sel) {
if (sel.charAt(0) === '@' && sel.slice(1, 4) === 'doc') {
var real = sel.slice(4);
try { return Array.prototype.slice.call(ctx.doc.querySelectorAll(real)); } catch (e) { return []; }
}
return qa(ctx.root, sel);
}
function click(win, el) {
var ev;
try {
ev = new win.MouseEvent('click', { bubbles: true, cancelable: true, view: win });
} catch (e) {
ev = win.document.createEvent('MouseEvents');
ev.initMouseEvent('click', true, true, win, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
}
el.dispatchEvent(ev);
}
function keydown(win, el, key) {
var ev;
try {
ev = new win.KeyboardEvent('keydown', { key: key, bubbles: true, cancelable: true });
} catch (e) {
ev = win.document.createEvent('Events');
ev.initEvent('keydown', true, true);
ev.key = key;
}
el.dispatchEvent(ev);
}
function visCount(win, root, sel) {
return qa(root, sel).filter(function (n) {
try {
var cs = win.getComputedStyle(n);
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
var r = n.getBoundingClientRect();
return r.width > 0 || r.height > 0;
} catch (e) { return false; }
}).length;
}
var VERBS = {
'click-toggles-class': function (ctx, trig, args) {
var sel = args[0], cls = args[1];
if (!sel || !cls) return { status: 'fail', err: '参数缺失(需 selector|class)' };
var before = qa(ctx.root, sel).map(function (n) { return n.classList.contains(cls); }).join(',');
click(ctx.win, trig);
var after = null;
pump(ctx.doc, function () {
after = qa(ctx.root, sel).map(function (n) { return n.classList.contains(cls); }).join(',');
return after !== before;
});
if (before === after) return { status: 'fail', err: '点击后 ' + sel + ' 的 .' + cls + ' 无变化(前=' + before + ' 后=' + after + ')' };
return { status: 'pass', err: null };
},
'click-adds-node': function (ctx, trig, args) {
var sel = args[0];
if (!sel) return { status: 'fail', err: '参数缺失(需 selector)' };
var before = qaScope(ctx, sel).length;
click(ctx.win, trig);
var after = before;
pump(ctx.doc, function () {
after = qaScope(ctx, sel).length;
return after > before;
});
if (after <= before) return { status: 'fail', err: '点击后 ' + sel + ' 数量未增加(前=' + before + ' 后=' + after + ')' };
return { status: 'pass', err: null };
},
'click-removes-node': function (ctx, trig, args) {
var sel = args[0];
if (!sel) return { status: 'fail', err: '参数缺失(需 selector)' };
var before = qa(ctx.root, sel).length;
click(ctx.win, trig);
var after = qa(ctx.root, sel).length;
if (after >= before) return { status: 'fail', err: '点击后 ' + sel + ' 数量未减少(前=' + before + ' 后=' + after + ')' };
return { status: 'pass', err: null };
},
'click-sets-attr': function (ctx, trig, args) {
var sel = args[0], attr = args[1], val = args[2];
if (!sel || !attr || val === undefined) return { status: 'fail', err: '参数缺失(需 selector|attr|value)' };
click(ctx.win, trig);
var n = q(ctx.root, sel);
if (!n) return { status: 'fail', err: '点击后找不到 ' + sel };
var got = n.getAttribute(attr);
if (String(got) !== val) return { status: 'fail', err: sel + ' 的 ' + attr + ' 期望=' + val + ' 实际=' + got };
return { status: 'pass', err: null };
},
'input-clears': function (ctx, trig, args) {
var sel = args[0];
if (!sel) return { status: 'fail', err: '参数缺失(需 input selector)' };
// @input 形式:相对触发元素定位(closest .kole-input 内找 input),避免命中页内第一个 input
var n;
if (sel.charAt(0) === '@') {
var host = trig.closest ? trig.closest('.kole-input') : null;
n = host ? host.querySelector(sel.slice(1)) : null;
} else {
n = q(ctx.root, sel);
}
if (!n) return { status: 'fail', err: '找不到 ' + sel };
n.value = '待清空文本';
try { n.dispatchEvent(new ctx.win.Event('input', { bubbles: true })); } catch (e) {}
click(ctx.win, trig);
if (n.value !== '') return { status: 'fail', err: '点击清空后 value 仍为「' + n.value + '」' };
return { status: 'pass', err: null };
},
'input-filters': function (ctx, trig, args) {
var sel = args[0], min = parseInt(args[1] || '0', 10);
if (!sel) return { status: 'fail', err: '参数缺失(需 selector)' };
var before = visCount(ctx.win, ctx.root, sel);
trig.value = 'zzzz-no-match-zzz';
try { trig.dispatchEvent(new ctx.win.Event('input', { bubbles: true })); } catch (e) {}
var after = visCount(ctx.win, ctx.root, sel);
if (after >= before) return { status: 'fail', err: '输入过滤串后 ' + sel + ' 可见数未减少(前=' + before + ' 后=' + after + ')' };
if (after < min) return { status: 'fail', err: '过滤后可见数 ' + after + ' 低于最小保留 ' + min };
trig.value = '';
try { trig.dispatchEvent(new ctx.win.Event('input', { bubbles: true })); } catch (e) {}
return { status: 'pass', err: null };
},
'keyboard-activates': function (ctx, trig, args) {
var key = args[0] || 'Enter';
var sig = function () {
return qa(ctx.root, '[class]').map(function (n) { return n.className; }).join('|').length + '|' +
qa(ctx.root, '[aria-selected],[aria-expanded]').map(function (n) {
return n.getAttribute('aria-selected') + ',' + n.getAttribute('aria-expanded');
}).join('|');
};
try { trig.focus(); } catch (e) {}
var before = sig();
keydown(ctx.win, trig, key);
var after = sig();
if (before === after) return { status: 'fail', err: '按 ' + key + ' 后无状态变化(类/aria 快照一致)' };
return { status: 'pass', err: null };
},
'tab-switches': function (ctx, trig, args) {
var attr = args[0] || 'data-panel';
var tabs = qa(ctx.root, '.kole-tabs-tab');
var idx = tabs.indexOf(trig);
if (idx < 0) {
var all = qa(ctx.root, '[role="tab"],button');
idx = all.indexOf(trig);
if (idx < 0) return { status: 'fail', err: '触发元素不是 tab' };
}
var panels = qa(ctx.root, '[' + attr + ']');
var visBefore = panels.map(function (p) { return !p.hidden && p.style.display !== 'none'; });
click(ctx.win, trig);
var visAfter = visBefore.slice();
pump(ctx.doc, function () {
visAfter = panels.map(function (p) { return !p.hidden && p.style.display !== 'none'; });
return visBefore.join(',') !== visAfter.join(',');
});
if (visBefore.join(',') === visAfter.join(',')) {
return { status: 'fail', err: '点击 tab 后面板可见性无变化' };
}
return { status: 'pass', err: null };
},
/* nav-menu 族布局守卫(S6-P43,反例见 S6-P42):族重构曾把二级菜单渲染成横向
flex 子项 —— 行文本层层叠在一起、容器越出侧栏。三条不变量一次性查完:
①作用域内可见叶子文本两两不得大幅重叠;②可见子级容器不得越出菜单横向边界;
③折叠态(data-collapsed="true")不得露出子级。纯几何、无交互,不引入时序抖动。 */
'layout-menu-rows': function (ctx, trig) {
var win = ctx.win;
var menu = trig;
var fails = [];
var name = function (n) {
var t = (n.textContent || '').trim().slice(0, 12);
return (n.className || n.tagName).toString().split(' ')[0] + (t ? '「' + t + '」' : '');
};
var visible = function (n) {
var cs = win.getComputedStyle(n);
return cs.display !== 'none' && cs.visibility !== 'hidden';
};
/* ① 可见叶子文本两两重叠(叶子=无元素子节点且有文字) */
var leaves = [];
qa(menu, '*').forEach(function (n) {
if (n.children.length) return;
if (!(n.textContent || '').trim()) return;
if (!visible(n)) return;
var r = n.getBoundingClientRect();
if (r.width > 4 && r.height > 4) leaves.push({ n: n, r: r });
});
for (var i = 0; i < leaves.length; i++) {
for (var j = i + 1; j < leaves.length; j++) {
var a = leaves[i].r, b = leaves[j].r;
var ox = Math.min(a.right, b.right) - Math.max(a.left, b.left);
var oy = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
if (ox > 3 && oy > 3 && ox * oy > Math.min(a.width * a.height, b.width * b.height) * 0.55) {
fails.push('文本交叠 ' + name(leaves[i].n) + ' × ' + name(leaves[j].n) +
' 重叠 ' + Math.round(ox) + '×' + Math.round(oy) + 'px');
}
}
}
/* ② 可见子级容器越出菜单横向边界 */
var mr = menu.getBoundingClientRect();
qa(menu, '.kole-menu-children').forEach(function (k) {
if (!visible(k)) return;
var kr = k.getBoundingClientRect();
if (kr.width === 0) return;
if (kr.left < mr.left - 2 || kr.right > mr.right + 2) {
fails.push('子级越出菜单横向边界:子级 [' + Math.round(kr.left) + ',' + Math.round(kr.right) +
'] 越出菜单 [' + Math.round(mr.left) + ',' + Math.round(mr.right) + ']');
}
});
/* ③ 折叠态不得露出子级(该走 .kole-menu-pop 浮层) */
if (menu.getAttribute('data-collapsed') === 'true') {
qa(menu, '.kole-menu-children').forEach(function (k) {
if (!visible(k)) return;
var h = k.getBoundingClientRect().height;
if (h > 0) fails.push('折叠态仍露出子级:' + name(k) + '(高 ' + Math.round(h) + 'px)');
});
}
if (fails.length) {
return { status: 'fail', err: fails.slice(0, 3).join(';') + (fails.length > 3 ? ' …共 ' + fails.length + ' 处' : '') };
}
return { status: 'pass', err: null };
}
};
/* sulg 白名单:只在试点组件跑行为断言,避免全量 79 页引入不稳定。
nav-menu 族三个成员(S6-P43):其布局错位曾是"断言全绿但肉眼花屏"的盲区 */
var PILOTS = { button: 1, modal: 1, select: 1, input: 1, table: 1, tabs: 1, sidemenu: 1, topmenu: 1, mixednavigation: 1 };
/* 点击类动作是同步 dispatch,但演示页的状态更新(innerHTML/render/ hidden 翻转)
可能落在下一帧;并发回归抢主线程时读到的就是中间态。统一 pump 一帧再读后态。
同步 pump:强制同步 reflow(读 offsetHeight)+ 最多 3 次同步重读(状态翻转即停)。 */
function pump(doc, check) {
for (var i = 0; i < 3; i++) {
try { void doc.documentElement.offsetHeight; } catch (e) {}
if (check()) return true;
}
return check();
}
function runBehaviors(ctx) {
if (!PILOTS[ctx.slug]) return [];
/* 幂等:行为断言是有状态的(点击会改变演示页),同一 page-load 内只执行一次,
后续 run()(回归 runner 的兜底重跑 / 人工点「运行断言」)复用缓存。
想要重测 → 重载演示页(页面左上「重载演示页」按钮)。 */
if (window.__koleBehCache && window.__koleBehCache.slug === ctx.slug) return window.__koleBehCache.out;
var trigs = qa(ctx.root, '[data-behavior]');
var out = [];
trigs.forEach(function (trig, i) {
var spec = (trig.getAttribute('data-behavior') || '').trim();
var verb = spec.split(':')[0];
var args = spec.slice(verb.length + 1).split('|');
var id = 'matrix:behavior:' + ctx.slug + ':' + i + '-' + verb;
var fn = VERBS[verb];
if (!fn) {
out.push({ id: id, status: 'fail', ms: 0, err: '未知行为动词: ' + verb });
return;
}
var t0 = performance.now();
var r;
try {
r = fn(ctx, trig, args);
} catch (e) {
r = { status: 'fail', err: '执行异常: ' + (e && e.message || e) };
}
out.push({ id: id, status: r.status, ms: Math.round(performance.now() - t0), err: r.err });
});
window.__koleBehCache = { slug: ctx.slug, out: out };
return out;
}
window.__koleBehaviors = { run: runBehaviors, verbs: Object.keys(VERBS), pilots: Object.keys(PILOTS) };
})();