Files
wordpress-theme-aksrt/tests/widget-system/global-dom-init-contract.test.cjs
T
kate522 1f4a39d8f8 feat(runtime): 全站前端脚本统一接入 AksrtFrontend 全局 DOM 生命周期
- rehydrate.js 新增 boot()/aksrt:ready 事件,支持 boot 后晚注册立即以 init 触发
- aksrt-runtime.js 初始加载改走 AksrtFrontend.boot,tooltip/倒计时注册为组件
- 约 30 个前端脚本(首页、评论、支付、路由、页面叶子、widgets)
  从直接 DOMContentLoaded 迁移为 AksrtFrontend.register,并加幂等标记
- section-navs 依赖改为 aksrt-runtime,保证注册时序
- 清理死代码:删除孤儿 loader.js/main.js 及 runtime/page-navs 遗留副本
- loader.js 移除已迁移模块的 tbquire 路径映射,main.js 相应瘦身
- build-public-assets.mjs 增加 runtime/main.min.js 自动压缩步骤
- 新增 4 个契约测试(homepage-dom/rehydrate-runtime/global-dom-init/
  public-bundle-parity),3 个既有 harness 适配注册表初始化方式
2026-09-02 21:48:56 +08:00

118 lines
4.7 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const publicSources = [
'assets/js/core/comment-fixed-input.js',
'assets/js/core/misc.js',
'assets/js/follow.js',
'assets/js/enhancements.js',
'assets/js/enhancements2.js',
'assets/js/color-extract.js',
'assets/js/effects.js',
'assets/js/captcha-front.js',
'assets/widgets/frontend.js',
];
const routedSources = [
'assets/js/auth.js',
'assets/js/author.js',
'assets/js/routes/user-center.js',
'assets/js/order-center.js',
'assets/js/credits.js',
'assets/js/signin.js',
'assets/js/reward.js',
'assets/js/report.js',
'assets/js/page-navs.js',
'assets/js/section-navs.js',
'assets/js/posts-navs-page.js',
'assets/js/front-submit.js',
'assets/js/mini-upload.js',
'assets/js/page-template.js',
'assets/js/page-edit.js',
'assets/js/precode.js',
'assets/js/post-share.js',
'assets/js/poster-share.js',
'assets/js/pay-vip.js',
'assets/js/video-player.js',
'assets/js/download-status.js',
'assets/js/ban-appeal-page.js',
'assets/js/docs-navigation.js',
'assets/js/input-expand.js',
'assets/js/waterfall.js',
'assets/js/yiyan.js',
'assets/js/messages.js',
'assets/js/site-notice.js',
'assets/js/user-badge-actions.js',
];
const readyPattern = /(?:document\.addEventListener\(\s*['"]DOMContentLoaded['"]|document\.readyState\s*===|jQuery\(document\)\.ready\s*\()/;
test('global DOM boot is the only initial page lifecycle owner', () => {
const runtime = read('assets/js/aksrt-runtime.js');
const registry = read('assets/js/rehydrate.js');
assert.match(registry, /boot:\s*function/);
assert.match(registry, /register:[\s\S]*?_booted/);
assert.match(runtime, /AksrtFrontend\.boot\(root\)/);
assert.match(runtime, /aksrt\.autoFun\(document, 'init'\)/);
assert.match(runtime, /reason === 'init'[\s\S]*?AksrtFrontend\.boot\(root\)[\s\S]*?else[\s\S]*?AksrtFrontend\.rehydrate\(root, reason\)/);
});
test('public bundle components register instead of owning DOM ready', () => {
for (const file of publicSources) {
const source = read(file);
assert.doesNotMatch(source, readyPattern, `${file} still owns a DOM-ready boot`);
assert.match(source, /AksrtFrontend\.register\(/, `${file} must register with the global lifecycle`);
}
});
test('active routed UI scripts register with the global lifecycle', () => {
for (const file of routedSources) {
const source = read(file);
if (/API|coordinator|runtime/i.test(source.slice(0, 500)) && !/querySelector|addEventListener/.test(source)) continue;
assert.doesNotMatch(source, readyPattern, `${file} still owns a DOM-ready boot`);
assert.match(source, /AksrtFrontend\.register\(/, `${file} must register with the global lifecycle`);
}
});
test('high-risk routed components remain idempotent under global rehydrate', () => {
const pageNavs = read('assets/js/page-navs.js');
const sectionNavs = read('assets/js/section-navs.js');
const poster = read('assets/js/poster-share.js');
assert.match(pageNavs, /__aksrtPageNavInitialized/);
assert.match(pageNavs, /var grid = self\.root\.querySelector\('\.aksrt-doc-grid'\)/);
assert.match(sectionNavs, /if \(scrollspyBound\) return/);
assert.match(sectionNavs, /if \(autoHashHandled \|\| !window\.location\.hash\) return/);
assert.match(poster, /triggers\.filter\(function \(el\) \{ return el\.dataset\.posterInit !== '1'; \}\)/);
assert.doesNotMatch(poster, /triggers\.some\(function \(el\) \{ return el\.dataset\.posterInit === '1'; \}\)/);
});
test('resource and SDK load listeners are not mistaken for page boot', () => {
const captcha = read('assets/js/captcha-front.js');
const color = read('assets/js/color-extract.js');
assert.match(captcha, /\.onload\s*=|addEventListener\(['"]load['"]/);
assert.match(color, /addEventListener\(['"]load['"]/);
assert.doesNotMatch(captcha, /DOMContentLoaded/);
assert.doesNotMatch(color, /DOMContentLoaded/);
});
test('route scripts that use the registry load after the public runtime', () => {
const scripts = read('src/Core/ScriptsPart1b.php') + read('src/Core/ScriptsPart1c.php');
const files = ['section-navs.js', 'page-navs.js', 'posts-navs-page.js', 'author.js', 'auth.js'];
for (const file of files) {
const uri = scripts.indexOf("/assets/js/" + file);
assert.notEqual(uri, -1, `${file} must be enqueued`);
const blockStart = scripts.lastIndexOf('wp_enqueue_script(', uri);
const blockEnd = scripts.indexOf(');', uri);
const block = scripts.slice(blockStart, blockEnd + 2);
assert.match(block, /aksrt-runtime/, `${file} must depend on aksrt-runtime`);
}
});