Files
aurora-admin/tests/mobile/_behaviors.js
T
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

257 lines
11 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.
/* Kole UI Mobile 测试页 — 移动端行为断言库
*
* 与 PC 侧 tests/_behaviors.js 的关系:
* - 同一套协议([data-behavior="动词:参数|参数"]、window.__koleBehaviors、id 形如
* matrix:behavior:<slug>:<index>-<verb>)与同一套引擎(tests/_runtime.js 调用)。
* - **本文件是移动端专属**:新增触控动词(滑动 / 下拉),PC 端不认识这些动词;
* 移动端也不加载 PC 的行为库,故两端互不影响(隔离规则见 PLATFORMS.md)。
*
* 动词一览:
* click-toggles-class:<selector>|<class> 点击后该类出现/消失
* click-sets-attr:<selector>|<attr>|<val> 点击后属性变为期望值
* tab-switches:<panel-attr> 点击标签后对应面板 hidden 翻转
* swipe-sets-class:<selector>|<class>[|left|right] 在该元素上滑动后该类出现(默认左滑)
* swipe-sets-attr:<selector>|<attr>|<val>[|left|right] 同上,断言属性
* pull-triggers:<selector>|<class>[|<distance>] 在该元素上向下拖动后该类出现
*/
(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 []; }
}
function classesOf(root, sel, cls) {
return qa(root, sel).map(function (n) { return n.classList.contains(cls); }).join(',');
}
function attrsOf(root, sel, attr) {
return qa(root, sel).map(function (n) { return n.getAttribute(attr); }).join(',');
}
/* 同步 pump:点击/手势是同步 dispatch,但演示页可能在下一帧才写状态。
与 PC 侧同口径:强制 reflow + 最多 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 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 makeEvent(win, type, x, y, buttons) {
var init = {
bubbles: true,
cancelable: true,
view: win,
clientX: x,
clientY: y,
buttons: buttons,
button: type === 'pointerup' || type === 'pointercancel' ? 0 : 0,
pointerId: 1,
pointerType: 'touch',
isPrimary: true
};
var ev;
if (typeof win.PointerEvent === 'function') {
try { return new win.PointerEvent(type, init); } catch (e) { /* 落到 MouseEvent */ }
}
try {
ev = new win.MouseEvent(type.replace('pointer', 'mouse'), init);
} catch (e) {
ev = win.document.createEvent('MouseEvents');
ev.initMouseEvent(type.replace('pointer', 'mouse'), true, true, win, 1, x, y, x, y,
false, false, false, false, 0, null);
}
return ev;
}
function dispatch(win, el, type, x, y, buttons) {
el.dispatchEvent(makeEvent(win, type, x, y, buttons));
}
/* 手势基座:down → 若干 move → up。steps 分段以保证「判定为滑动」的逻辑能读到中间位移。 */
function gesture(ctx, el, dx, dy, steps) {
var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : { left: 0, top: 0, width: 320, height: 120 };
var x0 = Math.round(rect.left + Math.min(120, Math.max(20, (rect.width || 320) / 3)));
var y0 = Math.round(rect.top + Math.min(40, Math.max(12, (rect.height || 120) / 3)));
var n = steps || 4;
dispatch(ctx.win, el, 'pointerdown', x0, y0, 1);
for (var i = 1; i <= n; i++) {
dispatch(ctx.win, el, 'pointermove', x0 + Math.round((dx * i) / n), y0 + Math.round((dy * i) / n), 1);
}
dispatch(ctx.win, el, 'pointerup', x0 + dx, y0 + dy, 0);
}
/* 滑动(SwipeCell):左滑 dx=-100,右滑 dx=+100 —— 均超过默认操作区宽度的一半 */
function swipe(ctx, sel, dir) {
var el = q(ctx.root, sel);
if (!el) return { el: null, err: '找不到元素 ' + sel };
var sign = dir === 'right' ? 1 : -1;
gesture(ctx, el, sign * 100, 0, 4);
return { el: el, err: null };
}
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 = classesOf(ctx.root, sel, cls);
click(ctx.win, trig);
var after = before;
pump(ctx.doc, function () {
after = classesOf(ctx.root, sel, cls);
return after !== before;
});
if (before === after) return { status: 'fail', err: '点击后 ' + sel + ' 的 .' + cls + ' 无变化(前=' + 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 got = null;
pump(ctx.doc, function () {
var n = q(ctx.root, sel);
got = n ? n.getAttribute(attr) : null;
return String(got) === val;
});
if (String(got) !== val) return { status: 'fail', err: sel + ' 的 ' + attr + ' 期望=' + val + ' 实际=' + got };
return { status: 'pass', err: null };
},
'tab-switches': function (ctx, trig, args) {
var attr = args[0] || 'data-panel';
var items = qa(ctx.root, '.kole-m-tabbar__item');
if (items.indexOf(trig) < 0) items = qa(ctx.root, '[role="tab"],button');
if (items.indexOf(trig) < 0) return { status: 'fail', err: '触发元素不是标签项' };
var panels = qa(ctx.root, '[' + attr + ']');
if (!panels.length) return { status: 'fail', err: '未找到 [' + attr + '] 面板' };
var vis = function () {
return panels.map(function (p) { return !p.hidden && p.style.display !== 'none'; }).join(',');
};
var before = vis();
click(ctx.win, trig);
var after = before;
pump(ctx.doc, function () {
after = vis();
return after !== before;
});
if (before === after) return { status: 'fail', err: '点击标签后面板可见性无变化(' + before + ')' };
var sel = trig.getAttribute('aria-selected');
if (sel !== 'true') return { status: 'fail', err: '切换后 aria-selected 期望=true 实际=' + sel };
return { status: 'pass', err: null };
},
'swipe-sets-class': function (ctx, trig, args) {
var sel = args[0], cls = args[1], dir = args[2] || 'left';
if (!sel || !cls) return { status: 'fail', err: '参数缺失(需 selector|class[|left|right])' };
var before = classesOf(ctx.root, sel, cls);
var r = swipe(ctx, sel, dir);
if (r.err) return { status: 'fail', err: r.err };
var after = before;
pump(ctx.doc, function () {
after = classesOf(ctx.root, sel, cls);
return after !== before;
});
if (before === after) return { status: 'fail', err: dir + '滑后 ' + sel + ' 的 .' + cls + ' 无变化(' + before + ')' };
return { status: 'pass', err: null };
},
'swipe-sets-attr': function (ctx, trig, args) {
var sel = args[0], attr = args[1], val = args[2], dir = args[3] || 'left';
if (!sel || !attr || val === undefined) return { status: 'fail', err: '参数缺失(需 selector|attr|value[|left|right])' };
var r = swipe(ctx, sel, dir);
if (r.err) return { status: 'fail', err: r.err };
var got = null;
pump(ctx.doc, function () {
var n = q(ctx.root, sel);
got = n ? n.getAttribute(attr) : null;
return String(got) === val;
});
if (String(got) !== val) return { status: 'fail', err: dir + '滑后 ' + sel + ' 的 ' + attr + ' 期望=' + val + ' 实际=' + got };
return { status: 'pass', err: null };
},
'pull-triggers': function (ctx, trig, args) {
var sel = args[0], cls = args[1], dist = Number(args[2]) || 80;
if (!sel || !cls) return { status: 'fail', err: '参数缺失(需 selector|class[|distance])' };
if (dist < 61) return { status: 'fail', err: '下拉距离 ' + dist + 'px 小于默认阈值 60px,无法触发' };
var before = classesOf(ctx.root, sel, cls);
var el = q(ctx.root, sel);
if (!el) return { status: 'fail', err: '找不到元素 ' + sel };
gesture(ctx, el, 0, dist, 4);
var after = before;
pump(ctx.doc, function () {
after = classesOf(ctx.root, sel, cls);
return after !== before;
});
if (before === after) return { status: 'fail', err: '下拉 ' + dist + 'px 后 ' + sel + ' 的 .' + cls + ' 无变化(' + before + ')' };
return { status: 'pass', err: null };
}
};
/* 行为断言的适用范围(2026-09-20 修正:此前是硬编码白名单,索引里登记了 behaviors 的组件
也不会被点击验证 —— 演示页写了 data-behavior 却静默不跑,属静默缺口)。
现在两道来源取并集:
① 演示页里**真的**写了 [data-behavior](组件自己声明了行为,就该被验证);
② 兜底名单(历史 5 个组件;即使哪天演示页漏写属性也仍然跑)。
build-mobile 在生成测试页时把 ① 的结果写进 window.__koleBehaviorSlugs。 */
var FALLBACK_PILOTS = { navbar: 1, tabbar: 1, actionsheet: 1, pullrefresh: 1, swipecell: 1 };
function inScope(slug, root) {
if (FALLBACK_PILOTS[slug]) return true;
if (window.__koleBehaviorSlugs && window.__koleBehaviorSlugs.indexOf(slug) >= 0) return true;
/* 兜底:本页确实有 data-behavior(生成期数据未注入时,例如手工打开测试页) */
try {
return !!(root || document).querySelector && !!document.querySelector("[data-behavior]") && !!root && !!root.querySelector("[data-behavior]");
} catch (e) { return false; }
}
var PILOTS = FALLBACK_PILOTS;
function runBehaviors(ctx) {
if (!inScope(ctx.slug, ctx.root)) return [];
/* 幂等:手势会改变演示页状态,同一 page-load 内只跑一次(重载演示页即可重测) */
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), mobile: true };
})();