Files
001code-html--cocos/scratch-gui/scripts/test-devtool-control.cjs
2026-09-17 11:53:14 +08:00

167 lines
7.5 KiB
JavaScript

// Run: npm run test:devtool
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const babel = require('@babel/core');
const root = path.resolve(__dirname, '..');
const controlPath = 'src/playground/devtool-control.js';
const compile = file => babel.transformFileSync(path.join(root, file), {
configFile: false, babelrc: false, presets: ['@babel/preset-env', '@babel/preset-react']
}).code;
const controlCode = compile(controlPath);
const homeCode = compile('src/playground/home.js');
let checks = 0;
function test(name, action) {
action();
checks++;
console.log(`PASS ${name}`);
}
function page(storage = new Map(), {enabled = true, brokenStorage = false, immediate = false} = {}) {
const starts = [];
const effects = [];
function detector(options) {
starts.push(options);
// Simulate a library probe firing during initialization with F12 already open.
if (immediate) options.ondevtoolopen('already-open');
}
detector.isSuspend = false;
const window = {
location: {href: '/editor.html'}, onbeforeunload: () => {},
cppCodeStopRun: () => effects.push('stop C++'),
loadCppProject: () => effects.push('clear C++'),
pythonCodeStopRun: () => effects.push('stop Python'),
loadPythonProject: () => effects.push('clear Python'),
unityInstance: {Quit: () => effects.push('quit engine')}
};
const sessionStorage = {
getItem(key) {if (brokenStorage) throw new Error('storage denied'); return storage.get(key) || null;},
setItem(key, value) {if (brokenStorage) throw new Error('storage denied'); storage.set(key, value);}
};
const sandbox = {
exports: {}, window, sessionStorage,
localStorage: {getItem: () => '/index.html'},
vm: {
stopAll: () => effects.push('stop Scratch'),
editingTarget: {blocks: {deleteAllBlocks: () => effects.push('clear Scratch')}},
emitWorkspaceUpdate: () => effects.push('update workspace')
},
require(name) {
if (name === 'disable-devtool') return detector;
if (name === './config.js') return {disabledev: enabled};
if (name === './devtool-control.js') return api;
throw new Error(`Unexpected import: ${name}`);
}
};
const context = vm.createContext(sandbox);
vm.runInContext(controlCode, context, {filename: controlPath});
const api = sandbox.exports;
return {api, starts, effects, detector, window, context};
}
// Use the actual options and destructive callback from each editor mode.
function stageOptions(file) {
const source = fs.readFileSync(path.join(root, file), 'utf8');
const ast = babel.parseSync(source, {configFile: false, babelrc: false, parserOpts: {plugins: ['jsx']}});
let options;
function visit(node) {
if (!node || typeof node !== 'object') return;
if (node.type === 'ImportDeclaration') {
assert.notStrictEqual(node.source.value, 'disable-devtool', `${file} bypasses shared control`);
}
if (node.type === 'CallExpression' && node.callee.name === 'initDisableDevtool') options = node.arguments[0];
for (const value of Object.values(node)) {
if (Array.isArray(value)) value.forEach(visit);
else if (value && typeof value === 'object') visit(value);
}
}
visit(ast);
assert(options, `${file} must initialize through shared control`);
// Also verify the entire JSX file remains compatible with project Babel presets.
compile(file);
return source.slice(options.start, options.end);
}
const storage = new Map();
test('home exposes stopDev and persists it for navigation', () => {
const home = page(storage);
vm.runInContext(homeCode, home.context);
home.window.stopDev();
assert.strictEqual(storage.get('stopDev'), '1');
assert.strictEqual(home.detector.isSuspend, true);
});
for (const mode of ['stage-unity', 'stage-python-unity', 'stage-cpp-unity']) {
const optionSource = stageOptions(`src/components/${mode}/stage-unity.jsx`);
test(`${mode}: home -> editor with F12 open does not start detection or clear code`, () => {
const editor = page(storage, {immediate: true});
const options = vm.runInContext(`(${optionSource})`, editor.context);
editor.api.initDisableDevtool(options);
assert.strictEqual(editor.starts.length, 0);
assert.strictEqual(editor.detector.isSuspend, true);
assert.strictEqual(editor.window.location.href, '/editor.html');
assert.deepStrictEqual(editor.effects, []);
// A refresh or another mode initialization in this tab must stay stopped.
editor.api.initDisableDevtool(options);
assert.strictEqual(editor.starts.length, 0);
});
test(`${mode}: stopDev blocks an already queued destructive callback`, () => {
const editor = page();
const options = vm.runInContext(`(${optionSource})`, editor.context);
editor.api.initDisableDevtool(options);
assert.strictEqual(editor.starts.length, 1);
const callback = editor.starts[0];
editor.window.stopDev();
assert.strictEqual(callback.ignore(), true);
callback.ondevtoolopen('queued');
assert.strictEqual(editor.window.location.href, '/editor.html');
assert.deepStrictEqual(editor.effects, []);
});
}
test('normal session retains default detection and redirect', () => {
const normal = page();
normal.api.initDisableDevtool();
assert.strictEqual(normal.starts.length, 1);
assert.strictEqual(normal.starts[0].ignore(), false);
normal.starts[0].ondevtoolopen();
assert.strictEqual(normal.window.location.href, '/index.html');
});
test('custom options, ignore and callback arguments remain intact', () => {
const normal = page();
const seen = [];
const next = () => {};
normal.api.initDisableDevtool({interval: 123, ignore: () => true, ondevtoolopen: (...args) => seen.push(args)});
assert.strictEqual(normal.starts[0].interval, 123);
assert.strictEqual(normal.starts[0].ignore(), true);
normal.starts[0].ondevtoolopen(7, next);
assert.deepStrictEqual(seen, [[7, next]]);
});
test('disabled configuration does not initialize; stopDev remains available', () => {
const development = page(new Map(), {enabled: false});
development.api.initDisableDevtool();
assert.strictEqual(development.starts.length, 0);
assert.strictEqual(typeof development.window.stopDev, 'function');
});
test('blocked storage still permits immediate stop on current page', () => {
const broken = page(new Map(), {brokenStorage: true});
broken.api.initDisableDevtool();
broken.window.stopDev();
assert.strictEqual(broken.detector.isSuspend, true);
assert.strictEqual(broken.starts[0].ignore(), true);
broken.starts[0].ondevtoolopen();
broken.api.initDisableDevtool();
assert.strictEqual(broken.starts.length, 1);
assert.strictEqual(broken.window.location.href, '/editor.html');
});
for (const file of ['src/playground/competition.js', 'src/playground/competition_sum.js']) {
const optionsSource = stageOptions(file);
test(`${file}: same stopDev state is honored`, () => {
const competition = page(storage, {immediate: true});
competition.api.initDisableDevtool(vm.runInContext(`(${optionsSource})`, competition.context));
assert.strictEqual(competition.starts.length, 0);
});
}
console.log(`${checks} devtool regression checks passed`);