372 lines
11 KiB
JavaScript
372 lines
11 KiB
JavaScript
const {test} = require('node:test');
|
|
const assert = require('node:assert');
|
|
const Runtime = require('../../src/engine/runtime');
|
|
const MonitorRecord = require('../../src/engine/monitor-record');
|
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
|
const Target = require('../../src/engine/target');
|
|
|
|
test('setFramerate emits an event', t => {
|
|
t.plan(1);
|
|
const rt = new Runtime();
|
|
rt.addListener('FRAMERATE_CHANGED', framerate => {
|
|
if (framerate === 13) {
|
|
t.assert.ok(true);
|
|
}
|
|
});
|
|
rt.setFramerate(13);
|
|
});
|
|
|
|
test('setFramerate and setCompatibilityMode do not emit a stop event if not running', () => {
|
|
const rt = new Runtime();
|
|
rt.addListener('RUNTIME_STOPPED', () => {
|
|
assert.fail();
|
|
});
|
|
rt.setFramerate(13);
|
|
rt.setCompatibilityMode(true);
|
|
});
|
|
|
|
test('setInterpolation emits an event', t => {
|
|
t.plan(1);
|
|
const rt = new Runtime();
|
|
rt.addListener('INTERPOLATION_CHANGED', enabled => {
|
|
if (enabled) {
|
|
t.assert.ok(true);
|
|
}
|
|
});
|
|
rt.setInterpolation(true);
|
|
});
|
|
|
|
test('setInterpolation does not restart runtime if not running', () => {
|
|
const rt = new Runtime();
|
|
let started = false;
|
|
let stopped = false;
|
|
rt.addListener('RUNTIME_STARTED', () => {
|
|
started = true;
|
|
});
|
|
rt.addListener('RUNTIME_STOPPED', () => {
|
|
stopped = true;
|
|
});
|
|
rt.setInterpolation(true);
|
|
assert.strictEqual(started, false);
|
|
assert.strictEqual(stopped, false);
|
|
});
|
|
|
|
test('Stopping the runtime emits an event', () => {
|
|
const rt = new Runtime();
|
|
rt.start();
|
|
let stopped = false;
|
|
rt.addListener('RUNTIME_STOPPED', () => {
|
|
stopped = true;
|
|
});
|
|
rt.stop();
|
|
assert.strictEqual(stopped, true);
|
|
});
|
|
|
|
test('Stop does not emit an event if already stopped', () => {
|
|
const rt = new Runtime();
|
|
let stopped = false;
|
|
rt.addListener('RUNTIME_STOPPED', () => {
|
|
stopped = true;
|
|
});
|
|
rt.stop();
|
|
assert.strictEqual(stopped, false);
|
|
});
|
|
|
|
test('setRuntimeOptions emits an event', t => {
|
|
t.plan(1);
|
|
const rt = new Runtime();
|
|
rt.addListener('RUNTIME_OPTIONS_CHANGED', options => {
|
|
if (options.option === 17) {
|
|
t.assert.ok(true);
|
|
}
|
|
});
|
|
rt.setRuntimeOptions({option: 17});
|
|
});
|
|
|
|
test('setRuntimeOptions supports partial updates', t => {
|
|
t.plan(1);
|
|
const rt = new Runtime();
|
|
rt.setRuntimeOptions({option: 17});
|
|
rt.addListener('RUNTIME_OPTIONS_CHANGED', options => {
|
|
if (options.option === 17) {
|
|
t.assert.ok(true);
|
|
}
|
|
});
|
|
rt.setRuntimeOptions({otherOption: 1});
|
|
});
|
|
|
|
test('setCompilerOptions emits an event', t => {
|
|
t.plan(1);
|
|
const rt = new Runtime();
|
|
rt.addListener('COMPILER_OPTIONS_CHANGED', options => {
|
|
if (options.option === 17) {
|
|
t.assert.ok(true);
|
|
}
|
|
});
|
|
rt.setCompilerOptions({option: 17});
|
|
});
|
|
|
|
test('setCompilerOptions supports partial updates', t => {
|
|
t.plan(1);
|
|
const rt = new Runtime();
|
|
rt.setCompilerOptions({option: 17});
|
|
rt.addListener('COMPILER_OPTIONS_CHANGED', options => {
|
|
if (options.option === 17) {
|
|
t.assert.ok(true);
|
|
}
|
|
});
|
|
rt.setCompilerOptions({otherOption: 1});
|
|
});
|
|
|
|
test('maxClones runtime option', () => {
|
|
const rt = new Runtime();
|
|
rt.setRuntimeOptions({maxClones: 10});
|
|
for (let i = 0; i < 10; i++) {
|
|
assert.strictEqual(rt.clonesAvailable(), true);
|
|
rt.changeCloneCounter(1);
|
|
}
|
|
rt.changeCloneCounter(1);
|
|
assert.strictEqual(rt.clonesAvailable(), false);
|
|
});
|
|
|
|
test('stageWidth and stageHeight', () => {
|
|
const rt = new Runtime();
|
|
assert.strictEqual(rt.stageWidth, 480);
|
|
assert.strictEqual(rt.stageHeight, 360);
|
|
});
|
|
|
|
test('debug', () => {
|
|
const rt = new Runtime();
|
|
assert.strictEqual(rt.debug, false);
|
|
rt.enableDebug();
|
|
assert.strictEqual(rt.debug, true);
|
|
});
|
|
|
|
test('setStageSize preserves monitor position relative to center of stage', () => {
|
|
const rt = new Runtime();
|
|
rt.requestAddMonitor(new MonitorRecord({
|
|
id: 'abc',
|
|
// top right corner
|
|
x: 0,
|
|
y: 0
|
|
}));
|
|
rt.setStageSize(640, 362);
|
|
const finalState = rt.getMonitorState().get('abc');
|
|
assert.strictEqual(finalState.get('x'), 80);
|
|
assert.strictEqual(finalState.get('y'), 1);
|
|
});
|
|
|
|
test('setStageSize argument range', t => {
|
|
t.plan(6);
|
|
const rt = new Runtime();
|
|
|
|
rt.once('STAGE_SIZE_CHANGED', (width, height) => {
|
|
t.assert.strictEqual(width, 101);
|
|
t.assert.strictEqual(height, 103);
|
|
});
|
|
rt.setStageSize(101, 103);
|
|
|
|
rt.once('STAGE_SIZE_CHANGED', (width, height) => {
|
|
t.assert.strictEqual(width, 1);
|
|
t.assert.strictEqual(height, 1);
|
|
});
|
|
rt.setStageSize(-3.1, 0);
|
|
|
|
rt.once('STAGE_SIZE_CHANGED', (width, height) => {
|
|
t.assert.strictEqual(width, 99);
|
|
t.assert.strictEqual(height, 10000);
|
|
});
|
|
rt.setStageSize(99.3, 10000);
|
|
});
|
|
|
|
test('STAGE_SIZE_CHANGED does not fire if no change', () => {
|
|
const rt = new Runtime();
|
|
rt.on('STAGE_SIZE_CHANGED', () => {
|
|
assert.fail('STAGE_SIZE_CHANGED emitted');
|
|
});
|
|
rt.setStageSize(rt.stageWidth, rt.stageHeight);
|
|
});
|
|
|
|
test('getNumberOfCloudVariables', () => {
|
|
const rt = new Runtime();
|
|
|
|
assert.strictEqual(rt.getNumberOfCloudVariables(), 0);
|
|
rt.addCloudVariable();
|
|
assert.strictEqual(rt.getNumberOfCloudVariables(), 1);
|
|
rt.addCloudVariable();
|
|
assert.strictEqual(rt.getNumberOfCloudVariables(), 2);
|
|
rt.removeCloudVariable();
|
|
assert.strictEqual(rt.getNumberOfCloudVariables(), 1);
|
|
rt.removeCloudVariable();
|
|
assert.strictEqual(rt.getNumberOfCloudVariables(), 0);
|
|
|
|
rt.dispose();
|
|
assert.strictEqual(rt.getNumberOfCloudVariables(), 0);
|
|
rt.addCloudVariable();
|
|
assert.strictEqual(rt.getNumberOfCloudVariables(), 1);
|
|
});
|
|
|
|
test('currentStepTime default value', () => {
|
|
const rt = new Runtime();
|
|
assert.strictEqual(typeof rt.currentStepTime, 'number');
|
|
assert.ok(rt.currentStepTime > 0);
|
|
});
|
|
|
|
test('convertToPackagedRuntime', () => {
|
|
const rt = new Runtime();
|
|
assert.strictEqual(rt.isPackaged, false);
|
|
rt.convertToPackagedRuntime();
|
|
assert.strictEqual(rt.isPackaged, true);
|
|
});
|
|
|
|
test('convertToPackagedRuntime and attachStorage call order', () => {
|
|
const rt1 = new Runtime();
|
|
rt1.attachStorage(makeTestStorage());
|
|
assert.throws(() => {
|
|
rt1.convertToPackagedRuntime();
|
|
}, {
|
|
message: 'convertToPackagedRuntime must be called before attachStorage'
|
|
});
|
|
const rt2 = new Runtime();
|
|
rt2.convertToPackagedRuntime();
|
|
rt2.attachStorage(makeTestStorage());
|
|
});
|
|
|
|
test('visual report -0', t => {
|
|
t.plan(1);
|
|
|
|
const rt = new Runtime();
|
|
const target = new Target();
|
|
rt.setEditingTarget(target);
|
|
|
|
rt.on(Runtime.VISUAL_REPORT, report => {
|
|
t.assert.deepEqual(report, {
|
|
id: 'blockid',
|
|
value: '-0'
|
|
});
|
|
});
|
|
rt.visualReport(target, 'blockid', -0);
|
|
});
|
|
|
|
const makeMonitorBlock = () => ({
|
|
id: 'fakeMonitor',
|
|
opcode: 'data_variable',
|
|
fields: {
|
|
VARIABLE: {
|
|
name: 'VARIABLE',
|
|
value: 'x',
|
|
id: 'fakeMonitor'
|
|
}
|
|
},
|
|
inputs: {},
|
|
topLevel: true,
|
|
next: null,
|
|
parent: null,
|
|
shadow: false,
|
|
x: 0,
|
|
y: 0,
|
|
isMonitored: true,
|
|
targetId: null
|
|
});
|
|
|
|
test('_pushMonitors does nothing when no targets are installed', () => {
|
|
const rt = new Runtime();
|
|
rt.monitorBlocks.createBlock(makeMonitorBlock());
|
|
|
|
assert.strictEqual(rt.targets.length, 0);
|
|
rt._pushMonitors();
|
|
assert.strictEqual(rt.threads.length, 0);
|
|
});
|
|
|
|
test('_pushMonitors queues threads once targets are installed', () => {
|
|
const rt = new Runtime();
|
|
rt.monitorBlocks.createBlock(makeMonitorBlock());
|
|
|
|
const stage = new Target(rt);
|
|
rt.targets.push(stage);
|
|
rt.setEditingTarget(stage);
|
|
|
|
rt._pushMonitors();
|
|
assert.strictEqual(rt.threads.length, 1);
|
|
assert.strictEqual(rt.threads[0].target, stage);
|
|
assert.strictEqual(rt.threads[0].updateMonitor, true);
|
|
});
|
|
|
|
const makeCommandBlock = id => ({
|
|
id,
|
|
opcode: 'motion_movesteps',
|
|
fields: {},
|
|
inputs: {},
|
|
topLevel: true,
|
|
next: null,
|
|
parent: null,
|
|
shadow: false,
|
|
x: 0,
|
|
y: 0
|
|
});
|
|
|
|
test('toggleScript is scoped to its target when block IDs collide', () => {
|
|
const rt = new Runtime();
|
|
// Avoid compiling the fake single-block scripts.
|
|
rt.setCompilerOptions({enabled: false});
|
|
|
|
// Two sprites whose scripts share a top block ID. This happens when a block
|
|
// is copied between sprites without its ID being regenerated.
|
|
const spriteA = new Target(rt);
|
|
const spriteB = new Target(rt);
|
|
spriteA.blocks.createBlock(makeCommandBlock('dup'));
|
|
spriteB.blocks.createBlock(makeCommandBlock('dup'));
|
|
|
|
// Start the script on sprite A.
|
|
rt.toggleScript('dup', {target: spriteA, stackClick: true});
|
|
assert.strictEqual(rt.threads.length, 1, 'sprite A script started');
|
|
assert.strictEqual(rt.threads[0].target, spriteA);
|
|
|
|
// Clicking the same-ID script on sprite B must start B, not stop A.
|
|
rt.toggleScript('dup', {target: spriteB, stackClick: true});
|
|
assert.strictEqual(rt.threads.length, 2, 'sprite B script started as a second thread');
|
|
assert.ok(!rt.threads[0].isKilled, 'sprite A thread was not stopped');
|
|
assert.strictEqual(rt.threads[1].target, spriteB);
|
|
|
|
// Toggling B again stops only B, leaving A running.
|
|
rt.toggleScript('dup', {target: spriteB, stackClick: true});
|
|
assert.ok(!rt.threads[0].isKilled, 'sprite A thread still running');
|
|
assert.ok(rt.threads[1].isKilled, 'sprite B thread stopped');
|
|
});
|
|
|
|
test('toggleScript scopes colliding block IDs using the editing target', () => {
|
|
const rt = new Runtime();
|
|
rt.setCompilerOptions({enabled: false});
|
|
|
|
const spriteA = new Target(rt);
|
|
const spriteB = new Target(rt);
|
|
spriteA.blocks.createBlock(makeCommandBlock('dup'));
|
|
spriteB.blocks.createBlock(makeCommandBlock('dup'));
|
|
|
|
rt.setEditingTarget(spriteA);
|
|
rt.toggleScript('dup', {stackClick: true});
|
|
assert.strictEqual(rt.threads.length, 1, 'sprite A script started');
|
|
assert.strictEqual(rt.threads[0].target, spriteA);
|
|
|
|
rt.setEditingTarget(spriteB);
|
|
rt.toggleScript('dup', {stackClick: true});
|
|
assert.strictEqual(rt.threads.length, 2, 'sprite B script started as a second thread');
|
|
assert.ok(!rt.threads[0].isKilled, 'sprite A thread was not stopped');
|
|
assert.strictEqual(rt.threads[1].target, spriteB);
|
|
});
|
|
|
|
test('toggleScript still toggles a running script off on the same target', () => {
|
|
const rt = new Runtime();
|
|
rt.setCompilerOptions({enabled: false});
|
|
|
|
const sprite = new Target(rt);
|
|
sprite.blocks.createBlock(makeCommandBlock('top'));
|
|
|
|
rt.toggleScript('top', {target: sprite, stackClick: true});
|
|
assert.strictEqual(rt.threads.length, 1, 'script started');
|
|
assert.ok(!rt.threads[0].isKilled);
|
|
|
|
rt.toggleScript('top', {target: sprite, stackClick: true});
|
|
assert.ok(rt.threads[0].isKilled, 'clicking again stopped the script');
|
|
});
|