Files
wordpress-theme-aksrt/tests/admin/admin-build-graph-contract.test.cjs
T
kate522 324bcfdadc fix(admin): verify the whole SPA build graph, not just the entry
find_missing_admin_chunks() only parsed index.js, but a lazily
imported page chunk pulls in its own deps (shared-api, PageHeader,
sortable-options…). A deploy that drops one of those still leaves the
entry internally consistent, so the existing notice never fires and the
failure only shows up when an admin opens that menu — every schemaPage
keeps working, which makes the breakage look arbitrary.

The same scan never looked at CSS at all: 18 stylesheets are reachable
from the graph and none of them were checked, so a missing page
stylesheet ships an unstyled screen with no warning.

Walk the module graph transitively from index.js, queueing each
discovered dependency, and match .js and .css specifiers. Deleting
PageHeader now also reports every page that depends on it.

This is a diagnostic hardening only — it makes an incomplete deploy loud
instead of guessing at the cause of a broken page.
2026-11-01 13:41:58 +08:00

79 lines
3.2 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.
'use strict';
/**
* 后台 SPA 构建完整性检查契约。
*
* src/Admin/Admin.php::find_missing_admin_chunks() 必须遍历整张模块图,
* 而不是只扫 index.js:懒加载的页面 chunk 还会拉自己的依赖
* (shared-api、PageHeader…),只扫入口时这些文件缺失照样"看起来正常",
* 直到用户在后台点开对应菜单才 404。CSS 也必须纳入——入口扫描从来不检查
* 样式表,缺一个页面样式只会让后台变裸样式,不会有任何告警。
*/
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 adminPhp = fs.readFileSync(path.join(root, 'src/Admin/Admin.php'), 'utf8');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const methodBody = (() => {
const start = adminPhp.indexOf('private static function find_missing_admin_chunks');
assert.notEqual(start, -1, 'find_missing_admin_chunks() must exist in src/Admin/Admin.php');
const open = adminPhp.indexOf('{', start);
let depth = 0;
for (let i = open; i < adminPhp.length; i++) {
if (adminPhp[i] === '{') depth++;
else if (adminPhp[i] === '}') {
depth--;
if (depth === 0) return adminPhp.slice(open, i + 1);
}
}
throw new Error('unbalanced braces parsing find_missing_admin_chunks()');
})();
test('the chunk check walks the module graph instead of scanning the entry only', () => {
// A work queue + visited set is what makes the walk transitive.
assert.match(methodBody, /\$queue/, 'must seed a traversal queue');
assert.match(methodBody, /\$visited/, 'must track visited files to terminate');
assert.match(methodBody, /while\s*\(/, 'must loop until the queue drains');
// The dependency of a chunk must itself be re-read and followed.
assert.match(methodBody, /\$queue\[\]\s*=\s*\$dep/, 'discovered deps must be queued for traversal');
});
test('the chunk check covers CSS as well as JS', () => {
// A missing page stylesheet used to be invisible; the specifier must match both.
assert.match(methodBody, /\.\(\?:js\|css\)/, 'specifier pattern must match .js and .css');
});
test('a missing entry file is reported rather than silently passing', () => {
assert.match(methodBody, /!is_file\(\$entry_file\)/, 'must guard an absent index.js');
});
test('the real build graph is complete', () => {
// Guard against committing a build/ whose graph does not resolve.
const buildDir = path.join(root, 'assets/admin/build');
if (!fs.existsSync(path.join(buildDir, 'index.js'))) {
// build/ is gitignored and deploy-only; nothing to assert when absent.
return;
}
const files = new Set(fs.readdirSync(buildDir));
const referenced = new Set();
const walk = (file) => {
if (referenced.has(file)) return;
referenced.add(file);
if (!file.endsWith('.js') || !files.has(file)) return;
const src = fs.readFileSync(path.join(buildDir, file), 'utf8');
for (const m of src.matchAll(/\.\/([A-Za-z0-9_.-]+\.(?:js|css))/g)) {
walk(m[1]);
}
};
walk('index.js');
const missing = [...referenced].filter((f) => !files.has(f));
assert.deepEqual(missing, [], `build/ is missing: ${missing.join(', ')}`);
});