Files
aurora-admin/tests/_behaviors.js
T
aurora-admin 7e3d442dab
Deploy to GitHub Pages / deploy (push) Failing after 2m30s
Regression / regression (push) Failing after 53s
feat(S2-P7+S3-P8): 行为断言6试点全绿+RTL28文件迁移/回归1009-1009
P7: tests/_behaviors.js新建(8动词+@input/@doc定位+同步pump+page-load
    幂等缓存); _runtime聚合+重载清缓存; _template引入; 6演示页标注;
    79测试页重生成。关键修:runner兜底重跑致行为二轮误报,加缓存解决。
P8: tools/migrate-rtl.mjs新建(白名单dry-run/--write); 28个CSS转逻辑
    属性;物理清零(排除auto/拼接/居中14处人工例外);三页RTL零溢出。
回归:100%(79/79页,1009/1009断言,N/A 35)。
2026-09-14 03:37:08 +08:00

231 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.
/* Aurora Admin 测试页 — 行为断言库(S2-P7)
*
* 目标:把测试从"元素存在"升级到"交互后发生什么",验证点击/输入/键盘的真实行为。
* 运行环境:tests/<slug>.html 的帧内(与 _runtime.js 同作用域),直接操作演示页 DOM。
* 协议:在演示页 HTML 里用 data-behavior 声明行为,格式:
* <button data-behavior="click-toggles-class:.aa-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 翻转)
* 无 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 .au-input 内找 input),避免命中页内第一个 input
var n;
if (sel.charAt(0) === '@') {
var host = trig.closest ? trig.closest('.au-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, '.aa-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 };
}
};
/* sulg 白名单:只在 6 个试点组件跑行为断言,避免全量 79 页引入不稳定 */
var PILOTS = { button: 1, modal: 1, select: 1, input: 1, table: 1, tabs: 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.__aaBehCache && window.__aaBehCache.slug === ctx.slug) return window.__aaBehCache.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.__aaBehCache = { slug: ctx.slug, out: out };
return out;
}
window.__aaBehaviors = { run: runBehaviors, verbs: Object.keys(VERBS), pilots: Object.keys(PILOTS) };
})();