167 lines
7.5 KiB
JavaScript
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`);
|