Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 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 未做部分)
257 lines
11 KiB
JavaScript
257 lines
11 KiB
JavaScript
/* 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 };
|
||
})();
|