;提示行(submessage_row)还会把同一份 data-dep 与隐藏状态
// 额外挂在 | 内的提示块上,因此这里也为它建第二个元素(模拟提示块脱离行的情况)。
const rows = [];
for (const row of addRows) {
const json = JSON.stringify(row.deps);
const makeEl = () => ({ style: {}, getAttribute: (n) => (n === 'data-dep' ? json : null) });
rows.push({ label: row.label, deps: row.deps, el: makeEl() });
if (HINTS.includes(row.label)) rows.push({ label: row.label, deps: row.deps, el: makeEl() });
}
const handlers = {};
const document = {
readyState: 'loading',
querySelectorAll(sel) {
if (sel === '.aksrt-cardpass-form [name]') return inputs;
if (sel === '[data-dep]') return rows.map((row) => row.el);
return [];
},
addEventListener(type, fn) { (handlers[type] = handlers[type] || []).push(fn); },
};
vm.runInNewContext(script, { document, navigator: {}, console });
const fire = (type, event) => (handlers[type] || []).forEach((fn) => fn(event || {}));
const setRadio = (name, value) => inputs.forEach((el) => {
if (el.getAttribute('name') === name) el.checked = el.value === value;
});
const setChecked = (name, on) => inputs.forEach((el) => {
if (el.getAttribute('name') === name) el.checked = !!on;
});
fire('DOMContentLoaded');
const visible = () => [...new Set(rows.filter((row) => row.el.style.display !== 'none').map((row) => row.label))];
/** 提示的「行 + 提示块」两层各自的显示状态,用于验证两层不会分叉。 */
const hintLayers = () => rows
.filter((row) => HINTS.includes(row.label))
.map((row) => row.label + ':' + (row.el.style.display === 'none' ? 'off' : 'on'));
return {
setRadio, setChecked, visible, hintLayers,
change() { fire('change', { target: { closest: () => ({}) } }); },
hintVisible: () => visible().filter((label) => HINTS.includes(label)),
count: rows.length,
};
}
test('依赖键必须在 is_known_dep_key 与 default_form_value 中成对登记', () => {
const body = functionBody(page, 'private function is_known_dep_key(');
const known = new Set([...body.matchAll(/'([^']+)'/g)].map((m) => m[1]));
const defaults = new Set(
[...functionBody(page, 'private function default_form_value(').matchAll(/\bcase\s+'([^']+)':/g)].map((m) => m[1])
);
assert.ok(allRows.length > 0, '未解析到任何依赖行');
const used = new Set(allRows.flatMap((row) => row.deps.map((d) => d.key)));
assert.ok(used.size >= 6, '依赖键数量异常:' + used.size);
for (const key of used) {
assert.ok(known.has(key), '依赖键未登记到 is_known_dep_key():' + key);
assert.ok(defaults.has(key), '依赖键缺少 default_form_value() 的显式分支:' + key);
}
});
test('两层求值器实现同一组操作符,且覆盖表单用到的全部操作符', () => {
const serverOps = new Set([...functionBody(page, 'private function deps_visible(').matchAll(/case\s+'([^']+)':/g)].map((m) => m[1]));
const clientOps = new Set([...script.matchAll(/case\s+'([^']+)':/g)].map((m) => m[1]));
for (const op of ['==', '!=', 'any', 'checked', 'unchecked']) {
assert.ok(serverOps.has(op), 'deps_visible() 缺少操作符:' + op);
assert.ok(clientOps.has(op), 'evalDeps() 缺少操作符:' + op);
}
for (const row of allRows) {
for (const dep of row.deps) {
assert.ok(serverOps.has(dep.op), '表单使用了服务端未实现的操作符:' + dep.op);
assert.ok(clientOps.has(dep.op), '表单使用了前端未实现的操作符:' + dep.op);
}
}
});
test('两种行渲染都接入服务端预隐藏,避免首帧铺开', () => {
assert.match(page, /private function field_row\([\s\S]*?\$hidden\s*=\s*\(\$deps && !\$this->deps_visible\(\$deps\)\)/);
assert.match(page, /private function submessage_row\([\s\S]*?\$hidden\s*=\s*\$this->deps_visible\(\$deps\)/);
assert.match(script, /document\.addEventListener\('DOMContentLoaded', evalDeps\)/);
// 提示块必须自带 data-dep 与初始隐藏状态:即使提示块脱离所在行(WP 的
// wp-admin/js/common.js 会把页面上的 notice 搬到页头,见下一条用例),
// 它仍能被求值并保持隐藏。
assert.match(page, / | {
// wp-admin/js/common.js 在 DOM 就绪时执行:
// $('div.updated, div.error, div.notice').not('.inline, .below-h2').insertAfter($headerEnd)
// 会把页面上所有非 inline 的 notice 搬到 .wp-header-end(不存在时取 .wrap 内首个 h1/h2)之后。
// 提示块一旦带 .notice,就会脱离「添加方式—卡密数据」之间的字段区跑到页面顶部,
// 且搬迁发生在 evalDeps 之后,行内显隐逻辑无法纠正(历史故障:提示一直显示在页面最上方)。
assert.match(
page,
/private function submessage_row\([\s\S]*?class="aksrt-submsg"/,
'提示块只应带自有类名 aksrt-submsg'
);
assert.doesNotMatch(
page,
/class="aksrt-submsg[^"]*\bnotice\b/,
'提示块不得带 notice 类:会被 common.js 搬到页面顶部'
);
// 外观改由自有类名承担,删掉样式规则会让提示失去底色与左侧色条。
assert.match(
page,
/\.aksrt-submsg \{[^}]*background:[^}]*border-left:/,
'.aksrt-submsg 必须自带 background 与 border-left 外观'
);
// 允许(且要求)真正需要置顶的提示保留 notice:确认本用例没有误伤其它通知输出。
assert.match(page, /class="notice notice-error"/, '页面级错误通知仍应使用 notice');
});
test('导入提示的条目列表由 承载,圆点不越出文字边界', () => {
// list-style-position 默认 outside:圆点画在内容盒左侧的 padding 区里。
// - 若没有列表容器提供缩进(历史上的写法是裸
- 直接挂在 div 下),
// padding 区为空,圆点就会溢出到提示块的文字边界之外。
const calls = extractCalls(page, '->submessage_row(');
assert.ok(calls.length >= 4, '未解析到足够的 submessage_row 调用:' + calls.length);
let checked = 0;
for (const call of calls) {
const opens = [...call.text.matchAll(/
- /g)].length;
if (!opens) continue;
checked++;
assert.match(
call.text,
/
/,
'提示内的 - 必须包在
中'
);
assert.equal([...call.text.matchAll(//g)].length, 1, '每条提示只应有一个条目列表');
assert.equal(opens, [...call.text.matchAll(/<\/li>/g)].length, '- 标签必须成对');
assert.equal([...call.text.matchAll(/<\/ul>/g)].length, 1, '
必须成对');
}
assert.equal(checked, 4, '四条导入提示都应带条目列表');
assert.match(page, /\.aksrt-submsg-list \{[^}]*padding-left:/, '.aksrt-submsg-list 必须给出 padding-left,圆点才有落点');
assert.match(page, /\.aksrt-submsg-list \{[^}]*list-style:\s*disc/, '.aksrt-submsg-list 应显式声明圆点样式,不依赖 UA 默认值');
});
test('行内脚本不再保留解析期的空操作求值(死代码)', () => {
assert.doesNotMatch(script, /evalDeps\(\);\s*\}\)\(\);/);
assert.match(script, /document\.addEventListener\('change'/);
});
test('添加卡密:导入提示跟随「卡密类型 / 添加方式」切换,且不同时铺开', () => {
const form = createForm();
assert.equal(form.count, 21, '添加卡密的依赖元素数量发生变化(13 行 + 4 提示块×2),请同步本测试');
assert.deepEqual(form.hintVisible(), [], '默认(系统自动生成)下不应显示任何导入提示');
assert.ok(form.visible().includes('生成数量'));
assert.ok(!form.visible().includes('卡密数据'));
const cases = [
{ type: 'balance', hint: '导入卡密' },
{ type: 'credits', hint: '导入卡密' },
{ type: 'vip', hint: '导入会员兑换卡密' },
];
for (const { type, hint } of cases) {
form.setRadio('type', type);
form.setRadio('add_type', 'import');
form.change();
assert.deepEqual(form.hintVisible(), [hint], type + ' + 导入卡密 只应出现对应提示');
assert.ok(form.visible().includes('卡密数据'), type + ' + 导入卡密 应显示卡密数据');
assert.ok(!form.visible().includes('生成数量'), type + ' + 导入卡密 不应显示自动生成字段');
}
form.setRadio('type', 'custom');
form.change();
assert.deepEqual(form.hintVisible(), ['自定义卡密导入']);
assert.ok(form.visible().includes('单密码模式'));
form.setChecked('only_pass_s', true);
form.change();
assert.deepEqual(form.hintVisible(), ['自定义卡密导入(单密码模式)']);
form.setChecked('only_pass_s', false);
form.change();
assert.deepEqual(form.hintVisible(), ['自定义卡密导入']);
form.setRadio('add_type', 'auto');
form.setRadio('type', 'balance');
form.change();
assert.deepEqual(form.hintVisible(), [], '切回系统自动生成后导入提示必须全部收起');
form.setChecked('auto_top_s', true);
form.change();
assert.ok(form.visible().includes('自定义卡号位数'), '高级选项勾选后应显示自定义位数');
});
test('提示块与所在行同层落 display,提示块脱离行也不会失效', () => {
const form = createForm();
const HINT_LABELS = ['导入卡密', '导入会员兑换卡密', '自定义卡密导入', '自定义卡密导入(单密码模式)'];
const layersFor = (labels) => HINT_LABELS.flatMap((l) => [l + ':' + (labels.includes(l) ? 'on' : 'off'), l + ':' + (labels.includes(l) ? 'on' : 'off')]);
// 默认态(系统自动生成 + 余额充值):两层共 8 个元素必须全部隐藏。
assert.deepEqual(
form.hintLayers(),
HINT_LABELS.flatMap((l) => [l + ':off', l + ':off']),
'默认态下每条提示的行与提示块都必须隐藏'
);
form.setRadio('type', 'vip');
form.setRadio('add_type', 'import');
form.change();
assert.deepEqual(form.hintLayers(), layersFor(['导入会员兑换卡密']), '只有命中依赖的那条提示的两层应当显示');
form.setRadio('add_type', 'auto');
form.setRadio('type', 'balance');
form.change();
assert.deepEqual(form.hintLayers(), HINT_LABELS.flatMap((l) => [l + ':off', l + ':off']), '切回默认后两层必须同时收起');
});
|