Initial commit of 001code-html Scratch frontend project.
Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
79
scratch-vm/test/integration/addSprite.js
Normal file
79
scratch-vm/test/integration/addSprite.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
test('spec', t => {
|
||||
t.type(vm.addSprite, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('default cat', t => {
|
||||
// Get default cat from .sprite2
|
||||
const uri = path.resolve(__dirname, '../fixtures/example_sprite.sprite2');
|
||||
const sprite = readFileToBuffer(uri);
|
||||
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
t.doesNotThrow(() => {
|
||||
vm.loadProject(project).then(() => {
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2); // stage and default sprite
|
||||
|
||||
// Add another sprite
|
||||
vm.addSprite(sprite).then(() => {
|
||||
const targets = vm.runtime.targets;
|
||||
|
||||
// Test
|
||||
t.type(targets, 'object');
|
||||
t.equal(targets.length, 3);
|
||||
|
||||
const newTarget = targets[2];
|
||||
|
||||
t.ok(newTarget instanceof RenderedTarget);
|
||||
t.type(newTarget.id, 'string');
|
||||
t.type(newTarget.blocks, 'object');
|
||||
t.type(newTarget.variables, 'object');
|
||||
const varIds = Object.keys(newTarget.variables);
|
||||
t.type(varIds.length, 1);
|
||||
const variable = newTarget.variables[varIds[0]];
|
||||
t.equal(variable.name, 'foo');
|
||||
t.equal(variable.value, 0);
|
||||
|
||||
t.equal(newTarget.isOriginal, true);
|
||||
t.equal(newTarget.currentCostume, 0);
|
||||
t.equal(newTarget.isOriginal, true);
|
||||
t.equal(newTarget.isStage, false);
|
||||
t.equal(newTarget.sprite.name, 'Apple');
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
setTimeout(() => {
|
||||
t.equal(variable.value, 10);
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/block-to-workspace-comments.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('importing sb2 project where block comment is converted to workspace comment and block is deleted', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.equal(threads.length, 0);
|
||||
|
||||
const target = vm.runtime.targets[1];
|
||||
|
||||
// Sprite 1 has 3 Comments, 1 block comment and 2 workspace comments (which were
|
||||
// originally created via a block comment to workspace comment conversion in Scratch 2.0).
|
||||
const targetComments = Object.values(target.comments);
|
||||
t.equal(targetComments.length, 3);
|
||||
const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null);
|
||||
t.equal(spriteWorkspaceComments.length, 2);
|
||||
|
||||
// Test the sprite block comments
|
||||
const blockComments = targetComments.filter(comment => !!comment.blockId);
|
||||
t.equal(blockComments.length, 1);
|
||||
|
||||
// There should not be any comments where blockId is a number
|
||||
const invalidComments = targetComments.filter(comment => typeof comment.blockId === 'number');
|
||||
t.equal(invalidComments.length, 0);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/block-to-workspace-comments-without-scripts.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
/* eslint-disable-next-line max-len */
|
||||
test('importing sb2 project where block comment is converted to workspace comment and block is deleted, and there are no scripts on the workspace', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.equal(threads.length, 0);
|
||||
|
||||
const target = vm.runtime.targets[1];
|
||||
|
||||
// Sprite 1 has 1 comments, a workspace comment which was
|
||||
// originally created via a block comment to workspace comment conversion in Scratch 2.0.
|
||||
// What differentiates this test from above is that there are no scripts in this project.
|
||||
const targetComments = Object.values(target.comments);
|
||||
t.equal(targetComments.length, 1);
|
||||
const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null);
|
||||
t.equal(spriteWorkspaceComments.length, 1);
|
||||
|
||||
// Test the sprite block comments
|
||||
const blockComments = targetComments.filter(comment => !!comment.blockId);
|
||||
t.equal(blockComments.length, 0);
|
||||
|
||||
// There should not be any comments where blockId is a number
|
||||
const invalidComments = targetComments.filter(comment => typeof comment.blockId === 'number');
|
||||
t.equal(invalidComments.length, 0);
|
||||
|
||||
const targetBlocks = Object.values(target.blocks._blocks);
|
||||
t.equal(targetBlocks.length, 0);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
85
scratch-vm/test/integration/broadcast_special_chars_sb2.js
Normal file
85
scratch-vm/test/integration/broadcast_special_chars_sb2.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const Variable = require('../../src/engine/variable');
|
||||
const StringUtil = require('../../src/util/string-util');
|
||||
const VariableUtil = require('../../src/util/variable-util');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/broadcast_special_chars.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('importing sb2 project with special chars in message names', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.equal(threads.length, 0);
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const cat = vm.runtime.targets[1];
|
||||
|
||||
const allBroadcastFields = VariableUtil.getAllVarRefsForTargets(vm.runtime.targets, true);
|
||||
|
||||
const abMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'a&b')[0];
|
||||
const abMessage = stage.variables[abMessageId];
|
||||
// Check for unsafe characters, replaceUnsafeChars should just result in the original string
|
||||
// (e.g. there was nothing to replace)
|
||||
// Check that the message ID does not have any unsafe characters
|
||||
t.equal(StringUtil.replaceUnsafeChars(abMessageId), abMessageId);
|
||||
|
||||
// Check that the message still has the correct info
|
||||
t.equal(StringUtil.replaceUnsafeChars(abMessage.id), abMessage.id);
|
||||
t.equal(abMessage.id, abMessageId);
|
||||
t.equal(abMessage.type, Variable.BROADCAST_MESSAGE_TYPE);
|
||||
t.equal(abMessage.value, 'a&b');
|
||||
|
||||
|
||||
const ltPerfectMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === '< perfect')[0];
|
||||
const ltPerfectMessage = stage.variables[ltPerfectMessageId];
|
||||
// Check for unsafe characters, replaceUnsafeChars should just result in the original string
|
||||
// (e.g. there was nothing to replace)
|
||||
// Check that the message ID does not have any unsafe characters
|
||||
t.equal(StringUtil.replaceUnsafeChars(ltPerfectMessageId), ltPerfectMessageId);
|
||||
|
||||
// Check that the message still has the correct info
|
||||
t.equal(StringUtil.replaceUnsafeChars(ltPerfectMessage.id), ltPerfectMessage.id);
|
||||
t.equal(ltPerfectMessage.id, ltPerfectMessageId);
|
||||
t.equal(ltPerfectMessage.type, Variable.BROADCAST_MESSAGE_TYPE);
|
||||
t.equal(ltPerfectMessage.value, '< perfect');
|
||||
|
||||
// Find all the references for these messages, and verify they have the correct ID
|
||||
t.equal(allBroadcastFields[ltPerfectMessageId].length, 1);
|
||||
t.equal(allBroadcastFields[abMessageId].length, 1);
|
||||
const catBlocks = Object.keys(cat.blocks._blocks).map(blockId => cat.blocks._blocks[blockId]);
|
||||
const catMessageBlocks = catBlocks.filter(
|
||||
block => Object.prototype.hasOwnProperty.call(block.fields, 'BROADCAST_OPTION')
|
||||
);
|
||||
t.equal(catMessageBlocks.length, 2);
|
||||
t.equal(catMessageBlocks[0].fields.BROADCAST_OPTION.id, ltPerfectMessageId);
|
||||
t.equal(catMessageBlocks[1].fields.BROADCAST_OPTION.id, abMessageId);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
85
scratch-vm/test/integration/broadcast_special_chars_sb3.js
Normal file
85
scratch-vm/test/integration/broadcast_special_chars_sb3.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const Variable = require('../../src/engine/variable');
|
||||
const StringUtil = require('../../src/util/string-util');
|
||||
const VariableUtil = require('../../src/util/variable-util');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/broadcast_special_chars.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('importing sb3 project with special chars in message names', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.equal(threads.length, 0);
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const cat = vm.runtime.targets[1];
|
||||
|
||||
const allBroadcastFields = VariableUtil.getAllVarRefsForTargets(vm.runtime.targets, true);
|
||||
|
||||
const abMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'a&b')[0];
|
||||
const abMessage = stage.variables[abMessageId];
|
||||
// Check for unsafe characters, replaceUnsafeChars should just result in the original string
|
||||
// (e.g. there was nothing to replace)
|
||||
// Check that the message ID does not have any unsafe characters
|
||||
t.equal(StringUtil.replaceUnsafeChars(abMessageId), abMessageId);
|
||||
|
||||
// Check that the message still has the correct info
|
||||
t.equal(StringUtil.replaceUnsafeChars(abMessage.id), abMessage.id);
|
||||
t.equal(abMessage.id, abMessageId);
|
||||
t.equal(abMessage.type, Variable.BROADCAST_MESSAGE_TYPE);
|
||||
t.equal(abMessage.value, 'a&b');
|
||||
|
||||
|
||||
const ltPerfectMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === '< perfect')[0];
|
||||
const ltPerfectMessage = stage.variables[ltPerfectMessageId];
|
||||
// Check for unsafe characters, replaceUnsafeChars should just result in the original string
|
||||
// (e.g. there was nothing to replace)
|
||||
// Check that the message ID does not have any unsafe characters
|
||||
t.equal(StringUtil.replaceUnsafeChars(ltPerfectMessageId), ltPerfectMessageId);
|
||||
|
||||
// Check that the message still has the correct info
|
||||
t.equal(StringUtil.replaceUnsafeChars(ltPerfectMessage.id), ltPerfectMessage.id);
|
||||
t.equal(ltPerfectMessage.id, ltPerfectMessageId);
|
||||
t.equal(ltPerfectMessage.type, Variable.BROADCAST_MESSAGE_TYPE);
|
||||
t.equal(ltPerfectMessage.value, '< perfect');
|
||||
|
||||
// Find all the references for these messages, and verify they have the correct ID
|
||||
t.equal(allBroadcastFields[ltPerfectMessageId].length, 1);
|
||||
t.equal(allBroadcastFields[abMessageId].length, 1);
|
||||
const catBlocks = Object.keys(cat.blocks._blocks).map(blockId => cat.blocks._blocks[blockId]);
|
||||
const catMessageBlocks = catBlocks.filter(
|
||||
block => Object.prototype.hasOwnProperty.call(block.fields, 'BROADCAST_OPTION')
|
||||
);
|
||||
t.equal(catMessageBlocks.length, 2);
|
||||
t.equal(catMessageBlocks[0].fields.BROADCAST_OPTION.id, ltPerfectMessageId);
|
||||
t.equal(catMessageBlocks[1].fields.BROADCAST_OPTION.id, abMessageId);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
92
scratch-vm/test/integration/clone-cleanup.js
Normal file
92
scratch-vm/test/integration/clone-cleanup.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('clone-cleanup', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
/**
|
||||
* Track which step of the project is currently under test.
|
||||
* @type {number}
|
||||
*/
|
||||
let testStep = -1;
|
||||
|
||||
const verifyCounts = (expectedClones, extraThreads) => {
|
||||
// stage plus one sprite, plus clones
|
||||
t.strictEqual(vm.runtime.targets.length, 2 + expectedClones,
|
||||
`target count at step ${testStep}`);
|
||||
|
||||
// the stage should never have any clones
|
||||
t.strictEqual(vm.runtime.targets[0].sprite.clones.length, 1,
|
||||
`stage clone count at step ${testStep}`);
|
||||
|
||||
// check sprite clone count (+1 for original)
|
||||
t.strictEqual(vm.runtime.targets[1].sprite.clones.length, 1 + expectedClones,
|
||||
`sprite clone count at step ${testStep}`);
|
||||
|
||||
// thread count isn't directly tied to clone count since threads can end
|
||||
t.strictEqual(vm.runtime.threads.length, extraThreads + (2 * expectedClones),
|
||||
`thread count at step ${testStep}`);
|
||||
};
|
||||
|
||||
const testNextStep = () => {
|
||||
++testStep;
|
||||
switch (testStep) {
|
||||
case 0:
|
||||
// Project has started, main thread running, no clones yet
|
||||
verifyCounts(0, 1);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// 10 clones have been created, main thread still running
|
||||
verifyCounts(10, 1);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// The first batch of clones has deleted themselves; main thread still running
|
||||
verifyCounts(0, 1);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// The second batch of clones has been created and the main thread is about to end
|
||||
verifyCounts(10, 1);
|
||||
|
||||
// After the main thread ends, do one last test step
|
||||
setTimeout(() => testNextStep(), 1000);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
// The second batch of clones has deleted themselves; everything is finished
|
||||
verifyCounts(0, 0);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
|
||||
// Verify initial state: no clones, nothing running ("step -1")
|
||||
verifyCounts(0, 0);
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
// Let the project control the pace of the tests
|
||||
vm.runtime.on('SAY', () => testNextStep());
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
155
scratch-vm/test/integration/cloud_variables_sb2.js
Normal file
155
scratch-vm/test/integration/cloud_variables_sb2.js
Normal file
@@ -0,0 +1,155 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const cloudVarSimpleUri = path.resolve(__dirname, '../fixtures/cloud_variables_simple.sb2');
|
||||
const cloudVarLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_limit.sb2');
|
||||
const cloudVarExceededLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_exceeded_limit.sb2');
|
||||
const cloudVarLocalUri = path.resolve(__dirname, '../fixtures/cloud_variables_local.sb2');
|
||||
|
||||
const cloudVarSimple = readFileToBuffer(cloudVarSimpleUri);
|
||||
const cloudVarLimit = readFileToBuffer(cloudVarLimitUri);
|
||||
const cloudVarExceededLimit = readFileToBuffer(cloudVarExceededLimitUri);
|
||||
const cloudVarLocal = readFileToBuffer(cloudVarLocalUri);
|
||||
|
||||
test('importing an sb2 project with cloud variables', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarSimple).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
t.equal(stageVars.length, 1);
|
||||
|
||||
const variable = stageVars[0];
|
||||
t.equal(variable.name, '☁ firstCloud');
|
||||
t.equal(Number(variable.value), 100); // Though scratch 2 requires
|
||||
// cloud variables to be numbers, this is something that happens
|
||||
// when the message is being sent to the server rather than on the client
|
||||
t.equal(variable.isCloud, true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('importing an sb2 project with cloud variables at the limit for a project', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarLimit).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
|
||||
t.equal(stageVars.length, 10);
|
||||
// All of the 8 stage variables should be cloud variables
|
||||
t.equal(stageVars.filter(v => v.isCloud).length, 10);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('importing an sb2 project with cloud variables exceeding the limit for a project', t => {
|
||||
// This tests a hacked project where additional cloud variables exceeding
|
||||
// the project limit have been added.
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarExceededLimit).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
|
||||
t.equal(stageVars.length, 15);
|
||||
// Only 8 of the variables should have the isCloud flag set to true
|
||||
t.equal(stageVars.filter(v => v.isCloud).length, 10);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('importing one project after the other resets cloud variable limit', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarExceededLimit).then(() => {
|
||||
t.equal(vm.runtime.canAddCloudVariable(), false);
|
||||
|
||||
vm.loadProject(cloudVarSimple).then(() => {
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
t.equal(stageVars.length, 1);
|
||||
|
||||
const variable = stageVars[0];
|
||||
t.equal(variable.name, '☁ firstCloud');
|
||||
t.equal(Number(variable.value), 100); // Though scratch 2 requires
|
||||
// cloud variables to be numbers, this is something that happens
|
||||
// when the message is being sent to the server rather than on the client
|
||||
t.equal(variable.isCloud, true);
|
||||
|
||||
t.equal(vm.runtime.canAddCloudVariable(), true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('local cloud variables get imported as regular variables', t => {
|
||||
// This tests a hacked project where a sprite-local variable is
|
||||
// has the cloud variable flag set.
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarLocal).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), false);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
|
||||
t.equal(stageVars.length, 0);
|
||||
|
||||
const sprite = vm.runtime.targets[1];
|
||||
const spriteVars = Object.values(sprite.variables);
|
||||
|
||||
t.equal(spriteVars.length, 1);
|
||||
t.equal(spriteVars[0].isCloud, false);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
151
scratch-vm/test/integration/cloud_variables_sb3.js
Normal file
151
scratch-vm/test/integration/cloud_variables_sb3.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const cloudVarSimpleUri = path.resolve(__dirname, '../fixtures/cloud_variables_simple.sb3');
|
||||
const cloudVarLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_limit.sb3');
|
||||
const cloudVarExceededLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_exceeded_limit.sb3');
|
||||
const cloudVarLocalUri = path.resolve(__dirname, '../fixtures/cloud_variables_local.sb3');
|
||||
|
||||
const cloudVarSimple = readFileToBuffer(cloudVarSimpleUri);
|
||||
const cloudVarLimit = readFileToBuffer(cloudVarLimitUri);
|
||||
const cloudVarExceededLimit = readFileToBuffer(cloudVarExceededLimitUri);
|
||||
const cloudVarLocal = readFileToBuffer(cloudVarLocalUri);
|
||||
|
||||
test('importing an sb3 project with cloud variables', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarSimple).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
t.equal(stageVars.length, 1);
|
||||
|
||||
const variable = stageVars[0];
|
||||
t.equal(variable.name, '☁ firstCloud');
|
||||
t.equal(Number(variable.value), 100);
|
||||
t.equal(variable.isCloud, true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('importing an sb3 project with cloud variables at the limit for a project', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarLimit).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
|
||||
t.equal(stageVars.length, 10);
|
||||
// All of the 10 stage variables should be cloud variables
|
||||
t.equal(stageVars.filter(v => v.isCloud).length, 10);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('importing an sb3 project with cloud variables exceeding the limit for a project', t => {
|
||||
// This tests a hacked project where additional cloud variables exceeding
|
||||
// the project limit have been added.
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarExceededLimit).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
|
||||
t.equal(stageVars.length, 15);
|
||||
// Only 8 of the variables should have the isCloud flag set to true
|
||||
t.equal(stageVars.filter(v => v.isCloud).length, 10);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('importing one project after the other resets cloud variable limit', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarExceededLimit).then(() => {
|
||||
t.equal(vm.runtime.canAddCloudVariable(), false);
|
||||
|
||||
vm.loadProject(cloudVarSimple).then(() => {
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
t.equal(stageVars.length, 1);
|
||||
|
||||
const variable = stageVars[0];
|
||||
t.equal(variable.name, '☁ firstCloud');
|
||||
t.equal(Number(variable.value), 100);
|
||||
t.equal(variable.isCloud, true);
|
||||
|
||||
t.equal(vm.runtime.canAddCloudVariable(), true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('local cloud variables get imported as regular variables', t => {
|
||||
// This tests a hacked project where a sprite-local variable is
|
||||
// has the cloud variable flag set.
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarLocal).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), false);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
|
||||
t.equal(stageVars.length, 0);
|
||||
|
||||
const sprite = vm.runtime.targets[1];
|
||||
const spriteVars = Object.values(sprite.variables);
|
||||
|
||||
t.equal(spriteVars.length, 1);
|
||||
t.equal(spriteVars[0].isCloud, false);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
91
scratch-vm/test/integration/comments.js
Normal file
91
scratch-vm/test/integration/comments.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/comments.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('importing sb2 project with comments', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.equal(threads.length, 0);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const target = vm.runtime.targets[1];
|
||||
|
||||
const stageComments = Object.values(stage.comments);
|
||||
|
||||
// Stage has 1 comment, and it is minimized.
|
||||
t.equal(stageComments.length, 1);
|
||||
t.equal(stageComments[0].minimized, true);
|
||||
t.equal(stageComments[0].text, 'A minimized stage comment.');
|
||||
// The stage comment is a workspace comment
|
||||
t.equal(stageComments[0].blockId, null);
|
||||
|
||||
// Sprite 1 has 6 Comments, 1 workspace comment, and 5 block comments
|
||||
const targetComments = Object.values(target.comments);
|
||||
t.equal(targetComments.length, 6);
|
||||
const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null);
|
||||
t.equal(spriteWorkspaceComments.length, 1);
|
||||
t.equal(spriteWorkspaceComments[0].minimized, false);
|
||||
t.equal(spriteWorkspaceComments[0].text, 'This is a workspace comment.');
|
||||
|
||||
// Test the sprite block comments
|
||||
const blockComments = targetComments.filter(comment => !!comment.blockId);
|
||||
t.equal(blockComments.length, 5);
|
||||
|
||||
t.equal(blockComments[0].minimized, true);
|
||||
t.equal(blockComments[0].text, '1. Green Flag Comment.');
|
||||
const greenFlagBlock = target.blocks.getBlock(blockComments[0].blockId);
|
||||
t.equal(greenFlagBlock.comment, blockComments[0].id);
|
||||
t.equal(greenFlagBlock.opcode, 'event_whenflagclicked');
|
||||
|
||||
t.equal(blockComments[1].minimized, true);
|
||||
t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.');
|
||||
const turnRightBlock = target.blocks.getBlock(blockComments[1].blockId);
|
||||
t.equal(turnRightBlock.comment, blockComments[1].id);
|
||||
t.equal(turnRightBlock.opcode, 'motion_turnright');
|
||||
|
||||
t.equal(blockComments[2].minimized, false);
|
||||
t.equal(blockComments[2].text, '3. Comment for a loop.');
|
||||
const repeatBlock = target.blocks.getBlock(blockComments[2].blockId);
|
||||
t.equal(repeatBlock.comment, blockComments[2].id);
|
||||
t.equal(repeatBlock.opcode, 'control_repeat');
|
||||
|
||||
t.equal(blockComments[3].minimized, false);
|
||||
t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.');
|
||||
const changeColorBlock = target.blocks.getBlock(blockComments[3].blockId);
|
||||
t.equal(changeColorBlock.comment, blockComments[3].id);
|
||||
t.equal(changeColorBlock.opcode, 'looks_changeeffectby');
|
||||
|
||||
t.equal(blockComments[4].minimized, false);
|
||||
t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.');
|
||||
const stopAllBlock = target.blocks.getBlock(blockComments[4].blockId);
|
||||
t.equal(stopAllBlock.comment, blockComments[4].id);
|
||||
t.equal(stopAllBlock.opcode, 'control_stop');
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
91
scratch-vm/test/integration/comments_sb3.js
Normal file
91
scratch-vm/test/integration/comments_sb3.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/comments.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('load an sb3 project with comments', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.equal(threads.length, 0);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const target = vm.runtime.targets[1];
|
||||
|
||||
const stageComments = Object.values(stage.comments);
|
||||
|
||||
// Stage has 1 comment, and it is minimized.
|
||||
t.equal(stageComments.length, 1);
|
||||
t.equal(stageComments[0].minimized, true);
|
||||
t.equal(stageComments[0].text, 'A minimized stage comment.');
|
||||
// The stage comment is a workspace comment
|
||||
t.equal(stageComments[0].blockId, null);
|
||||
|
||||
// Sprite 1 has 6 Comments, 1 workspace comment, and 5 block comments
|
||||
const targetComments = Object.values(target.comments);
|
||||
t.equal(targetComments.length, 6);
|
||||
const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null);
|
||||
t.equal(spriteWorkspaceComments.length, 1);
|
||||
t.equal(spriteWorkspaceComments[0].minimized, false);
|
||||
t.equal(spriteWorkspaceComments[0].text, 'This is a workspace comment.');
|
||||
|
||||
// Test the sprite block comments
|
||||
const blockComments = targetComments.filter(comment => !!comment.blockId);
|
||||
t.equal(blockComments.length, 5);
|
||||
|
||||
t.equal(blockComments[0].minimized, true);
|
||||
t.equal(blockComments[0].text, '1. Green Flag Comment.');
|
||||
const greenFlagBlock = target.blocks.getBlock(blockComments[0].blockId);
|
||||
t.equal(greenFlagBlock.comment, blockComments[0].id);
|
||||
t.equal(greenFlagBlock.opcode, 'event_whenflagclicked');
|
||||
|
||||
t.equal(blockComments[1].minimized, true);
|
||||
t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.');
|
||||
const turnRightBlock = target.blocks.getBlock(blockComments[1].blockId);
|
||||
t.equal(turnRightBlock.comment, blockComments[1].id);
|
||||
t.equal(turnRightBlock.opcode, 'motion_turnright');
|
||||
|
||||
t.equal(blockComments[2].minimized, false);
|
||||
t.equal(blockComments[2].text, '3. Comment for a loop.');
|
||||
const repeatBlock = target.blocks.getBlock(blockComments[2].blockId);
|
||||
t.equal(repeatBlock.comment, blockComments[2].id);
|
||||
t.equal(repeatBlock.opcode, 'control_repeat');
|
||||
|
||||
t.equal(blockComments[3].minimized, false);
|
||||
t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.');
|
||||
const changeColorBlock = target.blocks.getBlock(blockComments[3].blockId);
|
||||
t.equal(changeColorBlock.comment, blockComments[3].id);
|
||||
t.equal(changeColorBlock.opcode, 'looks_changeeffectby');
|
||||
|
||||
t.equal(blockComments[4].minimized, false);
|
||||
t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.');
|
||||
const stopAllBlock = target.blocks.getBlock(blockComments[4].blockId);
|
||||
t.equal(stopAllBlock.comment, blockComments[4].id);
|
||||
t.equal(stopAllBlock.opcode, 'control_stop');
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
99
scratch-vm/test/integration/complex.js
Normal file
99
scratch-vm/test/integration/complex.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/complex.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/sprite.json');
|
||||
const sprite = fs.readFileSync(spriteUri, 'utf8');
|
||||
|
||||
test('complex', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Manipulate each target
|
||||
vm.on('targetsUpdate', data => {
|
||||
const targets = data.targetList;
|
||||
for (const i in targets) {
|
||||
if (targets[i].isStage === true) continue;
|
||||
if (targets[i].name.match(/test/)) continue;
|
||||
|
||||
vm.setEditingTarget(targets[i].id);
|
||||
vm.renameSprite(targets[i].id, 'test');
|
||||
vm.postSpriteInfo({
|
||||
x: 0,
|
||||
y: 10,
|
||||
direction: 90,
|
||||
draggable: true,
|
||||
rotationStyle: 'all around',
|
||||
visible: true
|
||||
});
|
||||
vm.addCostume(
|
||||
'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
||||
{
|
||||
name: 'costume1',
|
||||
baseLayerID: 0,
|
||||
baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
||||
bitmapResolution: 1,
|
||||
rotationCenterX: 47,
|
||||
rotationCenterY: 55
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// Post IO data
|
||||
vm.postIOData('mouse', {
|
||||
isDown: true,
|
||||
x: 0,
|
||||
y: 10,
|
||||
canvasWidth: 100,
|
||||
canvasHeight: 100
|
||||
});
|
||||
|
||||
// Add sprite
|
||||
vm.addSprite(sprite);
|
||||
|
||||
// Add backdrop
|
||||
vm.addBackdrop(
|
||||
'6b3d87ba2a7f89be703163b6c1d4c964.png',
|
||||
{
|
||||
name: 'baseball-field',
|
||||
baseLayerID: 26,
|
||||
baseLayerMD5: '6b3d87ba2a7f89be703163b6c1d4c964.png',
|
||||
bitmapResolution: 2,
|
||||
rotationCenterX: 480,
|
||||
rotationCenterY: 360
|
||||
}
|
||||
);
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
38
scratch-vm/test/integration/control.js
Normal file
38
scratch-vm/test/integration/control.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/control.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('control', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length > 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
});
|
||||
});
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
37
scratch-vm/test/integration/data.js
Normal file
37
scratch-vm/test/integration/data.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/data.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('data', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', () => {
|
||||
// @todo Additional tests
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
65
scratch-vm/test/integration/delete-and-restore-sprite.js
Normal file
65
scratch-vm/test/integration/delete-and-restore-sprite.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
// const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
test('spec', t => {
|
||||
t.type(vm.deleteSprite, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('default cat', t => {
|
||||
// Get default cat from .sprite2
|
||||
// const uri = path.resolve(__dirname, '../fixtures/example_sprite.sprite2');
|
||||
// const sprite = readFileToBuffer(uri);
|
||||
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
t.doesNotThrow(() => {
|
||||
vm.loadProject(project).then(() => {
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2); // stage and default sprite
|
||||
|
||||
const defaultSprite = vm.runtime.targets[1];
|
||||
|
||||
// Delete the sprite
|
||||
const addSpriteBack = vm.deleteSprite(vm.runtime.targets[1].id);
|
||||
|
||||
t.equal(vm.runtime.targets.length, 1);
|
||||
|
||||
t.type(addSpriteBack, 'function');
|
||||
|
||||
addSpriteBack().then(() => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
t.equal(vm.runtime.targets[1].getName(), defaultSprite.getName());
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
38
scratch-vm/test/integration/event.js
Normal file
38
scratch-vm/test/integration/event.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/event.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('event', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length > 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
150
scratch-vm/test/integration/execute.js
Normal file
150
scratch-vm/test/integration/execute.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const test = require('tap').test;
|
||||
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
/**
|
||||
* @fileoverview Transform each sb2 in fixtures/execute into a test.
|
||||
*
|
||||
* Test execution of a group of scratch blocks by SAYing if a test did "pass",
|
||||
* or did "fail". Four keywords can be set at the beginning of a SAY messaage
|
||||
* to indicate a test primitive.
|
||||
*
|
||||
* - "pass MESSAGE" will t.pass(MESSAGE).
|
||||
* - "fail MESSAGE" will t.fail(MESSAGE).
|
||||
* - "plan NUMBER_OF_TESTS" will t.plan(Number(NUMBER_OF_TESTS)).
|
||||
* - "end" will t.end().
|
||||
*
|
||||
* A good strategy to follow is to SAY "plan NUMBER_OF_TESTS" first. Then
|
||||
* "pass" and "fail" depending on expected scratch results in conditions, event
|
||||
* scripts, or what is best for testing the target block or group of blocks.
|
||||
* When its done you must SAY "end" so the test and tap know that the end has
|
||||
* been reached.
|
||||
*/
|
||||
|
||||
const whenThreadsComplete = (t, vm, uri, timeLimit = 5000) =>
|
||||
// When the number of threads reaches 0 the test is expected to be complete.
|
||||
new Promise((resolve, reject) => {
|
||||
const intervalId = setInterval(() => {
|
||||
let active = 0;
|
||||
const threads = vm.runtime.threads;
|
||||
for (let i = 0; i < threads.length; i++) {
|
||||
if (!threads[i].updateMonitor) {
|
||||
active += 1;
|
||||
}
|
||||
}
|
||||
if (active === 0) {
|
||||
resolve();
|
||||
}
|
||||
}, 50);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
t.fail(`Timeout waiting for threads to complete: ${uri}`);
|
||||
reject(new Error('time limit reached'));
|
||||
}, timeLimit);
|
||||
|
||||
// Clear the interval to allow the process to exit
|
||||
// naturally.
|
||||
t.tearDown(() => {
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
});
|
||||
|
||||
const executeDir = path.resolve(__dirname, '../fixtures/execute');
|
||||
|
||||
// Find files which end in ".sb", ".sb2", or ".sb3"
|
||||
const fileFilter = /\.sb[23]?$/i;
|
||||
|
||||
fs.readdirSync(executeDir)
|
||||
.filter(uri => fileFilter.test(uri))
|
||||
.forEach(uri => {
|
||||
const run = (t, enableCompiler) => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
// Map string messages to tap reporting methods. This will be used
|
||||
// with events from scratch's runtime emitted on block instructions.
|
||||
let didPlan;
|
||||
let didEnd;
|
||||
const reporters = {
|
||||
comment (message) {
|
||||
t.comment(message);
|
||||
},
|
||||
pass (reason) {
|
||||
t.pass(reason);
|
||||
},
|
||||
fail (reason) {
|
||||
t.fail(reason);
|
||||
},
|
||||
plan (count) {
|
||||
didPlan = true;
|
||||
t.plan(Number(count));
|
||||
},
|
||||
end () {
|
||||
didEnd = true;
|
||||
vm.quit();
|
||||
t.end();
|
||||
}
|
||||
};
|
||||
const reportVmResult = text => {
|
||||
const command = text.split(/\s+/, 1)[0].toLowerCase();
|
||||
if (reporters[command]) {
|
||||
return reporters[command](text.substring(command.length).trim());
|
||||
}
|
||||
|
||||
// Default to a comment with the full text if we didn't match
|
||||
// any command prefix
|
||||
return reporters.comment(text);
|
||||
};
|
||||
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start the VM and initialize some vm properties.
|
||||
// complete.
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.setCompilerOptions({enabled: enableCompiler});
|
||||
|
||||
// TW: Script compilation errors should fail.
|
||||
if (enableCompiler) {
|
||||
vm.on('COMPILE_ERROR', (target, error) => {
|
||||
throw new Error(`Could not compile script in ${target.getName()}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Report the text of SAY events as testing instructions.
|
||||
vm.runtime.on('SAY', (target, type, text) => reportVmResult(text));
|
||||
|
||||
const project = readFileToBuffer(path.resolve(executeDir, uri));
|
||||
|
||||
// Load the project and once all threads are complete ensure that
|
||||
// the scratch project sent us a "end" message.
|
||||
return vm.loadProject(project)
|
||||
.then(() => vm.greenFlag())
|
||||
.then(() => whenThreadsComplete(t, vm, uri))
|
||||
.then(() => {
|
||||
// Setting a plan is not required but is a good idea.
|
||||
if (!didPlan) {
|
||||
t.comment('did not say "plan NUMBER_OF_TESTS"');
|
||||
}
|
||||
|
||||
// End must be called so that tap knows the test is done. If
|
||||
// the test has an SAY "end" block but that block did not
|
||||
// execute, this explicit failure will raise that issue so
|
||||
// it can be resolved.
|
||||
if (!didEnd) {
|
||||
t.fail('did not say "end"');
|
||||
vm.quit();
|
||||
t.end();
|
||||
}
|
||||
});
|
||||
};
|
||||
test(`${uri} (interpreted)`, t => run(t, false));
|
||||
test(`${uri} (compiled)`, t => run(t, true));
|
||||
});
|
||||
57
scratch-vm/test/integration/hat-execution-order.js
Normal file
57
scratch-vm/test/integration/hat-execution-order.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/hat-execution-order.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const compilerAndInterpreter = (name, callback) => {
|
||||
test(`${name} - interpreted`, t => {
|
||||
callback(t, {
|
||||
enabled: false
|
||||
});
|
||||
});
|
||||
test(`${name} - compiled`, t => {
|
||||
callback(t, {
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
compilerAndInterpreter('complex', (t, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
|
||||
const resultKey = Object.keys(vm.runtime.targets[0].variables)[0];
|
||||
const results = vm.runtime.targets[0].variables[resultKey].value;
|
||||
t.deepEqual(results, ['3', '2', '1', 'stage']);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
377
scratch-vm/test/integration/hat-threads-run-every-frame.js
Normal file
377
scratch-vm/test/integration/hat-threads-run-every-frame.js
Normal file
@@ -0,0 +1,377 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const Thread = require('../../src/engine/thread');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const execute = require('../../src/engine/execute.js');
|
||||
|
||||
const compilerAndInterpreter = (name, callback) => {
|
||||
test(`${name} - interpreted`, t => {
|
||||
callback(t, {
|
||||
enabled: false
|
||||
});
|
||||
});
|
||||
test(`${name} - compiled`, t => {
|
||||
callback(t, {
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/timer-greater-than-hat.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const checkIsHatThread = (t, vm, hatThread) => {
|
||||
t.equal(hatThread.stackClick, false);
|
||||
t.equal(hatThread.updateMonitor, false);
|
||||
const blockContainer = hatThread.target.blocks;
|
||||
const opcode = blockContainer.getOpcode(blockContainer.getBlock(hatThread.topBlock));
|
||||
t.assert(vm.runtime.getIsEdgeActivatedHat(opcode));
|
||||
};
|
||||
|
||||
const checkIsStackClickThread = (t, vm, stackClickThread) => {
|
||||
t.equal(stackClickThread.stackClick, true);
|
||||
t.equal(stackClickThread.updateMonitor, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* timer-greater-than-hat.sb2 contains a single stack
|
||||
* when timer > -1
|
||||
* change color effect by 25
|
||||
* The intention is to make sure that the hat block condition is evaluated
|
||||
* on each frame.
|
||||
*/
|
||||
compilerAndInterpreter('edge activated hat thread runs once every frame', (t, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
let threads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(threads.length, 1);
|
||||
checkIsHatThread(t, vm, threads[0]);
|
||||
t.assert(threads[0].status === Thread.STATUS_DONE);
|
||||
|
||||
// Check that the hat thread is added again when another step is taken
|
||||
vm.runtime._step();
|
||||
threads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(threads.length, 1);
|
||||
checkIsHatThread(t, vm, threads[0]);
|
||||
t.assert(threads[0].status === Thread.STATUS_DONE);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* When a hat is added it should run in the next frame. Any block related
|
||||
* caching should be reset.
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
compilerAndInterpreter('edge activated hat thread runs after being added to previously executed target', (t, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
let threads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(threads.length, 1);
|
||||
checkIsHatThread(t, vm, threads[0]);
|
||||
t.assert(threads[0].status === Thread.STATUS_DONE);
|
||||
|
||||
// Add a second hat that should create a second thread
|
||||
const hatBlock = threads[0].target.blocks.getBlock(threads[0].topBlock);
|
||||
threads[0].target.blocks.createBlock(Object.assign(
|
||||
{}, hatBlock, {id: 'hatblock2', next: null}
|
||||
));
|
||||
|
||||
// Check that the hat thread is added again when another step is taken
|
||||
vm.runtime._step();
|
||||
threads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(threads.length, 2);
|
||||
checkIsHatThread(t, vm, threads[0]);
|
||||
checkIsHatThread(t, vm, threads[1]);
|
||||
t.assert(threads[0].status === Thread.STATUS_DONE);
|
||||
t.assert(threads[1].status === Thread.STATUS_DONE);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* If the hat doesn't finish evaluating within one frame, it shouldn't be added again
|
||||
* on the next frame. (We skip execution by setting the step time to 0)
|
||||
*/
|
||||
compilerAndInterpreter('edge activated hat thread not added twice', (t, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
let doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 1);
|
||||
t.equal(doneThreads.length, 0);
|
||||
const prevThread = vm.runtime.threads[0];
|
||||
checkIsHatThread(t, vm, vm.runtime.threads[0]);
|
||||
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
|
||||
|
||||
// Check that no new threads are added when another step is taken
|
||||
vm.runtime._step();
|
||||
doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
// There should now be one done hat thread and one new hat thread to run
|
||||
t.equal(vm.runtime.threads.length, 1);
|
||||
t.equal(doneThreads.length, 0);
|
||||
checkIsHatThread(t, vm, vm.runtime.threads[0]);
|
||||
t.assert(vm.runtime.threads[0] === prevThread);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Duplicating a sprite should also track duplicated edge activated hat in
|
||||
* runtime's _edgeActivatedHatValues map.
|
||||
*/
|
||||
compilerAndInterpreter('edge activated hat should trigger for both sprites when sprite is duplicated', (t, co) => {
|
||||
|
||||
// Project that is similar to timer-greater-than-hat.sb2, but has code on the sprite so that
|
||||
// the sprite can be duplicated
|
||||
const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3');
|
||||
const projectWithSprite = readFileToBuffer(projectWithSpriteUri);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(projectWithSprite).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
t.equal(vm.runtime.threads.length, 1);
|
||||
checkIsHatThread(t, vm, vm.runtime.threads[0]);
|
||||
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
|
||||
let numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
|
||||
val + Object.keys(target._edgeActivatedHatValues).length, 0);
|
||||
t.equal(numTargetEdgeHats, 1);
|
||||
|
||||
vm.duplicateSprite(vm.runtime.targets[1].id).then(() => {
|
||||
vm.runtime._step();
|
||||
// Check that the runtime's _edgeActivatedHatValues object has two separate keys
|
||||
// after execute is run on each thread
|
||||
numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
|
||||
val + Object.keys(target._edgeActivatedHatValues).length, 0);
|
||||
t.equal(numTargetEdgeHats, 2);
|
||||
t.end();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Cloning a sprite should also track cloned edge activated hat separately
|
||||
* runtime's _edgeActivatedHatValues map.
|
||||
*/
|
||||
compilerAndInterpreter('edge activated hat should trigger for both sprites when sprite is cloned', (t, co) => {
|
||||
|
||||
// Project that is similar to loudness-hat-block.sb2, but has code on the sprite so that
|
||||
// the sprite can be duplicated
|
||||
const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3');
|
||||
const projectWithSprite = readFileToBuffer(projectWithSpriteUri);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(projectWithSprite).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
t.equal(vm.runtime.threads.length, 1);
|
||||
checkIsHatThread(t, vm, vm.runtime.threads[0]);
|
||||
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
|
||||
// Run execute on the thread to populate the runtime's
|
||||
// _edgeActivatedHatValues object
|
||||
execute(vm.runtime.sequencer, vm.runtime.threads[0]);
|
||||
let numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
|
||||
val + Object.keys(target._edgeActivatedHatValues).length, 0);
|
||||
t.equal(numTargetEdgeHats, 1);
|
||||
|
||||
const cloneTarget = vm.runtime.targets[1].makeClone();
|
||||
vm.runtime.addTarget(cloneTarget);
|
||||
|
||||
vm.runtime._step();
|
||||
// Check that the runtime's _edgeActivatedHatValues object has two separate keys
|
||||
// after execute is run on each thread
|
||||
vm.runtime.threads.forEach(thread => execute(vm.runtime.sequencer, thread));
|
||||
numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
|
||||
val + Object.keys(target._edgeActivatedHatValues).length, 0);
|
||||
t.equal(numTargetEdgeHats, 2);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* When adding a stack click thread first, make sure that the edge activated hat thread and
|
||||
* the stack click thread are both pushed and run (despite having the same top block)
|
||||
*/
|
||||
compilerAndInterpreter('edge activated hat thread does not interrupt stack click thread', (t, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
let doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(doneThreads.length, 1);
|
||||
checkIsHatThread(t, vm, doneThreads[0]);
|
||||
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
|
||||
|
||||
// Add stack click thread on this hat
|
||||
vm.runtime.toggleScript(doneThreads[0].topBlock, {stackClick: true});
|
||||
|
||||
// Check that the hat thread is added again when another step is taken
|
||||
vm.runtime._step();
|
||||
doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(doneThreads.length, 2);
|
||||
let hatThread;
|
||||
let stackClickThread;
|
||||
if (doneThreads[0].stackClick) {
|
||||
stackClickThread = doneThreads[0];
|
||||
hatThread = doneThreads[1];
|
||||
} else {
|
||||
stackClickThread = doneThreads[1];
|
||||
hatThread = doneThreads[0];
|
||||
}
|
||||
checkIsHatThread(t, vm, hatThread);
|
||||
checkIsStackClickThread(t, vm, stackClickThread);
|
||||
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
|
||||
t.assert(doneThreads[1].status === Thread.STATUS_DONE);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* When adding the hat thread first, make sure that the edge activated hat thread and
|
||||
* the stack click thread are both pushed and run (despite having the same top block)
|
||||
*/
|
||||
compilerAndInterpreter('edge activated hat thread does not interrupt stack click thread', (t, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
let doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 1);
|
||||
t.equal(doneThreads.length, 0);
|
||||
checkIsHatThread(t, vm, vm.runtime.threads[0]);
|
||||
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
|
||||
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
|
||||
// Add stack click thread on this hat
|
||||
vm.runtime.toggleScript(vm.runtime.threads[0].topBlock, {stackClick: true});
|
||||
|
||||
// Check that the hat thread is added again when another step is taken
|
||||
vm.runtime._step();
|
||||
doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(doneThreads.length, 2);
|
||||
let hatThread;
|
||||
let stackClickThread;
|
||||
if (doneThreads[0].stackClick) {
|
||||
stackClickThread = doneThreads[0];
|
||||
hatThread = doneThreads[1];
|
||||
} else {
|
||||
stackClickThread = doneThreads[1];
|
||||
hatThread = doneThreads[0];
|
||||
}
|
||||
checkIsHatThread(t, vm, hatThread);
|
||||
checkIsStackClickThread(t, vm, stackClickThread);
|
||||
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
|
||||
t.assert(doneThreads[1].status === Thread.STATUS_DONE);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
45
scratch-vm/test/integration/import-sb.js
Normal file
45
scratch-vm/test/integration/import-sb.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/single_sound.sb');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('default', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
const stageSounds = vm.runtime.targets[0].sprite.sounds;
|
||||
const firstSound = stageSounds[0];
|
||||
|
||||
// Check that the sound has the correct md5
|
||||
// This md5 was obtained from the asset server
|
||||
t.equal(firstSound.md5, 'edb9713dedbe9a2e05c09e0540182ef1.wav');
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
scratch-vm/test/integration/import-sb2-from-object.js
Normal file
38
scratch-vm/test/integration/import-sb2-from-object.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const project = extractProjectJson(uri);
|
||||
|
||||
test('default', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
52
scratch-vm/test/integration/import_nested_sb2.js
Normal file
52
scratch-vm/test/integration/import_nested_sb2.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson;
|
||||
|
||||
const renderedTarget = require('../../src/sprites/rendered-target');
|
||||
const runtime = require('../../src/engine/runtime');
|
||||
const sb2 = require('../../src/serialization/sb2');
|
||||
|
||||
test('spec', t => {
|
||||
t.type(sb2.deserialize, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('nested default/*', t => {
|
||||
// Get SB2 JSON (string)
|
||||
const uri = path.resolve(__dirname, '../fixtures/default_nested.sb2');
|
||||
const json = extractProjectJson(uri, 'default');
|
||||
|
||||
// Create runtime instance & load SB2 into it
|
||||
const rt = new runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
// Test
|
||||
t.type(json, 'object');
|
||||
t.type(rt, 'object');
|
||||
t.type(targets, 'object');
|
||||
|
||||
t.ok(targets[0] instanceof renderedTarget);
|
||||
t.type(targets[0].id, 'string');
|
||||
t.type(targets[0].blocks, 'object');
|
||||
t.type(targets[0].variables, 'object');
|
||||
t.type(targets[0].comments, 'object');
|
||||
|
||||
t.equal(targets[0].isOriginal, true);
|
||||
t.equal(targets[0].currentCostume, 0);
|
||||
t.equal(targets[0].isOriginal, true);
|
||||
t.equal(targets[0].isStage, true);
|
||||
|
||||
t.ok(targets[1] instanceof renderedTarget);
|
||||
t.type(targets[1].id, 'string');
|
||||
t.type(targets[1].blocks, 'object');
|
||||
t.type(targets[1].variables, 'object');
|
||||
t.type(targets[1].comments, 'object');
|
||||
|
||||
t.equal(targets[1].isOriginal, true);
|
||||
t.equal(targets[1].currentCostume, 0);
|
||||
t.equal(targets[1].isOriginal, true);
|
||||
t.equal(targets[1].isStage, false);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
52
scratch-vm/test/integration/import_sb2.js
Normal file
52
scratch-vm/test/integration/import_sb2.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson;
|
||||
|
||||
const renderedTarget = require('../../src/sprites/rendered-target');
|
||||
const runtime = require('../../src/engine/runtime');
|
||||
const sb2 = require('../../src/serialization/sb2');
|
||||
|
||||
test('spec', t => {
|
||||
t.type(sb2.deserialize, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('default', t => {
|
||||
// Get SB2 JSON (string)
|
||||
const uri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const json = extractProjectJson(uri);
|
||||
|
||||
// Create runtime instance & load SB2 into it
|
||||
const rt = new runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
// Test
|
||||
t.type(json, 'object');
|
||||
t.type(rt, 'object');
|
||||
t.type(targets, 'object');
|
||||
|
||||
t.ok(targets[0] instanceof renderedTarget);
|
||||
t.type(targets[0].id, 'string');
|
||||
t.type(targets[0].blocks, 'object');
|
||||
t.type(targets[0].variables, 'object');
|
||||
t.type(targets[0].comments, 'object');
|
||||
|
||||
t.equal(targets[0].isOriginal, true);
|
||||
t.equal(targets[0].currentCostume, 0);
|
||||
t.equal(targets[0].isOriginal, true);
|
||||
t.equal(targets[0].isStage, true);
|
||||
|
||||
t.ok(targets[1] instanceof renderedTarget);
|
||||
t.type(targets[1].id, 'string');
|
||||
t.type(targets[1].blocks, 'object');
|
||||
t.type(targets[1].variables, 'object');
|
||||
t.type(targets[1].comments, 'object');
|
||||
|
||||
t.equal(targets[1].isOriginal, true);
|
||||
t.equal(targets[1].currentCostume, 0);
|
||||
t.equal(targets[1].isOriginal, true);
|
||||
t.equal(targets[1].isStage, false);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
127
scratch-vm/test/integration/internal-extension.js
Normal file
127
scratch-vm/test/integration/internal-extension.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const test = require('tap').test;
|
||||
const Worker = require('tiny-worker');
|
||||
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
class TestInternalExtension {
|
||||
constructor () {
|
||||
this.status = {};
|
||||
this.status.constructorCalled = true;
|
||||
}
|
||||
|
||||
getInfo () {
|
||||
this.status.getInfoCalled = true;
|
||||
return {
|
||||
id: 'testInternalExtension',
|
||||
name: 'Test Internal Extension',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'go'
|
||||
}
|
||||
],
|
||||
menus: {
|
||||
simpleMenu: this._buildAMenu(),
|
||||
dynamicMenu: '_buildDynamicMenu'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
go (args, util, blockInfo) {
|
||||
this.status.goCalled = true;
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
_buildAMenu () {
|
||||
this.status.buildMenuCalled = true;
|
||||
return ['abcd', 'efgh', 'ijkl'];
|
||||
}
|
||||
|
||||
_buildDynamicMenu () {
|
||||
this.status.buildDynamicMenuCalled = true;
|
||||
return [1, 2, 3, 4, 6];
|
||||
}
|
||||
}
|
||||
|
||||
test('internal extension', t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
const extension = new TestInternalExtension();
|
||||
t.ok(extension.status.constructorCalled);
|
||||
|
||||
t.notOk(extension.status.getInfoCalled);
|
||||
vm.extensionManager._registerInternalExtension(extension);
|
||||
t.ok(extension.status.getInfoCalled);
|
||||
|
||||
const func = vm.runtime.getOpcodeFunction('testInternalExtension_go');
|
||||
t.type(func, 'function');
|
||||
|
||||
t.notOk(extension.status.goCalled);
|
||||
const goBlockInfo = func();
|
||||
t.ok(extension.status.goCalled);
|
||||
|
||||
// The 'go' block returns its own blockInfo. Make sure it matches the expected info.
|
||||
// Note that the extension parser fills in missing fields so there are more fields here than in `getInfo`.
|
||||
const expectedBlockInfo = {
|
||||
arguments: {},
|
||||
blockAllThreads: false,
|
||||
blockType: BlockType.COMMAND,
|
||||
func: goBlockInfo.func, // Cheat since we don't have a good way to ensure we generate the same function
|
||||
opcode: 'go',
|
||||
terminal: false,
|
||||
text: 'go'
|
||||
};
|
||||
t.deepEqual(goBlockInfo, expectedBlockInfo);
|
||||
|
||||
// There should be 2 menus - one is an array, one is the function to call.
|
||||
t.equal(vm.runtime._blockInfo[0].menus.length, 2);
|
||||
// First menu has 3 items.
|
||||
t.equal(
|
||||
vm.runtime._blockInfo[0].menus[0].json.args0[0].options.length, 3);
|
||||
// Second menu is a dynamic menu and therefore should be a function.
|
||||
t.type(
|
||||
vm.runtime._blockInfo[0].menus[1].json.args0[0].options, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load sync', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager.loadExtensionIdSync('coreExample');
|
||||
t.ok(vm.extensionManager.isExtensionLoaded('coreExample'));
|
||||
|
||||
t.equal(vm.runtime._blockInfo.length, 1);
|
||||
|
||||
// blocks should be an array of two items: a button pseudo-block and a reporter block.
|
||||
t.equal(vm.runtime._blockInfo[0].blocks.length, 3);
|
||||
t.type(vm.runtime._blockInfo[0].blocks[0].info, 'object');
|
||||
t.type(vm.runtime._blockInfo[0].blocks[0].info.func, 'MAKE_A_VARIABLE');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[0].info.blockType, 'button');
|
||||
t.type(vm.runtime._blockInfo[0].blocks[1].info, 'object');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.opcode, 'exampleOpcode');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.blockType, 'reporter');
|
||||
t.type(vm.runtime._blockInfo[0].blocks[2].info, 'object');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[2].info.opcode, 'exampleWithInlineImage');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[2].info.blockType, 'command');
|
||||
|
||||
// Test the opcode function
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.func(), 'no stage yet');
|
||||
|
||||
const sprite = new Sprite(null, vm.runtime);
|
||||
sprite.name = 'Stage';
|
||||
const stage = new RenderedTarget(sprite, vm.runtime);
|
||||
stage.isStage = true;
|
||||
vm.runtime.targets = [stage];
|
||||
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.func(), 'Stage');
|
||||
|
||||
t.end();
|
||||
});
|
||||
52
scratch-vm/test/integration/list-monitor-rename.js
Normal file
52
scratch-vm/test/integration/list-monitor-rename.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/list-monitor-rename.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('importing sb3 project with incorrect list monitor name', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', () => {
|
||||
const stage = vm.runtime.targets[0];
|
||||
const cat = vm.runtime.targets[1];
|
||||
|
||||
for (const {target, renamedListName} of [
|
||||
{target: stage, renamedListName: 'renamed global'},
|
||||
{target: cat, renamedListName: 'renamed local'}
|
||||
]) {
|
||||
const listId = Object.keys(target.variables).find(k => target.variables[k].name === renamedListName);
|
||||
|
||||
const monitorRecord = vm.runtime._monitorState.get(listId);
|
||||
const monitorBlock = vm.runtime.monitorBlocks.getBlock(listId);
|
||||
t.equal(monitorRecord.opcode, 'data_listcontents');
|
||||
|
||||
// The list name should be properly renamed
|
||||
t.equal(monitorRecord.params.LIST, renamedListName);
|
||||
t.equal(monitorBlock.fields.LIST.value, renamedListName);
|
||||
}
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
83
scratch-vm/test/integration/load-extensions.js
Normal file
83
scratch-vm/test/integration/load-extensions.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const {test} = tap;
|
||||
const fs = require('fs');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
/**
|
||||
* Call _stopLoop() on the Video Sensing extension.
|
||||
* @param {VirtualMachine} vm - a VM instance which has loaded the 'videoSensing' extension.
|
||||
*/
|
||||
const stopVideoLoop = vm => {
|
||||
// TODO: provide a general way to tell extensions to shut down
|
||||
// Ideally we'd just dispose of the extension's Worker...
|
||||
const serviceName = vm.extensionManager._loadedExtensions.get('videoSensing');
|
||||
dispatch.call(serviceName, '_stopLoop');
|
||||
};
|
||||
|
||||
test('Load external extensions', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
const testFiles = fs.readdirSync('./test/fixtures/load-extensions/confirm-load/');
|
||||
|
||||
// Test each example extension file
|
||||
for (const file of testFiles) {
|
||||
const ext = file.split('-')[0];
|
||||
const uri = path.resolve(__dirname, `../fixtures/load-extensions/confirm-load/${file}`);
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
await t.test('Confirm expected extension is installed in example sb2 and sb3 projects', extTest => {
|
||||
vm.loadProject(project)
|
||||
.then(() => {
|
||||
extTest.ok(vm.extensionManager.isExtensionLoaded(ext));
|
||||
extTest.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stopVideoLoop(vm);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Load video sensing extension and video properties', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
// An array of test projects and their expected video state values
|
||||
const testProjects = [
|
||||
{
|
||||
file: 'videoState-off.sb2',
|
||||
videoState: 'off',
|
||||
videoTransparency: 50,
|
||||
mirror: undefined
|
||||
},
|
||||
{
|
||||
file: 'videoState-on-transparency-0.sb2',
|
||||
videoState: 'on',
|
||||
videoTransparency: 0,
|
||||
mirror: true
|
||||
}];
|
||||
|
||||
for (const project of testProjects) {
|
||||
const uri = path.resolve(__dirname, `../fixtures/load-extensions/video-state/${project.file}`);
|
||||
const projectData = readFileToBuffer(uri);
|
||||
|
||||
await vm.loadProject(projectData);
|
||||
|
||||
const stage = vm.runtime.getTargetForStage();
|
||||
|
||||
t.ok(vm.extensionManager.isExtensionLoaded('videoSensing'));
|
||||
|
||||
// Check that the stage target has the video state values we expect
|
||||
// based on the test project files, then check that the video io device
|
||||
// has the expected state as well
|
||||
t.equal(stage.videoState, project.videoState);
|
||||
t.equal(vm.runtime.ioDevices.video.mirror, project.mirror);
|
||||
t.equal(stage.videoTransparency, project.videoTransparency);
|
||||
t.equal(vm.runtime.ioDevices.video._ghost, project.videoTransparency);
|
||||
}
|
||||
|
||||
stopVideoLoop(vm);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/sb2-from-sb1-missing-backdrop-image.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
test('sb2 project (originally from Scratch 1.4) with missing backdrop image should load', t => {
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
t.doesNotThrow(() => {
|
||||
vm.loadProject(project).then(() => {
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2); // stage and default sprite
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
scratch-vm/test/integration/looks.js
Normal file
38
scratch-vm/test/integration/looks.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/looks.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('looks', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const Thread = require('../../src/engine/thread');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/timer-monitor.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const checkMonitorThreadPresent = (t, threads) => {
|
||||
t.equal(threads.length, 1);
|
||||
const monitorThread = threads[0];
|
||||
t.equal(monitorThread.stackClick, false);
|
||||
t.equal(monitorThread.updateMonitor, true);
|
||||
t.equal(monitorThread.topBlock.toString(), 'timer');
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a monitor and then checks if it gets run every frame.
|
||||
*/
|
||||
test('monitor thread runs every frame', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
let doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(doneThreads.length, 1);
|
||||
checkMonitorThreadPresent(t, doneThreads);
|
||||
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
|
||||
|
||||
// Check that both are added again when another step is taken
|
||||
vm.runtime._step();
|
||||
doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
t.equal(doneThreads.length, 1);
|
||||
checkMonitorThreadPresent(t, doneThreads);
|
||||
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* If the monitor doesn't finish evaluating within one frame, it shouldn't be added again
|
||||
* on the next frame. (We skip execution by setting the step time to 0)
|
||||
*/
|
||||
test('monitor thread not added twice', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
|
||||
vm.runtime._step();
|
||||
let doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 1);
|
||||
t.equal(doneThreads.length, 0);
|
||||
checkMonitorThreadPresent(t, vm.runtime.threads);
|
||||
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
|
||||
const prevThread = vm.runtime.threads[0];
|
||||
|
||||
// Check that both are added again when another step is taken
|
||||
vm.runtime._step();
|
||||
doneThreads = vm.runtime._lastStepDoneThreads;
|
||||
t.equal(vm.runtime.threads.length, 1);
|
||||
t.equal(doneThreads.length, 0);
|
||||
checkMonitorThreadPresent(t, vm.runtime.threads);
|
||||
t.equal(vm.runtime.threads[0], prevThread);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
143
scratch-vm/test/integration/monitors_sb2.js
Normal file
143
scratch-vm/test/integration/monitors_sb2.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('importing sb2 project with monitors', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
// All monitors should create threads that finish during the step and
|
||||
// are revoved from runtime.threads.
|
||||
t.equal(threads.length, 0);
|
||||
|
||||
// we care that the last step updated the right number of monitors
|
||||
// we don't care whether the last step ran other threads or not
|
||||
const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor);
|
||||
t.equal(lastStepUpdatedMonitorThreads.length, 8);
|
||||
|
||||
// There should be one additional hidden monitor that is in the monitorState but
|
||||
// does not start a thread.
|
||||
t.equal(vm.runtime._monitorState.size, 9);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const target = vm.runtime.targets[1];
|
||||
|
||||
// Global variable named "global" is a slider
|
||||
let variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'global')[0];
|
||||
let monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_variable');
|
||||
t.equal(monitorRecord.mode, 'slider');
|
||||
t.equal(monitorRecord.sliderMin, -200); // Make sure these are imported for sliders.
|
||||
t.equal(monitorRecord.sliderMax, 30);
|
||||
t.equal(monitorRecord.isDiscrete, false);
|
||||
t.equal(monitorRecord.x, 5); // These are imported for all monitors, just check once.
|
||||
t.equal(monitorRecord.y, 59);
|
||||
t.equal(monitorRecord.visible, true);
|
||||
|
||||
// Global variable named "global list" is a list
|
||||
variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'global list')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_listcontents');
|
||||
t.equal(monitorRecord.mode, 'list');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
|
||||
// Local variable named "local" is hidden
|
||||
variableId = Object.keys(target.variables).filter(k => target.variables[k].name === 'local')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_variable');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, false);
|
||||
|
||||
// Local list named "local list" is visible
|
||||
variableId = Object.keys(target.variables).filter(k => target.variables[k].name === 'local list')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_listcontents');
|
||||
t.equal(monitorRecord.mode, 'list');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.width, 106); // Make sure these are imported from lists.
|
||||
t.equal(monitorRecord.height, 206);
|
||||
|
||||
// Backdrop name monitor is visible, not sprite specific
|
||||
// should get imported with id that references the name parameter
|
||||
// via '_name' at the end since the 3.0 block has a dropdown.
|
||||
monitorRecord = vm.runtime._monitorState.get('backdropnumbername_name');
|
||||
t.equal(monitorRecord.opcode, 'looks_backdropnumbername');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
// x position monitor is in large mode, specific to sprite 1
|
||||
monitorRecord = vm.runtime._monitorState.get(`${target.id}_xposition`);
|
||||
t.equal(monitorRecord.opcode, 'motion_xposition');
|
||||
t.equal(monitorRecord.mode, 'large');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, 'Sprite1');
|
||||
t.equal(monitorRecord.targetId, target.id);
|
||||
|
||||
|
||||
let monitorId;
|
||||
let monitorBlock;
|
||||
|
||||
// The monitor IDs for the sensing_current block should be unique
|
||||
// to the parameter that is selected on the block being monitored.
|
||||
// The paramater portion of the id should be lowercase even
|
||||
// though the field value on the block is uppercase.
|
||||
|
||||
monitorId = 'current_date';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'DATE');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
monitorId = 'current_minute';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'MINUTE');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
monitorId = 'current_dayofweek';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'DAYOFWEEK');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
155
scratch-vm/test/integration/monitors_sb2_to_sb3.js
Normal file
155
scratch-vm/test/integration/monitors_sb2_to_sb3.js
Normal file
@@ -0,0 +1,155 @@
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
let vm;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// TODO figure out why running threads doesn't work in this test
|
||||
// vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
const test = tap.test;
|
||||
|
||||
test('saving and loading sb2 project with monitors preserves sliderMin and sliderMax', t => {
|
||||
|
||||
vm.on('playgroundData', e /* eslint-disable-line no-unused-vars */ => {
|
||||
// TODO related to above TODO, comment these back in when we figure out
|
||||
// why running threads doesn't work with this test
|
||||
|
||||
// const threads = JSON.parse(e.threads);
|
||||
// All monitors should create threads that finish during the step and
|
||||
// are revoved from runtime.threads.
|
||||
// t.equal(threads.length, 0);
|
||||
|
||||
// we care that the last step updated the right number of monitors
|
||||
// we don't care whether the last step ran other threads or not
|
||||
// const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor);
|
||||
// t.equal(lastStepUpdatedMonitorThreads.length, 8);
|
||||
|
||||
// There should be one additional hidden monitor that is in the monitorState but
|
||||
// does not start a thread.
|
||||
t.equal(vm.runtime._monitorState.size, 9);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const target = vm.runtime.targets[1];
|
||||
|
||||
// Global variable named "global" is a slider
|
||||
let variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'global')[0];
|
||||
// Used later when checking save and load of slider min/max
|
||||
let monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_variable');
|
||||
t.equal(monitorRecord.mode, 'slider');
|
||||
t.equal(monitorRecord.sliderMin, -200); // Make sure these are imported for sliders.
|
||||
t.equal(monitorRecord.sliderMax, 30);
|
||||
t.equal(monitorRecord.isDiscrete, false);
|
||||
t.equal(monitorRecord.x, 5); // These are imported for all monitors, just check once.
|
||||
t.equal(monitorRecord.y, 59);
|
||||
t.equal(monitorRecord.visible, true);
|
||||
|
||||
// Global variable named "global list" is a list
|
||||
variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'global list')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_listcontents');
|
||||
t.equal(monitorRecord.mode, 'list');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
|
||||
// Local variable named "local" is hidden
|
||||
variableId = Object.keys(target.variables).filter(k => target.variables[k].name === 'local')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_variable');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, false);
|
||||
|
||||
// Local list named "local list" is visible
|
||||
variableId = Object.keys(target.variables).filter(k => target.variables[k].name === 'local list')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_listcontents');
|
||||
t.equal(monitorRecord.mode, 'list');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.width, 106); // Make sure these are imported from lists.
|
||||
t.equal(monitorRecord.height, 206);
|
||||
|
||||
// Backdrop name monitor is visible, not sprite specific
|
||||
// should get imported with id that references the name parameter
|
||||
// via '_name' at the end since the 3.0 block has a dropdown.
|
||||
monitorRecord = vm.runtime._monitorState.get('backdropnumbername_name');
|
||||
t.equal(monitorRecord.opcode, 'looks_backdropnumbername');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
// x position monitor is in large mode, specific to sprite 1
|
||||
monitorRecord = vm.runtime._monitorState.get(`${target.id}_xposition`);
|
||||
t.equal(monitorRecord.opcode, 'motion_xposition');
|
||||
t.equal(monitorRecord.mode, 'large');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, 'Sprite1');
|
||||
t.equal(monitorRecord.targetId, target.id);
|
||||
|
||||
|
||||
let monitorId;
|
||||
let monitorBlock;
|
||||
|
||||
// The monitor IDs for the sensing_current block should be unique
|
||||
// to the parameter that is selected on the block being monitored.
|
||||
// The paramater portion of the id should be lowercase even
|
||||
// though the field value on the block is uppercase.
|
||||
|
||||
monitorId = 'current_date';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'DATE');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
monitorId = 'current_minute';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'MINUTE');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
monitorId = 'current_dayofweek';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'DAYOFWEEK');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
const sb3ProjectJson = vm.toJSON();
|
||||
return vm.loadProject(sb3ProjectJson).then(() => {
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
268
scratch-vm/test/integration/monitors_sb3.js
Normal file
268
scratch-vm/test/integration/monitors_sb3.js
Normal file
@@ -0,0 +1,268 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const Variable = require('../../src/engine/variable');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('importing sb3 project with monitors', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
// All monitors should create threads that finish during the step and
|
||||
// are revoved from runtime.threads.
|
||||
t.equal(threads.length, 0);
|
||||
|
||||
// we care that the last step updated the right number of monitors
|
||||
// we don't care whether the last step ran other threads or not
|
||||
const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor);
|
||||
t.equal(lastStepUpdatedMonitorThreads.length, 17);
|
||||
|
||||
// There should be one additional hidden monitor that is in the monitorState but
|
||||
// does not start a thread.
|
||||
t.equal(vm.runtime._monitorState.size, 18);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const shirtSprite = vm.runtime.targets[1];
|
||||
const heartSprite = vm.runtime.targets[2];
|
||||
|
||||
// Global variable named "my variable" exists
|
||||
let variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'my variable')[0];
|
||||
let monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
let monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_variable');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
// The following few properties are imported for all monitors, just check once.
|
||||
t.equal(monitorRecord.sliderMin, 0);
|
||||
t.equal(monitorRecord.sliderMax, 100);
|
||||
t.equal(monitorRecord.isDiscrete, true); // The default if not present
|
||||
t.equal(monitorRecord.x, 10);
|
||||
t.equal(monitorRecord.y, 62);
|
||||
// Height and width are only used for list monitors and should default to 0
|
||||
// for all other monitors
|
||||
t.equal(monitorRecord.width, 0);
|
||||
t.equal(monitorRecord.height, 0);
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.type(monitorRecord.params, 'object');
|
||||
// The variable name should be stored in the monitor params
|
||||
t.equal(monitorRecord.params.VARIABLE, 'my variable');
|
||||
// Test that the monitor block and its fields were constructed correctly
|
||||
t.equal(monitorBlock.fields.VARIABLE.value, 'my variable');
|
||||
t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE');
|
||||
t.equal(monitorBlock.fields.VARIABLE.id, variableId);
|
||||
t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE);
|
||||
|
||||
// There is a global variable named 'secret_slide' which has a hidden monitor
|
||||
variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'secret_slide')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_variable');
|
||||
t.equal(monitorRecord.mode, 'slider');
|
||||
t.equal(monitorRecord.visible, false);
|
||||
t.equal(monitorRecord.sliderMin, 0);
|
||||
t.equal(monitorRecord.sliderMax, 100);
|
||||
t.type(monitorRecord.params, 'object');
|
||||
t.equal(monitorRecord.params.VARIABLE, 'secret_slide');
|
||||
// Test that the monitor block and its fields were constructed correctly
|
||||
t.equal(monitorBlock.fields.VARIABLE.value, 'secret_slide');
|
||||
t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE');
|
||||
t.equal(monitorBlock.fields.VARIABLE.id, variableId);
|
||||
t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE);
|
||||
|
||||
|
||||
// Shirt sprite has a local list named "fashion"
|
||||
variableId = Object.keys(shirtSprite.variables).filter(k => shirtSprite.variables[k].name === 'fashion')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_listcontents');
|
||||
t.equal(monitorRecord.mode, 'list');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.height, 122);
|
||||
t.equal(monitorRecord.width, 104);
|
||||
t.type(monitorRecord.params, 'object');
|
||||
t.equal(monitorRecord.params.LIST, 'fashion'); // The list name should be stored in the monitor params
|
||||
// Test that the monitor block and its fields were constructed correctly
|
||||
t.equal(monitorBlock.fields.LIST.value, 'fashion');
|
||||
t.equal(monitorBlock.fields.LIST.name, 'LIST');
|
||||
t.equal(monitorBlock.fields.LIST.id, variableId);
|
||||
t.equal(monitorBlock.fields.LIST.variableType, Variable.LIST_TYPE);
|
||||
|
||||
// Shirt sprite has a local variable named "tee"
|
||||
variableId = Object.keys(shirtSprite.variables).filter(k => shirtSprite.variables[k].name === 'tee')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_variable');
|
||||
t.equal(monitorRecord.mode, 'slider');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.sliderMin, 0);
|
||||
t.equal(monitorRecord.sliderMax, 100);
|
||||
t.type(monitorRecord.params, 'object');
|
||||
t.equal(monitorRecord.params.VARIABLE, 'tee');
|
||||
// Test that the monitor block and its fields were constructed correctly
|
||||
t.equal(monitorBlock.fields.VARIABLE.value, 'tee');
|
||||
t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE');
|
||||
t.equal(monitorBlock.fields.VARIABLE.id, variableId);
|
||||
t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE);
|
||||
|
||||
// Heart sprite has a local list named "hearty"
|
||||
variableId = Object.keys(heartSprite.variables).filter(k => heartSprite.variables[k].name === 'hearty')[0];
|
||||
monitorRecord = vm.runtime._monitorState.get(variableId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId);
|
||||
t.equal(monitorRecord.opcode, 'data_variable');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.type(monitorRecord.params, 'object');
|
||||
t.equal(monitorRecord.params.VARIABLE, 'hearty'); // The variable name should be stored in the monitor params
|
||||
// Test that the monitor block and its fields were constructed correctly
|
||||
t.equal(monitorBlock.fields.VARIABLE.value, 'hearty');
|
||||
t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE');
|
||||
t.equal(monitorBlock.fields.VARIABLE.id, variableId);
|
||||
t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE);
|
||||
|
||||
// Backdrop name monitor is visible, not sprite specific
|
||||
// should get imported with id that references the name parameter
|
||||
// via '_name' at the end since the 3.0 block has a dropdown.
|
||||
let monitorId = 'backdropnumbername_name';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'looks_backdropnumbername');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
// Test that the monitor block and its fields were constructed correctly
|
||||
t.equal(monitorBlock.fields.NUMBER_NAME.value, 'name');
|
||||
|
||||
// Backdrop name monitor is visible, not sprite specific
|
||||
// should get imported with id that references the name parameter
|
||||
// via '_number' at the end since the 3.0 block has a dropdown.
|
||||
monitorId = 'backdropnumbername_number';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'looks_backdropnumbername');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
// Test that the monitor block and its fields were constructed correctly
|
||||
t.equal(monitorBlock.fields.NUMBER_NAME.value, 'number');
|
||||
|
||||
// x position monitor is in large mode, specific to shirt sprite
|
||||
monitorId = `${shirtSprite.id}_xposition`;
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'motion_xposition');
|
||||
t.equal(monitorRecord.mode, 'large');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, 'Shirt-T');
|
||||
t.equal(monitorRecord.targetId, shirtSprite.id);
|
||||
|
||||
// y position monitor is in large mode, specific to shirt sprite
|
||||
monitorId = `${shirtSprite.id}_yposition`;
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'motion_yposition');
|
||||
t.equal(monitorRecord.mode, 'large');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, 'Shirt-T');
|
||||
t.equal(monitorRecord.targetId, shirtSprite.id);
|
||||
|
||||
// direction monitor is in large mode, specific to shirt sprite
|
||||
monitorId = `${shirtSprite.id}_direction`;
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'motion_direction');
|
||||
t.equal(monitorRecord.mode, 'large');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, 'Shirt-T');
|
||||
t.equal(monitorRecord.targetId, shirtSprite.id);
|
||||
|
||||
monitorId = `${shirtSprite.id}_size`;
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'looks_size');
|
||||
t.equal(monitorRecord.mode, 'large');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, 'Shirt-T');
|
||||
t.equal(monitorRecord.targetId, shirtSprite.id);
|
||||
|
||||
// The monitor IDs for the sensing_current block should be unique
|
||||
// to the parameter that is selected on the block being monitored.
|
||||
// The paramater portion of the id should be lowercase even
|
||||
// though the field value on the block is uppercase.
|
||||
monitorId = 'current_date';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'DATE');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
monitorId = 'current_year';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'YEAR');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
monitorId = 'current_month';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'sensing_current');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorBlock.fields.CURRENTMENU.value, 'MONTH');
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
|
||||
// Extension Monitors
|
||||
monitorId = 'music_getTempo';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'music_getTempo');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('music'), true);
|
||||
|
||||
monitorId = 'ev3_getDistance';
|
||||
monitorRecord = vm.runtime._monitorState.get(monitorId);
|
||||
t.equal(monitorRecord.opcode, 'ev3_getDistance');
|
||||
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
|
||||
t.equal(monitorRecord.mode, 'default');
|
||||
t.equal(monitorRecord.visible, true);
|
||||
t.equal(monitorRecord.spriteName, null);
|
||||
t.equal(monitorRecord.targetId, null);
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('ev3'), true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
scratch-vm/test/integration/motion.js
Normal file
38
scratch-vm/test/integration/motion.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/motion.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('motion', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length > 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
77
scratch-vm/test/integration/offline-custom-assets.js
Normal file
77
scratch-vm/test/integration/offline-custom-assets.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* This integration test ensures that a local upload of a sb2 project pulls
|
||||
* in assets correctly (from the provided .sb2 file) even if the assets
|
||||
* are not present on our servers.
|
||||
*/
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const test = require('tap').test;
|
||||
const AdmZip = require('adm-zip');
|
||||
const ScratchStorage = require('scratch-storage');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/offline-custom-assets.sb2');
|
||||
const projectZip = AdmZip(projectUri);
|
||||
const project = Buffer.from(fs.readFileSync(projectUri));
|
||||
// Custom costume from sb2 file (which was downloaded from offline editor)
|
||||
// This sound should not be available on our servers
|
||||
const costume = projectZip.readFile('1.svg');
|
||||
const costumeData = new Uint8Array(costume);
|
||||
// Custom sound recording from sb2 file (which was downloaded from offline editor)
|
||||
// This sound should not be available on our servers
|
||||
const sound = projectZip.readFile('0.wav');
|
||||
const soundData = new Uint8Array(sound);
|
||||
|
||||
test('offline-custom-assets', t => {
|
||||
const vm = new VirtualMachine();
|
||||
// Use a test storage here that does not have any web sources added to it.
|
||||
const testStorage = new ScratchStorage();
|
||||
vm.attachStorage(testStorage);
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
|
||||
// Verify initial state
|
||||
t.equals(vm.runtime.targets.length, 2);
|
||||
const costumes = vm.runtime.targets[1].getCostumes();
|
||||
t.equals(costumes.length, 1);
|
||||
const customCostume = costumes[0];
|
||||
t.equals(customCostume.name, 'A_Test_Costume');
|
||||
|
||||
const storedCostume = customCostume.asset;
|
||||
t.type(storedCostume, 'object');
|
||||
t.deepEquals(storedCostume.data, costumeData);
|
||||
|
||||
const sounds = vm.runtime.targets[1].sprite.sounds;
|
||||
t.equals(sounds.length, 1);
|
||||
const customSound = sounds[0];
|
||||
t.equals(customSound.name, 'A_Test_Recording');
|
||||
const storedSound = customSound.asset;
|
||||
t.type(storedSound, 'object');
|
||||
t.deepEquals(storedSound.data, soundData);
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
61
scratch-vm/test/integration/pen.js
Normal file
61
scratch-vm/test/integration/pen.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const Worker = require('tiny-worker');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
|
||||
const Scratch3PenBlocks = require('../../src/extensions/scratch3_pen/index.js');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/pen.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
test('pen', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', () => {
|
||||
// @todo Additional tests
|
||||
|
||||
const catSprite = vm.runtime.targets[1].sprite;
|
||||
const [originalCat, cloneCat] = catSprite.clones;
|
||||
t.notStrictEqual(originalCat, cloneCat);
|
||||
|
||||
/** @type {PenState} */
|
||||
const originalPenState = originalCat.getCustomState(Scratch3PenBlocks.STATE_KEY);
|
||||
|
||||
/** @type {PenState} */
|
||||
const clonePenState = cloneCat.getCustomState(Scratch3PenBlocks.STATE_KEY);
|
||||
|
||||
t.notStrictEqual(originalPenState, clonePenState);
|
||||
t.equal(originalPenState.penAttributes.diameter, 51);
|
||||
t.equal(clonePenState.penAttributes.diameter, 42);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project)
|
||||
.then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
scratch-vm/test/integration/procedure.js
Normal file
38
scratch-vm/test/integration/procedure.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/procedure.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('procedure', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
73
scratch-vm/test/integration/runId.js
Normal file
73
scratch-vm/test/integration/runId.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const Worker = require('tiny-worker');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
|
||||
// it doesn't really matter which project we use: we're testing side effects of loading any project
|
||||
const uri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
test('runId', async t => {
|
||||
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
const isGuid = data => guidRegex.test(data);
|
||||
|
||||
const storage = makeTestStorage();
|
||||
|
||||
// add to this list every time the RunId should have changed
|
||||
const runIdLog = [];
|
||||
const pushRunId = () => {
|
||||
const runId = storage.scratchFetch.getMetadata(storage.scratchFetch.RequestMetadata.RunId);
|
||||
t.ok(isGuid(runId), 'Run IDs should always be a properly-formatted GUID', {runId});
|
||||
runIdLog.push(runId);
|
||||
};
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
pushRunId(); // check that the initial run ID is valid
|
||||
|
||||
vm.start(); // starts the VM, not the project, so this doesn't change the run ID
|
||||
|
||||
vm.clear();
|
||||
pushRunId(); // clearing the project conceptually changes the project identity does it DOES change the run ID
|
||||
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
await vm.loadProject(project);
|
||||
pushRunId();
|
||||
|
||||
vm.greenFlag();
|
||||
pushRunId();
|
||||
|
||||
// Turn the playgroundData event into a Promise that we can await
|
||||
const playgroundDataPromise = new Promise(resolve => {
|
||||
vm.on('playgroundData', data => resolve(data));
|
||||
});
|
||||
|
||||
// Let the project run for a bit, then get playground data and stop the project
|
||||
// This test doesn't need the playground data but it does need to run & stop the project
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
pushRunId();
|
||||
}, 100);
|
||||
|
||||
// wait for the project to run to completion
|
||||
await playgroundDataPromise;
|
||||
|
||||
for (let i = 0; i < runIdLog.length - 1; ++i) {
|
||||
for (let j = i + 1; j < runIdLog.length; ++j) {
|
||||
t.notSame(runIdLog[i], runIdLog[j], 'Run IDs should always be unique', {runIdLog});
|
||||
}
|
||||
}
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
48
scratch-vm/test/integration/running_project_changed_state.js
Normal file
48
scratch-vm/test/integration/running_project_changed_state.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/looks.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('Running project should not emit project changed event', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
let projectChanged = false;
|
||||
vm.on('PROJECT_CHANGED', () => {
|
||||
projectChanged = true;
|
||||
});
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', () => {
|
||||
t.equal(projectChanged, false);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
// The test in unit/project_load_changed_state.js tests
|
||||
// that loading a project does not emit a project changed
|
||||
// event. This setup tries to be agnostic of whether that
|
||||
// test is passing or failing.
|
||||
projectChanged = false;
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
37
scratch-vm/test/integration/saythink-and-wait.js
Normal file
37
scratch-vm/test/integration/saythink-and-wait.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const Worker = require('tiny-worker');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/saythink-and-wait.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
test('say/think and wait', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, stop the project.
|
||||
// The test will fail if the project throws.
|
||||
setTimeout(() => {
|
||||
vm.stopAll();
|
||||
vm.quit();
|
||||
t.end();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
111
scratch-vm/test/integration/sb2-import-extension-monitors.js
Normal file
111
scratch-vm/test/integration/sb2-import-extension-monitors.js
Normal file
@@ -0,0 +1,111 @@
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const test = tap.test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const {readFileToBuffer, extractProjectJson} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const sb2 = require('../../src/serialization/sb2');
|
||||
|
||||
const invisibleVideoMonitorProjectUri = path.resolve(__dirname, '../fixtures/invisible-video-monitor.sb2');
|
||||
const invisibleVideoMonitorProject = readFileToBuffer(invisibleVideoMonitorProjectUri);
|
||||
|
||||
const visibleVideoMonitorProjectUri = path.resolve(
|
||||
__dirname, '../fixtures/visible-video-monitor-no-other-video-blocks.sb2');
|
||||
const visibleVideoMonitorProject = readFileToBuffer(visibleVideoMonitorProjectUri);
|
||||
|
||||
const visibleVideoMonitorAndBlocksProjectUri = path.resolve(
|
||||
__dirname, '../fixtures/visible-video-monitor-and-video-blocks.sb2');
|
||||
const visibleVideoMonitorAndBlocksProject = extractProjectJson(visibleVideoMonitorAndBlocksProjectUri);
|
||||
|
||||
const invisibleTempoMonitorProjectUri = path.resolve(
|
||||
__dirname, '../fixtures/invisible-tempo-monitor-no-other-music-blocks.sb2');
|
||||
const invisibleTempoMonitorProject = readFileToBuffer(invisibleTempoMonitorProjectUri);
|
||||
|
||||
const visibleTempoMonitorProjectUri = path.resolve(
|
||||
__dirname, '../fixtures/visible-tempo-monitor-no-other-music-blocks.sb2');
|
||||
const visibleTempoMonitorProject = readFileToBuffer(visibleTempoMonitorProjectUri);
|
||||
|
||||
test('loading sb2 project with invisible video monitor should not load monitor or extension', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(invisibleVideoMonitorProject).then(() => {
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('videoSensing'), false);
|
||||
t.equal(vm.runtime._monitorState.size, 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('loading sb2 project with visible video monitor should not load extension', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(visibleVideoMonitorProject).then(() => {
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('videoSensing'), false);
|
||||
t.equal(vm.runtime._monitorState.size, 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// This test looks a little different than the rest because loading a project with
|
||||
// the video sensing block requires a mock renderer and other setup, so instead
|
||||
// we are just using deserialize to test what we need instead
|
||||
test('sb2 project with video sensing blocks and monitor should load extension but not monitor', t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
sb2.deserialize(visibleVideoMonitorAndBlocksProject, vm.runtime).then(project => {
|
||||
// Extension loads but monitor does not
|
||||
project.extensions.extensionIDs.has('videoSensing');
|
||||
// Non-core extension monitors haven't been added to the runtime
|
||||
t.equal(vm.runtime._monitorState.size, 0);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('sb2 project with invisible music monitor should not load monitor or extension', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(invisibleTempoMonitorProject).then(() => {
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('music'), false);
|
||||
t.equal(vm.runtime._monitorState.size, 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('sb2 project with visible music monitor should load monitor and extension', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(visibleTempoMonitorProject).then(() => {
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('music'), true);
|
||||
t.equal(vm.runtime._monitorState.size, 1);
|
||||
t.equal(vm.runtime._monitorState.has('music_getTempo'), true);
|
||||
t.equal(vm.runtime._monitorState.get('music_getTempo').visible, true);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
127
scratch-vm/test/integration/sb2_corrupted_png.js
Normal file
127
scratch-vm/test/integration/sb2_corrupted_png.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* This test mocks render breaking on loading a corrupted bitmap costume.
|
||||
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||
* but keeping track of the original costume data and serializing the
|
||||
* original costume data back out. The saved project.json should not
|
||||
* reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
const costumeFileName = '1.png';
|
||||
const originalCostume = extractAsset(projectUri, costumeFileName);
|
||||
// We need to get the actual md5 because we hand modified the png to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenCostumeMd5 = md5(originalCostume);
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => {
|
||||
const base64Image = image.src.split(',')[1];
|
||||
const decodedText = Buffer.from(base64Image, 'base64').toString();
|
||||
if (decodedText.includes('Here is some')) {
|
||||
image.onerror();
|
||||
} else {
|
||||
image.onload();
|
||||
}
|
||||
}, 100);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
let defaultBitmapAssetId;
|
||||
|
||||
tap.setTimeout(30000);
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sb2 project with corrupted bitmap costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = vm.runtime.targets[1];
|
||||
t.equal(greenGuySprite.getName(), 'GreenGuy');
|
||||
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||
|
||||
const corruptedCostume = greenGuySprite.getCostumes()[0];
|
||||
t.equal(corruptedCostume.name, 'GreenGuy');
|
||||
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
|
||||
t.equal(corruptedCostume.dataFormat, 'png');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedCostume.broken);
|
||||
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save project with corrupted bitmap costume file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = resavedProject.targets[1];
|
||||
t.equal(greenGuySprite.name, 'GreenGuy');
|
||||
t.equal(greenGuySprite.costumes.length, 1);
|
||||
|
||||
const corruptedCostume = greenGuySprite.costumes[0];
|
||||
t.equal(corruptedCostume.name, 'GreenGuy');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||
t.equal(corruptedCostume.dataFormat, 'png');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume saves orignal broken costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id);
|
||||
t.equal(costumeDescs.length, 1);
|
||||
const costume = costumeDescs[0];
|
||||
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
|
||||
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||
t.end();
|
||||
});
|
||||
127
scratch-vm/test/integration/sb2_corrupted_svg.js
Normal file
127
scratch-vm/test/integration/sb2_corrupted_svg.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* This test mocks render breaking on loading a corrupted vector costume.
|
||||
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||
* but keeping track of the original costume data and serializing the
|
||||
* original costume data back out. The saved project.json should not
|
||||
* reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
const costumeFileName = '1.svg';
|
||||
const originalCostume = extractAsset(projectUri, costumeFileName);
|
||||
// We need to get the actual md5 because we hand modified the svg to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenCostumeMd5 = md5(originalCostume);
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
|
||||
setTimeout(() => image.onload(), 1000);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
let defaultVectorAssetId;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||
|
||||
// Mock renderer breaking on loading a corrupt costume
|
||||
FakeRenderer.prototype.createSVGSkin = function (svgString) {
|
||||
// Look for text added to costume to make it a corrupt svg
|
||||
if (svgString.includes('<here is some')) {
|
||||
throw new Error('mock createSVGSkin broke');
|
||||
}
|
||||
return FakeRenderer._nextSkinId++;
|
||||
};
|
||||
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sb2 project with corrupted vector costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = vm.runtime.targets[1];
|
||||
t.equal(blueGuySprite.getName(), 'Blue Guy');
|
||||
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||
|
||||
const corruptedCostume = blueGuySprite.getCostumes()[0];
|
||||
t.equal(corruptedCostume.name, 'Blue Guy 2');
|
||||
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedCostume.broken);
|
||||
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save project with corrupted vector costume file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = resavedProject.targets[1];
|
||||
t.equal(blueGuySprite.name, 'Blue Guy');
|
||||
t.equal(blueGuySprite.costumes.length, 1);
|
||||
|
||||
const corruptedCostume = blueGuySprite.costumes[0];
|
||||
t.equal(corruptedCostume.name, 'Blue Guy 2');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume saves orignal broken costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id);
|
||||
t.equal(costumeDescs.length, 1);
|
||||
const costume = costumeDescs[0];
|
||||
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
|
||||
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||
t.end();
|
||||
});
|
||||
112
scratch-vm/test/integration/sb2_missing_png.js
Normal file
112
scratch-vm/test/integration/sb2_missing_png.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles an sb2 project with
|
||||
* a missing bitmap costume. The VM should handle this safely by displaying
|
||||
* a Gray Question Mark, but keeping track of the original costume data
|
||||
* and serializing the original costume data back out. The saved project.json
|
||||
* should not reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
|
||||
const missingCostumeAssetId = 'aadce129bfe4e57f0dd81478f3ed82aa';
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => image.onload(), 100);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
|
||||
tap.setTimeout(30000);
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sb2 project with missing bitmap costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = vm.runtime.targets[1];
|
||||
t.equal(greenGuySprite.getName(), 'GreenGuy');
|
||||
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||
|
||||
const missingCostume = greenGuySprite.getCostumes()[0];
|
||||
t.equal(missingCostume.name, 'GreenGuy');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'png');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingCostume.broken);
|
||||
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sb2 project with missing costume file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = resavedProject.targets[1];
|
||||
t.equal(greenGuySprite.name, 'GreenGuy');
|
||||
t.equal(greenGuySprite.costumes.length, 1);
|
||||
|
||||
const missingCostume = greenGuySprite.costumes[0];
|
||||
t.equal(missingCostume.name, 'GreenGuy');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'png');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime);
|
||||
|
||||
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
|
||||
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.png`);
|
||||
|
||||
t.end();
|
||||
});
|
||||
109
scratch-vm/test/integration/sb2_missing_svg.js
Normal file
109
scratch-vm/test/integration/sb2_missing_svg.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles an sb2 project with
|
||||
* a missing vector costume. The VM should handle this safely by displaying
|
||||
* a Gray Question Mark, but keeping track of the original costume data
|
||||
* and serializing the original costume data back out. The saved project.json
|
||||
* should not reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const missingCostumeAssetId = 'beca8009621913e2f5b3111eed2d8210';
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => image.onload(), 1000);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sb2 project with missing vector costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = vm.runtime.targets[1];
|
||||
t.equal(blueGuySprite.getName(), 'Blue Guy');
|
||||
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||
|
||||
const missingCostume = blueGuySprite.getCostumes()[0];
|
||||
t.equal(missingCostume.name, 'Blue Guy 2');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'svg');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingCostume.broken);
|
||||
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sb2 project with missing costume file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = resavedProject.targets[1];
|
||||
t.equal(blueGuySprite.name, 'Blue Guy');
|
||||
t.equal(blueGuySprite.costumes.length, 1);
|
||||
|
||||
const missingCostume = blueGuySprite.costumes[0];
|
||||
t.equal(missingCostume.name, 'Blue Guy 2');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'svg');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime);
|
||||
|
||||
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
|
||||
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.svg`);
|
||||
|
||||
t.end();
|
||||
});
|
||||
110
scratch-vm/test/integration/sb3-roundtrip.js
Normal file
110
scratch-vm/test/integration/sb3-roundtrip.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const Blocks = require('../../src/engine/blocks');
|
||||
const Clone = require('../../src/util/clone');
|
||||
const {loadCostume} = require('../../src/import/load-costume');
|
||||
const {loadSound} = require('../../src/import/load-sound');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const sb3 = require('../../src/serialization/sb3');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
|
||||
const defaultCostumeInfo = {
|
||||
bitmapResolution: 1,
|
||||
rotationCenterX: 0,
|
||||
rotationCenterY: 0
|
||||
};
|
||||
|
||||
const defaultSoundInfo = {
|
||||
};
|
||||
|
||||
test('sb3-roundtrip', t => {
|
||||
const runtime1 = new Runtime();
|
||||
runtime1.attachStorage(makeTestStorage());
|
||||
|
||||
const runtime2 = new Runtime();
|
||||
runtime2.attachStorage(makeTestStorage());
|
||||
|
||||
const testRuntimeState = (label, runtime) => {
|
||||
t.strictEqual(runtime.targets.length, 2, `${label}: target count`);
|
||||
const [stageClone, spriteClone] = runtime.targets;
|
||||
|
||||
t.strictEqual(stageClone.isOriginal, true);
|
||||
t.strictEqual(stageClone.isStage, true);
|
||||
|
||||
const stage = stageClone.sprite;
|
||||
t.strictEqual(stage.name, 'Stage');
|
||||
t.strictEqual(stage.clones.length, 1);
|
||||
t.strictEqual(stage.clones[0], stageClone);
|
||||
|
||||
t.strictEqual(stage.costumes.length, 1);
|
||||
const [building] = stage.costumes;
|
||||
t.strictEqual(building.assetId, 'fe5e3566965f9de793beeffce377d054');
|
||||
t.strictEqual(building.dataFormat, 'jpg');
|
||||
|
||||
t.strictEqual(stage.sounds.length, 0);
|
||||
|
||||
t.strictEqual(spriteClone.isOriginal, true);
|
||||
t.strictEqual(spriteClone.isStage, false);
|
||||
|
||||
const sprite = spriteClone.sprite;
|
||||
t.strictEqual(sprite.name, 'Sprite');
|
||||
t.strictEqual(sprite.clones.length, 1);
|
||||
t.strictEqual(sprite.clones[0], spriteClone);
|
||||
|
||||
t.strictEqual(sprite.costumes.length, 2);
|
||||
const [cat, squirrel] = sprite.costumes;
|
||||
t.strictEqual(cat.assetId, 'f88bf1935daea28f8ca098462a31dbb0');
|
||||
t.strictEqual(cat.dataFormat, 'svg');
|
||||
t.strictEqual(squirrel.assetId, '7e24c99c1b853e52f8e7f9004416fa34');
|
||||
t.strictEqual(squirrel.dataFormat, 'png');
|
||||
|
||||
t.strictEqual(sprite.sounds.length, 1);
|
||||
const [meow] = sprite.sounds;
|
||||
t.strictEqual(meow.md5, '83c36d806dc92327b9e7049a565c6bff.wav');
|
||||
};
|
||||
|
||||
const loadThings = Promise.all([
|
||||
loadCostume('fe5e3566965f9de793beeffce377d054.jpg', Clone.simple(defaultCostumeInfo), runtime1),
|
||||
loadCostume('f88bf1935daea28f8ca098462a31dbb0.svg', Clone.simple(defaultCostumeInfo), runtime1),
|
||||
loadCostume('7e24c99c1b853e52f8e7f9004416fa34.png', Clone.simple(defaultCostumeInfo), runtime1),
|
||||
loadSound(Object.assign({md5: '83c36d806dc92327b9e7049a565c6bff.wav'}, defaultSoundInfo), runtime1)
|
||||
]);
|
||||
|
||||
const installThings = loadThings.then(results => {
|
||||
const [building, cat, squirrel, meow] = results;
|
||||
|
||||
const stageBlocks = new Blocks(runtime1);
|
||||
const stage = new Sprite(stageBlocks, runtime1);
|
||||
stage.name = 'Stage';
|
||||
stage.costumes = [building];
|
||||
stage.sounds = [];
|
||||
const stageClone = stage.createClone();
|
||||
stageClone.isStage = true;
|
||||
|
||||
const spriteBlocks = new Blocks(runtime1);
|
||||
const sprite = new Sprite(spriteBlocks, runtime1);
|
||||
sprite.name = 'Sprite';
|
||||
sprite.costumes = [cat, squirrel];
|
||||
sprite.sounds = [meow];
|
||||
const spriteClone = sprite.createClone();
|
||||
|
||||
runtime1.targets = [stageClone, spriteClone];
|
||||
|
||||
testRuntimeState('original', runtime1);
|
||||
});
|
||||
|
||||
const serializeAndDeserialize = installThings.then(() => {
|
||||
// Doing a JSON `stringify` and `parse` here more accurately simulate a save/load cycle. In particular:
|
||||
// 1. it ensures that any non-serializable data is thrown away, and
|
||||
// 2. `sb3.deserialize` and its helpers do some `hasOwnProperty` checks which fail on the object returned by
|
||||
// `sb3.serialize` but succeed if that object is "flattened" in this way.
|
||||
const serializedState = JSON.parse(JSON.stringify(sb3.serialize(runtime1)));
|
||||
return sb3.deserialize(serializedState, runtime2);
|
||||
});
|
||||
|
||||
return serializeAndDeserialize.then(({targets}) => {
|
||||
runtime2.targets = targets;
|
||||
testRuntimeState('copy', runtime2);
|
||||
});
|
||||
});
|
||||
127
scratch-vm/test/integration/sb3_corrupted_png.js
Normal file
127
scratch-vm/test/integration/sb3_corrupted_png.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* This test mocks render breaking on loading a corrupted bitmap costume.
|
||||
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||
* but keeping track of the original costume data and serializing the
|
||||
* original costume data back out. The saved project.json should not
|
||||
* reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
const costumeFileName = 'e1320c21995dcf6de10119be7f08c26b.png';
|
||||
const originalCostume = extractAsset(projectUri, costumeFileName);
|
||||
// We need to get the actual md5 because we hand modified the png to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenCostumeMd5 = md5(originalCostume);
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => {
|
||||
const base64Image = image.src.split(',')[1];
|
||||
const decodedText = Buffer.from(base64Image, 'base64').toString();
|
||||
if (decodedText.includes('Here is some')) {
|
||||
image.onerror();
|
||||
} else {
|
||||
image.onload();
|
||||
}
|
||||
}, 100);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
let defaultBitmapAssetId;
|
||||
|
||||
tap.setTimeout(30000);
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sb3 project with corrupted bitmap costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = vm.runtime.targets[1];
|
||||
t.equal(greenGuySprite.getName(), 'Green Guy');
|
||||
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||
|
||||
const corruptedCostume = greenGuySprite.getCostumes()[0];
|
||||
t.equal(corruptedCostume.name, 'Green Guy');
|
||||
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
|
||||
t.equal(corruptedCostume.dataFormat, 'png');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedCostume.broken);
|
||||
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save project with corrupted bitmap costume file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = resavedProject.targets[1];
|
||||
t.equal(greenGuySprite.name, 'Green Guy');
|
||||
t.equal(greenGuySprite.costumes.length, 1);
|
||||
|
||||
const corruptedCostume = greenGuySprite.costumes[0];
|
||||
t.equal(corruptedCostume.name, 'Green Guy');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||
t.equal(corruptedCostume.dataFormat, 'png');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume saves orignal broken costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id);
|
||||
t.equal(costumeDescs.length, 1);
|
||||
const costume = costumeDescs[0];
|
||||
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
|
||||
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||
t.end();
|
||||
});
|
||||
119
scratch-vm/test/integration/sb3_corrupted_sound.js
Normal file
119
scratch-vm/test/integration/sb3_corrupted_sound.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* This test mocks breaking on loading a corrupted sound.
|
||||
* The VM should handle this safely by replacing the sound data with the default (empty) sound,
|
||||
* but keeping track of the original sound data and serializing the
|
||||
* original sound data back out. The saved project.json should not
|
||||
* reflect that the sound is broken and should therefore re-attempt
|
||||
* to load the sound if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeSounds} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/corrupt_sound.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
const soundFileName = '78618aadd225b1db7bf837fa17dc0568.wav';
|
||||
const originalSound = extractAsset(projectUri, soundFileName);
|
||||
// We need to get the actual md5 because we hand modified the sound file to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenSoundMd5 = md5(originalSound);
|
||||
|
||||
let fakeId = -1;
|
||||
|
||||
const FakeAudioEngine = function () {
|
||||
return {
|
||||
decodeSoundPlayer: soundData => {
|
||||
const soundDataString = soundData.asset.decodeText();
|
||||
if (soundDataString.includes('here is some')) {
|
||||
return Promise.reject(new Error('mock audio engine broke'));
|
||||
}
|
||||
|
||||
// Otherwise return fake data
|
||||
return Promise.resolve({
|
||||
id: fakeId++,
|
||||
buffer: {
|
||||
sampleRate: 1,
|
||||
length: 1
|
||||
}
|
||||
});
|
||||
},
|
||||
createBank: () => null
|
||||
};
|
||||
};
|
||||
|
||||
let vm;
|
||||
let defaultSoundAssetId;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultSoundAssetId = vm.runtime.storage.defaultAssetId.Sound;
|
||||
|
||||
vm.attachAudioEngine(FakeAudioEngine());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sb3 project with corrupted sound file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const catSprite = vm.runtime.targets[1];
|
||||
t.equal(catSprite.getName(), 'Sprite1');
|
||||
t.equal(catSprite.getSounds().length, 1);
|
||||
|
||||
const corruptedSound = catSprite.getSounds()[0];
|
||||
t.equal(corruptedSound.name, 'Boop Sound Recording');
|
||||
t.equal(corruptedSound.assetId, defaultSoundAssetId);
|
||||
t.equal(corruptedSound.dataFormat, 'wav');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedSound.broken);
|
||||
t.equal(corruptedSound.broken.assetId, brokenSoundMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedSound.broken.asset.data), brokenSoundMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save project with corrupted sound file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const catSprite = resavedProject.targets[1];
|
||||
t.equal(catSprite.name, 'Sprite1');
|
||||
t.equal(catSprite.sounds.length, 1);
|
||||
|
||||
const corruptedSound = catSprite.sounds[0];
|
||||
t.equal(corruptedSound.name, 'Boop Sound Recording');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedSound.assetId, brokenSoundMd5);
|
||||
t.equal(corruptedSound.dataFormat, 'wav');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedSound.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeSounds saves orignal broken sound', t => {
|
||||
const soundDescs = serializeSounds(vm.runtime, vm.runtime.targets[1].id);
|
||||
t.equal(soundDescs.length, 1);
|
||||
const sound = soundDescs[0];
|
||||
t.equal(sound.fileName, `${brokenSoundMd5}.wav`);
|
||||
t.equal(md5(sound.fileContent), brokenSoundMd5);
|
||||
t.end();
|
||||
});
|
||||
106
scratch-vm/test/integration/sb3_corrupted_svg.js
Normal file
106
scratch-vm/test/integration/sb3_corrupted_svg.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* This test mocks render breaking on loading a corrupted vector costume.
|
||||
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||
* but keeping track of the original costume data and serializing the
|
||||
* original costume data back out. The saved project.json should not
|
||||
* reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
const costumeFileName = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb.svg';
|
||||
const originalCostume = extractAsset(projectUri, costumeFileName);
|
||||
// We need to get the actual md5 because we hand modified the svg to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenCostumeMd5 = md5(originalCostume);
|
||||
|
||||
let vm;
|
||||
let defaultVectorAssetId;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||
|
||||
// Mock renderer breaking on loading a corrupt costume
|
||||
FakeRenderer.prototype.createSVGSkin = function (svgString) {
|
||||
// Look for text added to costume to make it a corrupt svg
|
||||
if (svgString.includes('<here is some')) {
|
||||
throw new Error('mock createSVGSkin broke');
|
||||
}
|
||||
return FakeRenderer._nextSkinId++;
|
||||
};
|
||||
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sb3 project with corrupted vector costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = vm.runtime.targets[1];
|
||||
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
|
||||
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||
|
||||
const corruptedCostume = blueGuySprite.getCostumes()[0];
|
||||
t.equal(corruptedCostume.name, 'costume1');
|
||||
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedCostume.broken);
|
||||
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save project with corrupted vector costume file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = resavedProject.targets[1];
|
||||
t.equal(blueGuySprite.name, 'Blue Square Guy');
|
||||
t.equal(blueGuySprite.costumes.length, 1);
|
||||
|
||||
const corruptedCostume = blueGuySprite.costumes[0];
|
||||
t.equal(corruptedCostume.name, 'costume1');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume saves orignal broken costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id);
|
||||
t.equal(costumeDescs.length, 1);
|
||||
const costume = costumeDescs[0];
|
||||
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
|
||||
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||
t.end();
|
||||
});
|
||||
112
scratch-vm/test/integration/sb3_missing_png.js
Normal file
112
scratch-vm/test/integration/sb3_missing_png.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles an sb3 project with
|
||||
* a missing bitmap costume. The VM should handle this safely by displaying
|
||||
* a Gray Question Mark, but keeping track of the original costume data
|
||||
* and serializing the original costume data back out. The saved project.json
|
||||
* should not reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
|
||||
const missingCostumeAssetId = 'e1320c21995dcf6de10119be7f08c26b';
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => image.onload(), 100);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
|
||||
tap.setTimeout(30000);
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sb3 project with missing bitmap costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = vm.runtime.targets[1];
|
||||
t.equal(greenGuySprite.getName(), 'Green Guy');
|
||||
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||
|
||||
const missingCostume = greenGuySprite.getCostumes()[0];
|
||||
t.equal(missingCostume.name, 'Green Guy');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'png');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingCostume.broken);
|
||||
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sb3 project with missing costume file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = resavedProject.targets[1];
|
||||
t.equal(greenGuySprite.name, 'Green Guy');
|
||||
t.equal(greenGuySprite.costumes.length, 1);
|
||||
|
||||
const missingCostume = greenGuySprite.costumes[0];
|
||||
t.equal(missingCostume.name, 'Green Guy');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'png');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime);
|
||||
|
||||
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
|
||||
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.png`);
|
||||
|
||||
t.end();
|
||||
});
|
||||
86
scratch-vm/test/integration/sb3_missing_sound.js
Normal file
86
scratch-vm/test/integration/sb3_missing_sound.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles an sb3 project with
|
||||
* a missing sound. The project should load without error.
|
||||
* TODO: handle missing or corrupted sounds by replacing the missing sound data
|
||||
* with the empty sound file but keeping the info about the original missing / corrupted sound
|
||||
* so that user data does not get overwritten / lost.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeSounds} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/missing_sound.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const missingSoundAssetId = '78618aadd225b1db7bf837fa17dc0568';
|
||||
|
||||
let vm;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sb3 project with missing sound file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const catSprite = vm.runtime.targets[1];
|
||||
t.equal(catSprite.getSounds().length, 1);
|
||||
|
||||
const missingSound = catSprite.getSounds()[0];
|
||||
t.equal(missingSound.name, 'Boop Sound Recording');
|
||||
// Sound should have original data but no asset
|
||||
const defaultSoundAssetId = vm.runtime.storage.defaultAssetId.Sound;
|
||||
t.equal(missingSound.assetId, defaultSoundAssetId);
|
||||
t.equal(missingSound.dataFormat, 'wav');
|
||||
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingSound.broken);
|
||||
t.equal(missingSound.broken.assetId, missingSoundAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sb3 project with missing sound file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const catSprite = resavedProject.targets[1];
|
||||
t.equal(catSprite.name, 'Sprite1');
|
||||
t.equal(catSprite.sounds.length, 1);
|
||||
|
||||
const missingSound = catSprite.sounds[0];
|
||||
t.equal(missingSound.name, 'Boop Sound Recording');
|
||||
// Costume should have both default sound data (e.g. "Gray Question Sound" ^_^) and original data
|
||||
t.equal(missingSound.assetId, missingSoundAssetId);
|
||||
t.equal(missingSound.dataFormat, 'wav');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingSound.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const soundDescs = serializeSounds(vm.runtime);
|
||||
|
||||
t.equal(soundDescs.length, 1); // Should only have one sound, the pop sound for the stage
|
||||
t.not(soundDescs[0].fileName, `${missingSoundAssetId}.wav`);
|
||||
|
||||
t.end();
|
||||
});
|
||||
89
scratch-vm/test/integration/sb3_missing_svg.js
Normal file
89
scratch-vm/test/integration/sb3_missing_svg.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles an sb3 project with
|
||||
* a missing vector costume. The VM should handle this safely by displaying
|
||||
* a Gray Question Mark, but keeping track of the original costume data
|
||||
* and serializing the original costume data back out. The saved project.json
|
||||
* should not reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const missingCostumeAssetId = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb';
|
||||
|
||||
let vm;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
|
||||
return vm.loadProject(project);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sb3 project with missing vector costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = vm.runtime.targets[1];
|
||||
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
|
||||
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||
|
||||
const missingCostume = blueGuySprite.getCostumes()[0];
|
||||
t.equal(missingCostume.name, 'costume1');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'svg');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingCostume.broken);
|
||||
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sb3 project with missing costume file', t => {
|
||||
const resavedProject = JSON.parse(vm.toJSON());
|
||||
|
||||
t.equal(resavedProject.targets.length, 2);
|
||||
|
||||
const stage = resavedProject.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = resavedProject.targets[1];
|
||||
t.equal(blueGuySprite.name, 'Blue Square Guy');
|
||||
t.equal(blueGuySprite.costumes.length, 1);
|
||||
|
||||
const missingCostume = blueGuySprite.costumes[0];
|
||||
t.equal(missingCostume.name, 'costume1');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'svg');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime);
|
||||
|
||||
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
|
||||
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.svg`);
|
||||
|
||||
t.end();
|
||||
});
|
||||
38
scratch-vm/test/integration/sensing.js
Normal file
38
scratch-vm/test/integration/sensing.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/sensing.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('sensing', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length > 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
43
scratch-vm/test/integration/sound.js
Normal file
43
scratch-vm/test/integration/sound.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const Worker = require('tiny-worker');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/sound.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
test('sound', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length > 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
126
scratch-vm/test/integration/sprite2_corrupted_png.js
Normal file
126
scratch-vm/test/integration/sprite2_corrupted_png.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* This test mocks render breaking on loading a sprite2 with a
|
||||
* corrupted bitmap costume.
|
||||
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||
* but keeping track of the original costume data and serializing the
|
||||
* original costume data back out. The saved project.json should not
|
||||
* reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_png.sprite2');
|
||||
const sprite = readFileToBuffer(spriteUri);
|
||||
|
||||
const costumeFileName = '0.png';
|
||||
const originalCostume = extractAsset(spriteUri, costumeFileName);
|
||||
// We need to get the actual md5 because we hand modified the png to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenCostumeMd5 = md5(originalCostume);
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => {
|
||||
const base64Image = image.src.split(',')[1];
|
||||
const decodedText = Buffer.from(base64Image, 'base64').toString();
|
||||
if (decodedText.includes('Here is some')) {
|
||||
image.onerror();
|
||||
} else {
|
||||
image.onload();
|
||||
}
|
||||
}, 100);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
let defaultBitmapAssetId;
|
||||
|
||||
tap.setTimeout(30000);
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sprite2 with corrupted bitmap costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 3);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = vm.runtime.targets[2];
|
||||
t.equal(greenGuySprite.getName(), 'GreenGuy');
|
||||
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||
|
||||
const corruptedCostume = greenGuySprite.getCostumes()[0];
|
||||
t.equal(corruptedCostume.name, 'GreenGuy');
|
||||
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
|
||||
t.equal(corruptedCostume.dataFormat, 'png');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedCostume.broken);
|
||||
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sprite with corrupted costume file', t => {
|
||||
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||
|
||||
t.equal(resavedSprite.name, 'GreenGuy');
|
||||
t.equal(resavedSprite.costumes.length, 1);
|
||||
|
||||
const corruptedCostume = resavedSprite.costumes[0];
|
||||
t.equal(corruptedCostume.name, 'GreenGuy');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||
t.equal(corruptedCostume.dataFormat, 'png');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume saves orignal broken costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||
t.equal(costumeDescs.length, 1);
|
||||
const costume = costumeDescs[0];
|
||||
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
|
||||
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||
t.end();
|
||||
});
|
||||
125
scratch-vm/test/integration/sprite2_corrupted_svg.js
Normal file
125
scratch-vm/test/integration/sprite2_corrupted_svg.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* This test mocks render breaking on loading a sprite2 with a
|
||||
* corrupted vector costume.
|
||||
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||
* but keeping track of the original costume data and serializing the
|
||||
* original costume data back out. The saved project.json should not
|
||||
* reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sprite2');
|
||||
const sprite = readFileToBuffer(spriteUri);
|
||||
|
||||
const costumeFileName = '0.svg';
|
||||
const originalCostume = extractAsset(spriteUri, costumeFileName);
|
||||
// We need to get the actual md5 because we hand modified the svg to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenCostumeMd5 = md5(originalCostume);
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => image.onload(), 1000);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
let defaultVectorAssetId;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||
|
||||
// Mock renderer breaking on loading a corrupt costume
|
||||
FakeRenderer.prototype.createSVGSkin = function (svgString) {
|
||||
// Look for text added to costume to make it a corrupt svg
|
||||
if (svgString.includes('<here is some')) {
|
||||
throw new Error('mock createSVGSkin broke');
|
||||
}
|
||||
return FakeRenderer.prototype._nextSkinId++;
|
||||
};
|
||||
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sprite2 with corrupted vector costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 3);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = vm.runtime.targets[2];
|
||||
t.equal(blueGuySprite.getName(), 'Blue Guy');
|
||||
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||
|
||||
const corruptedCostume = blueGuySprite.getCostumes()[0];
|
||||
t.equal(corruptedCostume.name, 'Blue Guy 2');
|
||||
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedCostume.broken);
|
||||
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sprite with corrupted costume file', t => {
|
||||
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||
|
||||
t.equal(resavedSprite.name, 'Blue Guy');
|
||||
t.equal(resavedSprite.costumes.length, 1);
|
||||
|
||||
const corruptedCostume = resavedSprite.costumes[0];
|
||||
t.equal(corruptedCostume.name, 'Blue Guy 2');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume saves orignal broken costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||
t.equal(costumeDescs.length, 1);
|
||||
const costume = costumeDescs[0];
|
||||
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
|
||||
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||
t.end();
|
||||
});
|
||||
108
scratch-vm/test/integration/sprite2_missing_png.js
Normal file
108
scratch-vm/test/integration/sprite2_missing_png.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles a sprite2 file with
|
||||
* a missing bitmap costume. The VM should handle this safely by displaying
|
||||
* a Gray Question Mark, but keeping track of the original costume data
|
||||
* and serializing the original costume data back out. The saved project.json
|
||||
* should not reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
// The particular project that we're loading doesn't matter for this test
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/missing_png.sprite2');
|
||||
const sprite = readFileToBuffer(spriteUri);
|
||||
|
||||
const missingCostumeAssetId = 'aadce129bfe4e57f0dd81478f3ed82aa';
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => image.onload(), 100);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
|
||||
tap.setTimeout(30000);
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sprite2 with missing bitmap costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 3);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = vm.runtime.targets[2];
|
||||
t.equal(greenGuySprite.getName(), 'GreenGuy');
|
||||
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||
|
||||
const missingCostume = greenGuySprite.getCostumes()[0];
|
||||
t.equal(missingCostume.name, 'GreenGuy');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
const defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||
t.equal(missingCostume.assetId, defaultBitmapAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'png');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingCostume.broken);
|
||||
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sprite2 with missing bitmap costume file', t => {
|
||||
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||
|
||||
t.equal(resavedSprite.name, 'GreenGuy');
|
||||
t.equal(resavedSprite.costumes.length, 1);
|
||||
|
||||
const missingCostume = resavedSprite.costumes[0];
|
||||
t.equal(missingCostume.name, 'GreenGuy');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'png');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||
|
||||
t.equal(costumeDescs.length, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
106
scratch-vm/test/integration/sprite2_missing_svg.js
Normal file
106
scratch-vm/test/integration/sprite2_missing_svg.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles a sprite2 file with
|
||||
* a missing vector costume. The VM should handle this safely by displaying
|
||||
* a Gray Question Mark, but keeping track of the original costume data
|
||||
* and serializing the original costume data back out. The saved project.json
|
||||
* should not reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
// The particular project that we're loading doesn't matter for this test
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/missing_svg.sprite2');
|
||||
const sprite = readFileToBuffer(spriteUri);
|
||||
|
||||
const missingCostumeAssetId = 'beca8009621913e2f5b3111eed2d8210';
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => image.onload(), 1000);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sprite2 with missing vector costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 3);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = vm.runtime.targets[2];
|
||||
t.equal(blueGuySprite.getName(), 'Blue Guy');
|
||||
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||
|
||||
const missingCostume = blueGuySprite.getCostumes()[0];
|
||||
t.equal(missingCostume.name, 'Blue Guy 2');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'svg');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingCostume.broken);
|
||||
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sprite2 with missing vector costume file', t => {
|
||||
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||
|
||||
t.equal(resavedSprite.name, 'Blue Guy');
|
||||
t.equal(resavedSprite.costumes.length, 1);
|
||||
|
||||
const missingCostume = resavedSprite.costumes[0];
|
||||
t.equal(missingCostume.name, 'Blue Guy 2');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'svg');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||
|
||||
t.equal(costumeDescs.length, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
126
scratch-vm/test/integration/sprite3_corrupted_png.js
Normal file
126
scratch-vm/test/integration/sprite3_corrupted_png.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* This test mocks render breaking on loading a sprite3 with a
|
||||
* corrupted bitmap costume.
|
||||
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||
* but keeping track of the original costume data and serializing the
|
||||
* original costume data back out. The saved project.json should not
|
||||
* reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_png.sprite3');
|
||||
const sprite = readFileToBuffer(spriteUri);
|
||||
|
||||
const costumeFileName = 'e1320c21995dcf6de10119be7f08c26b.png';
|
||||
const originalCostume = extractAsset(spriteUri, costumeFileName);
|
||||
// We need to get the actual md5 because we hand modified the png to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenCostumeMd5 = md5(originalCostume);
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => {
|
||||
const base64Image = image.src.split(',')[1];
|
||||
const decodedText = Buffer.from(base64Image, 'base64').toString();
|
||||
if (decodedText.includes('Here is some')) {
|
||||
image.onerror();
|
||||
} else {
|
||||
image.onload();
|
||||
}
|
||||
}, 100);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
let defaultBitmapAssetId;
|
||||
|
||||
tap.setTimeout(30000);
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sprite3 with corrupted bitmap costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 3);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = vm.runtime.targets[2];
|
||||
t.equal(greenGuySprite.getName(), 'Green Guy');
|
||||
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||
|
||||
const corruptedCostume = greenGuySprite.getCostumes()[0];
|
||||
t.equal(corruptedCostume.name, 'Green Guy');
|
||||
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
|
||||
t.equal(corruptedCostume.dataFormat, 'png');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedCostume.broken);
|
||||
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sprite with corrupted costume file', t => {
|
||||
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||
|
||||
t.equal(resavedSprite.name, 'Green Guy');
|
||||
t.equal(resavedSprite.costumes.length, 1);
|
||||
|
||||
const corruptedCostume = resavedSprite.costumes[0];
|
||||
t.equal(corruptedCostume.name, 'Green Guy');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||
t.equal(corruptedCostume.dataFormat, 'png');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume saves orignal broken costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||
t.equal(costumeDescs.length, 1);
|
||||
const costume = costumeDescs[0];
|
||||
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
|
||||
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||
t.end();
|
||||
});
|
||||
105
scratch-vm/test/integration/sprite3_corrupted_svg.js
Normal file
105
scratch-vm/test/integration/sprite3_corrupted_svg.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* This test mocks render breaking on loading a sprite with a
|
||||
* corrupted vector costume.
|
||||
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||
* but keeping track of the original costume data and serializing the
|
||||
* original costume data back out. The saved project.json should not
|
||||
* reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const md5 = require('js-md5');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sprite3');
|
||||
const sprite = readFileToBuffer(spriteUri);
|
||||
|
||||
const costumeFileName = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb.svg';
|
||||
const originalCostume = extractAsset(spriteUri, costumeFileName);
|
||||
// We need to get the actual md5 because we hand modified the svg to corrupt it
|
||||
// after we downloaded the project from Scratch
|
||||
// Loading the project back into the VM will correct the assetId and md5
|
||||
const brokenCostumeMd5 = md5(originalCostume);
|
||||
|
||||
let vm;
|
||||
let defaultVectorAssetId;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||
|
||||
// Mock renderer breaking on loading a corrupt costume
|
||||
FakeRenderer.prototype.createSVGSkin = function (svgString) {
|
||||
// Look for text added to costume to make it a corrupt svg
|
||||
if (svgString.includes('<here is some')) {
|
||||
throw new Error('mock createSVGSkin broke');
|
||||
}
|
||||
return FakeRenderer.prototype._nextSkinId++;
|
||||
};
|
||||
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
|
||||
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('load sprite3 with corrupted vector costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 3);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = vm.runtime.targets[2];
|
||||
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
|
||||
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||
|
||||
const corruptedCostume = blueGuySprite.getCostumes()[0];
|
||||
t.equal(corruptedCostume.name, 'costume1');
|
||||
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(corruptedCostume.broken);
|
||||
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||
// Verify that we saved the original asset data
|
||||
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sprite with corrupted costume file', t => {
|
||||
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||
|
||||
t.equal(resavedSprite.name, 'Blue Square Guy');
|
||||
t.equal(resavedSprite.costumes.length, 1);
|
||||
|
||||
const corruptedCostume = resavedSprite.costumes[0];
|
||||
t.equal(corruptedCostume.name, 'costume1');
|
||||
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(corruptedCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume saves orignal broken costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||
t.equal(costumeDescs.length, 1);
|
||||
const costume = costumeDescs[0];
|
||||
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
|
||||
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||
t.end();
|
||||
});
|
||||
108
scratch-vm/test/integration/sprite3_missing_png.js
Normal file
108
scratch-vm/test/integration/sprite3_missing_png.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles a sprite3 file with
|
||||
* a missing bitmap costume. The VM should handle this safely by displaying
|
||||
* a Gray Question Mark, but keeping track of the original costume data
|
||||
* and serializing the original costume data back out. The saved project.json
|
||||
* should not reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
// The particular project that we're loading doesn't matter for this test
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/missing_png.sprite3');
|
||||
const sprite = readFileToBuffer(spriteUri);
|
||||
|
||||
const missingCostumeAssetId = 'e1320c21995dcf6de10119be7f08c26b';
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
setTimeout(() => image.onload(), 100);
|
||||
return image;
|
||||
};
|
||||
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
// Create mock canvas
|
||||
getContext: () => ({
|
||||
drawImage: () => ({})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let vm;
|
||||
|
||||
tap.setTimeout(30000);
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
|
||||
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sprite3 with missing bitmap costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 3);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const greenGuySprite = vm.runtime.targets[2];
|
||||
t.equal(greenGuySprite.getName(), 'Green Guy');
|
||||
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||
|
||||
const missingCostume = greenGuySprite.getCostumes()[0];
|
||||
t.equal(missingCostume.name, 'Green Guy');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
const defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||
t.equal(missingCostume.assetId, defaultBitmapAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'png');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingCostume.broken);
|
||||
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sprite3 with missing bitmap costume file', t => {
|
||||
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||
|
||||
t.equal(resavedSprite.name, 'Green Guy');
|
||||
t.equal(resavedSprite.costumes.length, 1);
|
||||
|
||||
const missingCostume = resavedSprite.costumes[0];
|
||||
t.equal(missingCostume.name, 'Green Guy');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'png');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||
|
||||
t.equal(costumeDescs.length, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
86
scratch-vm/test/integration/sprite3_missing_svg.js
Normal file
86
scratch-vm/test/integration/sprite3_missing_svg.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* This test ensures that the VM gracefully handles a sprite3 file with
|
||||
* a missing vector costume. The VM should handle this safely by displaying
|
||||
* a Gray Question Mark, but keeping track of the original costume data
|
||||
* and serializing the original costume data back out. The saved project.json
|
||||
* should not reflect that the costume is broken and should therefore re-attempt
|
||||
* to load the costume if the saved project is re-loaded.
|
||||
*/
|
||||
const path = require('path');
|
||||
const tap = require('tap');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||
|
||||
// The particular project that we're loading doesn't matter for this test
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/missing_svg.sprite3');
|
||||
const sprite = readFileToBuffer(spriteUri);
|
||||
|
||||
const missingCostumeAssetId = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb';
|
||||
|
||||
let vm;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const storage = makeTestStorage();
|
||||
|
||||
vm = new VirtualMachine();
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
|
||||
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('loading sprite3 with missing vector costume file', t => {
|
||||
t.equal(vm.runtime.targets.length, 3);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
t.ok(stage.isStage);
|
||||
|
||||
const blueGuySprite = vm.runtime.targets[2];
|
||||
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
|
||||
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||
|
||||
const missingCostume = blueGuySprite.getCostumes()[0];
|
||||
t.equal(missingCostume.name, 'costume1');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'svg');
|
||||
// Runtime should have info about broken asset
|
||||
t.ok(missingCostume.broken);
|
||||
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load and then save sprite3 with missing vector costume file', t => {
|
||||
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||
|
||||
t.equal(resavedSprite.name, 'Blue Square Guy');
|
||||
t.equal(resavedSprite.costumes.length, 1);
|
||||
|
||||
const missingCostume = resavedSprite.costumes[0];
|
||||
t.equal(missingCostume.name, 'costume1');
|
||||
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||
t.equal(missingCostume.dataFormat, 'svg');
|
||||
// Test that we didn't save any data about the costume being broken
|
||||
t.notOk(missingCostume.broken);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serializeCostume does not save data for missing costume', t => {
|
||||
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||
|
||||
t.equal(costumeDescs.length, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
59
scratch-vm/test/integration/stack-click.js
Normal file
59
scratch-vm/test/integration/stack-click.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/stack-click.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
/**
|
||||
* stack-click.sb2 contains a sprite at (0, 0) with a single stack
|
||||
* when timer > 100000000
|
||||
* move 100 steps
|
||||
* The intention is to make sure that the stack can be activated by a stack click
|
||||
* even when the hat predicate is false.
|
||||
*/
|
||||
test('stack click activates the stack', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', () => {
|
||||
// The sprite should have moved 100 to the right
|
||||
t.equal(vm.editingTarget.x, 100);
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
const blockContainer = vm.runtime.targets[1].blocks;
|
||||
const allBlocks = blockContainer._blocks;
|
||||
|
||||
// Confirm the editing target is initially at 0
|
||||
t.equal(vm.editingTarget.x, 0);
|
||||
|
||||
// Find hat for greater than and click it
|
||||
for (const blockId in allBlocks) {
|
||||
if (allBlocks[blockId].opcode === 'event_whengreaterthan') {
|
||||
blockContainer.blocklyListen({
|
||||
blockId: blockId,
|
||||
element: 'stackclick'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
198
scratch-vm/test/integration/tw-block-stop-thread.js
Normal file
198
scratch-vm/test/integration/tw-block-stop-thread.js
Normal file
@@ -0,0 +1,198 @@
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const Thread = require('../../src/engine/thread');
|
||||
const Clone = require('../../src/util/clone');
|
||||
|
||||
const fixturePath = path.join(__dirname, '../fixtures/tw-block-stop-thread.sb3');
|
||||
const fixtureData = fs.readFileSync(fixturePath);
|
||||
|
||||
const stopByRetireThread = thread => {
|
||||
thread.target.runtime.sequencer.retireThread(thread);
|
||||
};
|
||||
|
||||
const stopBySetStatus = thread => {
|
||||
thread.status = Thread.STATUS_DONE;
|
||||
};
|
||||
|
||||
test('constants', t => {
|
||||
t.equal(Thread.STATUS_DONE, 4);
|
||||
t.end();
|
||||
});
|
||||
|
||||
for (const [enableCompiler, stopFunction] of [
|
||||
[true, stopByRetireThread],
|
||||
[true, stopBySetStatus],
|
||||
[false, stopByRetireThread],
|
||||
[false, stopBySetStatus]
|
||||
]) {
|
||||
const subtestName = `${enableCompiler ? 'compiler' : 'interpreter'} - ${stopFunction.name}`;
|
||||
|
||||
test(`${subtestName} - stop in command block`, t => {
|
||||
const vm = new VM();
|
||||
vm.setCompilerOptions({enabled: enableCompiler});
|
||||
t.equal(vm.runtime.compilerOptions.enabled, enableCompiler);
|
||||
|
||||
const callOrder = [];
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'first block %s',
|
||||
arguments: ['number or text'],
|
||||
callback: (args, util) => {
|
||||
callOrder.push(['first block', Clone.simple(args)]);
|
||||
stopFunction(util.thread);
|
||||
}
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'inner block',
|
||||
return: 1,
|
||||
callback: args => {
|
||||
callOrder.push(['input block', Clone.simple(args)]);
|
||||
return callOrder.length;
|
||||
}
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'second block',
|
||||
callback: args => {
|
||||
callOrder.push(['second block', Clone.simple(args)]);
|
||||
}
|
||||
});
|
||||
|
||||
vm.loadProject(fixtureData).then(() => {
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
t.same(callOrder, [
|
||||
['input block', {}],
|
||||
['first block', {'number or text': 1}]
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test(`${subtestName} - stop in command block after yielding once`, t => {
|
||||
const vm = new VM();
|
||||
vm.setCompilerOptions({enabled: enableCompiler});
|
||||
t.equal(vm.runtime.compilerOptions.enabled, enableCompiler);
|
||||
|
||||
const callOrder = [];
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'first block %s',
|
||||
arguments: ['number or text'],
|
||||
callback: (args, util) => {
|
||||
callOrder.push(['first block', Clone.simple(args)]);
|
||||
if (callOrder.length === 1) {
|
||||
util.yield();
|
||||
} else {
|
||||
stopFunction(util.thread);
|
||||
}
|
||||
}
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'inner block',
|
||||
return: 1,
|
||||
// Ignore calls because interpreter will re-evaluate on yield but compiler won't
|
||||
callback: () => 5
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'second block',
|
||||
callback: args => {
|
||||
callOrder.push(['second block', Clone.simple(args)]);
|
||||
}
|
||||
});
|
||||
|
||||
vm.loadProject(fixtureData).then(() => {
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
t.same(callOrder, [
|
||||
['first block', {'number or text': 5}],
|
||||
['first block', {'number or text': 5}]
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test(`${subtestName} - stop in reporter block`, t => {
|
||||
const vm = new VM();
|
||||
vm.setCompilerOptions({enabled: enableCompiler});
|
||||
t.equal(vm.runtime.compilerOptions.enabled, enableCompiler);
|
||||
|
||||
const callOrder = [];
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'first block %s',
|
||||
arguments: ['number or text'],
|
||||
callback: args => {
|
||||
callOrder.push(['first block', Clone.simple(args)]);
|
||||
}
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'inner block',
|
||||
return: 1,
|
||||
callback: (args, util) => {
|
||||
callOrder.push(['input block', Clone.simple(args)]);
|
||||
stopFunction(util.thread);
|
||||
return callOrder.length;
|
||||
}
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'second block',
|
||||
callback: args => {
|
||||
callOrder.push(['second block', Clone.simple(args)]);
|
||||
}
|
||||
});
|
||||
|
||||
vm.loadProject(fixtureData).then(() => {
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
t.same(callOrder, [
|
||||
['input block', {}]
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test(`${subtestName} - stop in reporter block after yielding once`, t => {
|
||||
const vm = new VM();
|
||||
vm.setCompilerOptions({enabled: enableCompiler});
|
||||
t.equal(vm.runtime.compilerOptions.enabled, enableCompiler);
|
||||
|
||||
const callOrder = [];
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'first block %s',
|
||||
arguments: ['number or text'],
|
||||
callback: args => {
|
||||
callOrder.push(['first block', Clone.simple(args)]);
|
||||
}
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'inner block',
|
||||
return: 1,
|
||||
callback: (args, util) => {
|
||||
callOrder.push(['input block', Clone.simple(args)]);
|
||||
if (callOrder.length === 1) {
|
||||
util.yield();
|
||||
// TODO: interpreter bug, should not need to do this...
|
||||
util.thread.peekStackFrame().waitingReporter = true;
|
||||
} else {
|
||||
stopFunction(util.thread);
|
||||
}
|
||||
return callOrder.length;
|
||||
}
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'second block',
|
||||
callback: args => {
|
||||
callOrder.push(['second block', Clone.simple(args)]);
|
||||
}
|
||||
});
|
||||
|
||||
vm.loadProject(fixtureData).then(() => {
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
t.same(callOrder, [
|
||||
['input block', {}],
|
||||
['input block', {}]
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
23
scratch-vm/test/integration/tw-snapshots.js
Normal file
23
scratch-vm/test/integration/tw-snapshots.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const {test} = require('tap');
|
||||
const Snapshots = require('../snapshot/lib');
|
||||
|
||||
for (const testCase of Snapshots.tests) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
test(testCase.id, async t => {
|
||||
const expected = Snapshots.getExpectedSnapshot(testCase);
|
||||
const actual = await Snapshots.generateActualSnapshot(testCase);
|
||||
const result = Snapshots.compareSnapshots(expected, actual);
|
||||
if (result === 'VALID') {
|
||||
t.pass('matches');
|
||||
} else if (result === 'INPUT_MODIFIED') {
|
||||
t.fail('input project changed; run: node test/snapshot --update');
|
||||
} else if (result === 'MISSING_SNAPSHOT') {
|
||||
t.fail('snapshot is missing; run: node test/snapshot --update');
|
||||
} else {
|
||||
// This assertion will always fail, but tap will print out the snapshots
|
||||
// for easier comparison.
|
||||
t.equal(expected, actual, 'did not match; you may have to run: node test/snapshot --update');
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
}
|
||||
60
scratch-vm/test/integration/tw_add_builtin_extension.js
Normal file
60
scratch-vm/test/integration/tw_add_builtin_extension.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {test} = require('tap');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const Scratch = require('../../src/extension-support/tw-extension-api-common');
|
||||
|
||||
class TestBuiltinExtension {
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'testbuiltin',
|
||||
name: 'Test Builtin',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'test',
|
||||
blockType: Scratch.BlockType.REPORTER,
|
||||
text: 'test'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
test () {
|
||||
return 'It works! 123';
|
||||
}
|
||||
}
|
||||
|
||||
test('addBuiltingExtension', t => {
|
||||
const vm = new VM();
|
||||
|
||||
t.equal(vm.extensionManager.isBuiltinExtension('testbuiltin'), false, 'extension is not known');
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('testbuiltin'), false, 'extension is not loaded');
|
||||
|
||||
vm.extensionManager.addBuiltinExtension('testbuiltin', TestBuiltinExtension);
|
||||
t.equal(vm.extensionManager.isBuiltinExtension('testbuiltin'), true, 'extension is now known');
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('testbuiltin'), false, 'extension is still not loaded');
|
||||
|
||||
const fixture = fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'tw-add-builtin-extension.sb3'));
|
||||
vm.loadProject(fixture).then(() => {
|
||||
t.equal(vm.extensionManager.isExtensionLoaded('testbuiltin'), true, 'extension was loaded automatically');
|
||||
|
||||
vm.runtime.on('SAY', (target, type, text) => {
|
||||
t.equal(text, 'It works! 123', 'said value from extension');
|
||||
t.end();
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
});
|
||||
});
|
||||
|
||||
test('each runtime has own set of extensions', t => {
|
||||
const vm1 = new VM();
|
||||
const vm2 = new VM();
|
||||
|
||||
vm1.extensionManager.addBuiltinExtension('testbuiltin', TestBuiltinExtension);
|
||||
|
||||
t.ok(vm1.extensionManager.isBuiltinExtension('testbuiltin'));
|
||||
t.notOk(vm2.extensionManager.isBuiltinExtension('testbuiltin'));
|
||||
|
||||
t.end();
|
||||
});
|
||||
326
scratch-vm/test/integration/tw_addon_blocks.js
Normal file
326
scratch-vm/test/integration/tw_addon_blocks.js
Normal file
@@ -0,0 +1,326 @@
|
||||
const tap = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const Thread = require('../../src/engine/thread');
|
||||
|
||||
const fixtureData = fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'tw-addon-blocks.sb3'));
|
||||
|
||||
const runExecutionTests = compilerEnabled => test => {
|
||||
const load = async () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions({
|
||||
enabled: compilerEnabled
|
||||
});
|
||||
vm.on('COMPILE_ERROR', (target, error) => {
|
||||
test.fail(`Compile error in ${target.getName()}: ${error}`);
|
||||
});
|
||||
await vm.loadProject(fixtureData);
|
||||
return vm;
|
||||
};
|
||||
|
||||
const getVar = (vm, variableName) => {
|
||||
const variable = vm.runtime.getTargetForStage().lookupVariableByNameAndType(variableName, '');
|
||||
return variable.value;
|
||||
};
|
||||
|
||||
test.test('baseline - no addon blocks', t => {
|
||||
load().then(vm => {
|
||||
t.equal(getVar(vm, 'block 1'), 'initial');
|
||||
t.equal(getVar(vm, 'block 2'), 'initial');
|
||||
t.equal(getVar(vm, 'block 3'), 'initial');
|
||||
t.equal(getVar(vm, 'block 4'), 'initial');
|
||||
t.equal(getVar(vm, 'block 4 output'), 'initial');
|
||||
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
|
||||
t.equal(getVar(vm, 'block 1'), 'block 1 ran');
|
||||
t.equal(getVar(vm, 'block 2'), 'block 2: banana');
|
||||
t.equal(getVar(vm, 'block 3'), 'block 3 ran');
|
||||
t.equal(getVar(vm, 'block 4'), 'block 4 ran');
|
||||
t.equal(getVar(vm, 'block 4 output'), 'block 4: apple');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.test('simple statement blocks', t => {
|
||||
load().then(vm => {
|
||||
t.plan(9);
|
||||
|
||||
let calledBlock1 = false;
|
||||
let calledBlock2 = false;
|
||||
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'block 1',
|
||||
callback: (args, util) => {
|
||||
calledBlock1 = true;
|
||||
t.same(args, {});
|
||||
t.ok(util.thread instanceof Thread);
|
||||
// may have to update when project changes
|
||||
t.equal(util.thread.peekStack(), 'd');
|
||||
}
|
||||
});
|
||||
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'block 2 %s',
|
||||
arguments: ['number or text'],
|
||||
callback: (args, util) => {
|
||||
calledBlock2 = true;
|
||||
t.same(args, {
|
||||
'number or text': 'banana'
|
||||
});
|
||||
// may have to update when project changes
|
||||
t.equal(util.thread.peekStack(), 'c');
|
||||
}
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
|
||||
t.ok(calledBlock1);
|
||||
t.ok(calledBlock2);
|
||||
|
||||
// Overridden blocks should not run
|
||||
t.equal(getVar(vm, 'block 1'), 'false');
|
||||
t.equal(getVar(vm, 'block 2'), 'false');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.test('yield with thread.status = STATUS_PROMISE_WAIT', t => {
|
||||
load().then(vm => {
|
||||
t.plan(7);
|
||||
|
||||
let threadToResume;
|
||||
let ranBlock3 = false;
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'block 3',
|
||||
callback: (args, util) => {
|
||||
ranBlock3 = true;
|
||||
util.thread.status = Thread.STATUS_PROMISE_WAIT;
|
||||
threadToResume = util.thread;
|
||||
},
|
||||
arguments: []
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
|
||||
// Make sure we paused it
|
||||
t.ok(ranBlock3);
|
||||
t.equal(threadToResume.status, Thread.STATUS_PROMISE_WAIT);
|
||||
|
||||
// Should've stopped after block 2
|
||||
t.equal(getVar(vm, 'block 2'), 'block 2: banana');
|
||||
t.equal(getVar(vm, 'block 3'), 'false');
|
||||
t.equal(getVar(vm, 'block 4'), 'false');
|
||||
|
||||
threadToResume.status = Thread.STATUS_RUNNING;
|
||||
vm.runtime._step();
|
||||
|
||||
// Should've finished running
|
||||
t.equal(getVar(vm, 'block 3'), 'false'); // overridden, should not run
|
||||
t.equal(getVar(vm, 'block 4'), 'block 4 ran');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.test('yield with util.yield()', t => {
|
||||
load().then(vm => {
|
||||
t.plan(10);
|
||||
|
||||
let shouldYield = true;
|
||||
let calledBlock1 = 0;
|
||||
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'block 1',
|
||||
callback: (args, util) => {
|
||||
calledBlock1++;
|
||||
|
||||
if (shouldYield) {
|
||||
util.runtime.requestRedraw();
|
||||
util.yield();
|
||||
}
|
||||
},
|
||||
arguments: []
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
vm.runtime._step();
|
||||
}
|
||||
|
||||
t.equal(calledBlock1, 10);
|
||||
t.equal(getVar(vm, 'block 1'), 'false');
|
||||
t.equal(getVar(vm, 'block 2'), 'false');
|
||||
t.equal(getVar(vm, 'block 3'), 'false');
|
||||
t.equal(getVar(vm, 'block 4'), 'false');
|
||||
|
||||
shouldYield = false;
|
||||
vm.runtime._step();
|
||||
|
||||
t.equal(calledBlock1, 11);
|
||||
t.equal(getVar(vm, 'block 1'), 'false'); // overrridden, should not run
|
||||
t.equal(getVar(vm, 'block 2'), 'block 2: banana');
|
||||
t.equal(getVar(vm, 'block 3'), 'block 3 ran');
|
||||
t.equal(getVar(vm, 'block 4'), 'block 4 ran');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.test('yield with resolved Promise', t => {
|
||||
load().then(vm => {
|
||||
let resolveCallback;
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'block 2 %s',
|
||||
arguments: ['number or text'],
|
||||
callback: () => new Promise(resolve => {
|
||||
resolveCallback = resolve;
|
||||
})
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vm.runtime._step();
|
||||
}
|
||||
|
||||
t.type(resolveCallback, 'function');
|
||||
t.equal(getVar(vm, 'block 1'), 'block 1 ran');
|
||||
t.equal(getVar(vm, 'block 2'), 'false');
|
||||
t.equal(getVar(vm, 'block 3'), 'false');
|
||||
t.equal(getVar(vm, 'block 4'), 'false');
|
||||
|
||||
resolveCallback();
|
||||
Promise.resolve().then(() => {
|
||||
vm.runtime._step();
|
||||
t.equal(getVar(vm, 'block 2'), 'false'); // overridden, should not run
|
||||
t.equal(getVar(vm, 'block 3'), 'block 3 ran');
|
||||
t.equal(getVar(vm, 'block 4'), 'block 4 ran');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
// Doesn't work right now -- not clear whether it should or not
|
||||
test.skip('yield with rejected Promise', t => {
|
||||
load().then(vm => {
|
||||
let rejectCallback;
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'block 2 %s',
|
||||
arguments: ['number or text'],
|
||||
callback: () => new Promise((resolve, reject) => {
|
||||
rejectCallback = reject;
|
||||
})
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vm.runtime._step();
|
||||
}
|
||||
|
||||
t.type(rejectCallback, 'function');
|
||||
t.equal(getVar(vm, 'block 1'), 'block 1 ran');
|
||||
t.equal(getVar(vm, 'block 2'), 'false');
|
||||
t.equal(getVar(vm, 'block 3'), 'false');
|
||||
t.equal(getVar(vm, 'block 4'), 'false');
|
||||
|
||||
rejectCallback(new Error('Intentional error for testing'));
|
||||
Promise.resolve().then(() => {
|
||||
vm.runtime._step();
|
||||
t.equal(getVar(vm, 'block 2'), 'false'); // overridden, should not run
|
||||
t.equal(getVar(vm, 'block 3'), 'block 3 ran');
|
||||
t.equal(getVar(vm, 'block 4'), 'block 4 ran');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
test.test('returning values', t => {
|
||||
load().then(vm => {
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'block 4 %s',
|
||||
callback: args => {
|
||||
t.same(args, {
|
||||
'number or text': 'apple'
|
||||
});
|
||||
return `value from addon block: ${args['number or text']}`;
|
||||
},
|
||||
arguments: ['number or text']
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
|
||||
t.equal(getVar(vm, 'block 1'), 'block 1 ran');
|
||||
t.equal(getVar(vm, 'block 2'), 'block 2: banana');
|
||||
t.equal(getVar(vm, 'block 3'), 'block 3 ran');
|
||||
t.equal(getVar(vm, 'block 4'), 'false'); // block 4 itself should not have run, we overrode it
|
||||
t.equal(getVar(vm, 'block 4 output'), 'value from addon block: apple');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.end();
|
||||
};
|
||||
|
||||
tap.test('with compiler disabled', runExecutionTests(false));
|
||||
tap.test('with compiler enabled', runExecutionTests(true));
|
||||
|
||||
tap.test('block info', t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
const BLOCK_INFO_ID = 'a-b';
|
||||
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'hidden %s',
|
||||
arguments: ['number or text'],
|
||||
callback: () => {},
|
||||
hidden: true
|
||||
});
|
||||
|
||||
let blockInfo = vm.runtime._blockInfo.find(i => i.id === BLOCK_INFO_ID);
|
||||
t.equal(blockInfo, undefined);
|
||||
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'statement %s',
|
||||
arguments: ['number or text'],
|
||||
callback: () => {}
|
||||
});
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'input %s',
|
||||
arguments: ['an argument'],
|
||||
callback: () => {},
|
||||
return: 1
|
||||
});
|
||||
|
||||
blockInfo = vm.runtime._blockInfo.find(i => i.id === BLOCK_INFO_ID);
|
||||
t.type(blockInfo.id, 'string');
|
||||
t.type(blockInfo.name, 'string');
|
||||
t.type(blockInfo.color1, 'string');
|
||||
t.type(blockInfo.color2, 'string');
|
||||
t.type(blockInfo.color3, 'string');
|
||||
t.same(blockInfo.blocks, [
|
||||
{
|
||||
info: {},
|
||||
// eslint-disable-next-line max-len
|
||||
xml: '<block type="procedures_call" gap="16"><mutation generateshadows="true" warp="false" proccode="statement %s" argumentnames="["number or text"]" argumentids="["arg0"]" argumentdefaults="[""]"></mutation></block>'
|
||||
},
|
||||
{
|
||||
info: {},
|
||||
// eslint-disable-next-line max-len
|
||||
xml: '<block type="procedures_call" gap="16"><mutation generateshadows="true" warp="false" proccode="input %s" argumentnames="["an argument"]" argumentids="["arg0"]" argumentdefaults="[""]" return="1"></mutation></block>'
|
||||
}
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
33
scratch-vm/test/integration/tw_allow_drop_anywhere.js
Normal file
33
scratch-vm/test/integration/tw_allow_drop_anywhere.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const {test} = require('tap');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
test('allowDropAnywhere', t => {
|
||||
t.plan(2);
|
||||
|
||||
const rt = new Runtime();
|
||||
|
||||
rt.on('EXTENSION_ADDED', json => {
|
||||
t.equal(json.blocks[0].json.output, 'String');
|
||||
t.equal(json.blocks[1].json.output, null);
|
||||
t.end();
|
||||
});
|
||||
|
||||
rt._registerExtensionPrimitives({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'drop1',
|
||||
text: 'drop not anywhere',
|
||||
blockType: BlockType.REPORTER
|
||||
},
|
||||
{
|
||||
opcode: 'drop2',
|
||||
text: 'drop anywhere',
|
||||
blockType: BlockType.REPORTER,
|
||||
allowDropAnywhere: true
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
140
scratch-vm/test/integration/tw_asset_progress.js
Normal file
140
scratch-vm/test/integration/tw_asset_progress.js
Normal file
@@ -0,0 +1,140 @@
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const AssetUtil = require('../../src/util/tw-asset-util');
|
||||
|
||||
test('emitAssetProgress', t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
let runtimeOK = false;
|
||||
let vmOK = false;
|
||||
vm.runtime.on('ASSET_PROGRESS', (finished, total) => {
|
||||
t.equal(finished, 1, 'runtime finished');
|
||||
t.equal(total, 2, 'runtime total');
|
||||
runtimeOK = true;
|
||||
});
|
||||
vm.on('ASSET_PROGRESS', (finished, total) => {
|
||||
t.equal(finished, 1, 'vm finished');
|
||||
t.equal(total, 2, 'vm total');
|
||||
vmOK = true;
|
||||
});
|
||||
|
||||
vm.runtime.totalAssetRequests = 2;
|
||||
vm.runtime.finishedAssetRequests = 1;
|
||||
vm.runtime.emitAssetProgress();
|
||||
|
||||
t.ok(runtimeOK, 'runtime');
|
||||
t.ok(vmOK, 'vm');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('resetProgress', t => {
|
||||
t.plan(4);
|
||||
|
||||
const runtime = new Runtime();
|
||||
runtime.finishedAssetRequests = 10;
|
||||
runtime.totalAssetRequests = 10;
|
||||
|
||||
runtime.on('ASSET_PROGRESS', (finished, total) => {
|
||||
t.equal(finished, 0, 'event finished');
|
||||
t.equal(total, 0, 'event total');
|
||||
});
|
||||
|
||||
runtime.resetProgress();
|
||||
|
||||
t.equal(runtime.finishedAssetRequests, 0, 'property finishedAssetRequests');
|
||||
t.equal(runtime.totalAssetRequests, 0, 'property totalAssetRequests');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('dispose', t => {
|
||||
t.plan(1);
|
||||
const runtime = new Runtime();
|
||||
runtime.resetProgress = () => {
|
||||
t.pass();
|
||||
};
|
||||
runtime.dispose();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('wrapAssetRequest', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
const log = [];
|
||||
runtime.on('ASSET_PROGRESS', (finished, total) => {
|
||||
log.push([finished, total]);
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
runtime.wrapAssetRequest(() => Promise.resolve(1)),
|
||||
runtime.wrapAssetRequest(() => Promise.resolve(2))
|
||||
]).then(results => {
|
||||
t.same(results, [1, 2]);
|
||||
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
runtime.wrapAssetRequest(() => Promise.reject(3)).catch(error => {
|
||||
t.equal(error, 3);
|
||||
t.same(log, [
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
[1, 2],
|
||||
[2, 2],
|
||||
[2, 3],
|
||||
[3, 3]
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('asset util emits progress', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
const storage = makeTestStorage();
|
||||
storage.load = (assetType, assetId) => Promise.resolve({
|
||||
assetId
|
||||
});
|
||||
runtime.attachStorage(storage);
|
||||
|
||||
const log = [];
|
||||
runtime.on('ASSET_PROGRESS', (finished, total) => {
|
||||
log.push([finished, total]);
|
||||
});
|
||||
|
||||
AssetUtil.getByMd5ext(runtime, null, runtime.storage.AssetType.SVG, 'abcdef.svg').then(() => {
|
||||
t.same(log, [
|
||||
[0, 1],
|
||||
[1, 1]
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// For the next tests, we have some fixtures that contain 2 assets: 1 sound + 1 costume
|
||||
// We'll just load them and make sure that each deserializer emits reasonable progress events
|
||||
for (const format of ['sb', 'sb2', 'sb3']) {
|
||||
test(format, t => {
|
||||
const fixture = fs.readFileSync(path.join(__dirname, `../fixtures/tw-asset-progress.${format}`));
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
const log = [];
|
||||
vm.on('ASSET_PROGRESS', (finished, total) => {
|
||||
log.push([finished, total]);
|
||||
});
|
||||
|
||||
vm.loadProject(fixture)
|
||||
.then(() => {
|
||||
t.same(log, [
|
||||
[0, 0], // loadProject() implies dispose()
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
[1, 2],
|
||||
[2, 2]
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const Scratch = require('../../src/extension-support/tw-extension-api-common');
|
||||
|
||||
// based on https://github.com/TurboWarp/scratch-vm/issues/184
|
||||
|
||||
class TestExtension {
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'test',
|
||||
blockType: Scratch.BlockType.COMMAND,
|
||||
text: 'test block'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
test () {
|
||||
// returns a PromiseLike that calls handler immediately instead of in next microtask.
|
||||
const promise = {
|
||||
then (callbackFn) {
|
||||
callbackFn();
|
||||
return promise;
|
||||
}
|
||||
// intentionally omit catch() as that is not part of PromiseLike, so it should not be used
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = fs.readFileSync(path.join(__dirname, '../fixtures/tw-block-returning-promise-like.sb3'));
|
||||
|
||||
for (const compilerEnabled of [false, true]) {
|
||||
test(`handles blocks that return a promise-like object - ${compilerEnabled ? 'compiled' : 'interpreted'}`, t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager.addBuiltinExtension('testextension', TestExtension);
|
||||
|
||||
vm.setCompilerOptions({
|
||||
enabled: compilerEnabled
|
||||
});
|
||||
t.equal(vm.runtime.compilerOptions.enabled, compilerEnabled, 'sanity check');
|
||||
|
||||
vm.runtime.on('COMPILE_ERROR', () => {
|
||||
t.fail('Compile error');
|
||||
});
|
||||
|
||||
vm.loadProject(fixture).then(() => {
|
||||
let ended = 0;
|
||||
vm.runtime.on('SAY', (target, type, text) => {
|
||||
if (text === 'end') {
|
||||
ended++;
|
||||
} else {
|
||||
t.fail('said something unknown');
|
||||
}
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
|
||||
t.equal(ended, 1, 'script ran once immediately');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const Timer = require('../../src/util/timer');
|
||||
|
||||
test('compatibility stack frame is exposed on thread', t => {
|
||||
const vm = new VM();
|
||||
vm.loadProject(fs.readFileSync(path.join(__dirname, '../fixtures/tw-glide.sb3'))).then(() => {
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
t.ok(vm.runtime.threads[0].compatibilityStackFrame.timer instanceof Timer);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
224
scratch-vm/test/integration/tw_conditional_and_loop.js
Normal file
224
scratch-vm/test/integration/tw_conditional_and_loop.js
Normal file
@@ -0,0 +1,224 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const Scratch = require('../../src/extension-support/tw-extension-api-common');
|
||||
|
||||
// Based on https://github.com/TurboWarp/scratch-vm/pull/141
|
||||
class TestExtensionUsingReturn {
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'loopsAndThings',
|
||||
name: 'Loops and things test - return',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'conditional',
|
||||
blockType: Scratch.BlockType.CONDITIONAL,
|
||||
text: 'run branch [BRANCH] of',
|
||||
arguments: {
|
||||
BRANCH: {
|
||||
type: Scratch.ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
},
|
||||
branchCount: 3
|
||||
},
|
||||
{
|
||||
opcode: 'loop',
|
||||
blockType: Scratch.BlockType.LOOP,
|
||||
text: 'my repeat [TIMES]',
|
||||
arguments: {
|
||||
TIMES: {
|
||||
type: Scratch.ArgumentType.NUMBER,
|
||||
defaultValue: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
'---',
|
||||
{
|
||||
opcode: 'testPromise',
|
||||
blockType: Scratch.BlockType.REPORTER,
|
||||
text: 'return [VALUE] in a Promise',
|
||||
arguments: {
|
||||
VALUE: {
|
||||
type: Scratch.ArgumentType.STRING,
|
||||
defaultValue: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
conditional ({BRANCH}) {
|
||||
return Scratch.Cast.toNumber(BRANCH);
|
||||
}
|
||||
|
||||
loop ({TIMES}, util) {
|
||||
const times = Math.round(Scratch.Cast.toNumber(TIMES));
|
||||
if (typeof util.stackFrame.loopCounter === 'undefined') {
|
||||
util.stackFrame.loopCounter = times;
|
||||
}
|
||||
util.stackFrame.loopCounter--;
|
||||
if (util.stackFrame.loopCounter >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
testPromise ({VALUE}) {
|
||||
return Promise.resolve(VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
class TestExtensionUsingStartBranch {
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'loopsAndThings',
|
||||
name: 'Loops and things test - startBranch',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'conditional',
|
||||
blockType: Scratch.BlockType.CONDITIONAL,
|
||||
text: 'run branch [BRANCH] of',
|
||||
arguments: {
|
||||
BRANCH: {
|
||||
type: Scratch.ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
},
|
||||
branchCount: 3
|
||||
},
|
||||
{
|
||||
opcode: 'loop',
|
||||
blockType: Scratch.BlockType.LOOP,
|
||||
text: 'my repeat [TIMES]',
|
||||
arguments: {
|
||||
TIMES: {
|
||||
type: Scratch.ArgumentType.NUMBER,
|
||||
defaultValue: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
'---',
|
||||
{
|
||||
opcode: 'testPromise',
|
||||
blockType: Scratch.BlockType.REPORTER,
|
||||
text: 'return [VALUE] in a Promise',
|
||||
arguments: {
|
||||
VALUE: {
|
||||
type: Scratch.ArgumentType.STRING,
|
||||
defaultValue: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
conditional ({BRANCH}, util) {
|
||||
util.startBranch(Scratch.Cast.toNumber(BRANCH), false);
|
||||
}
|
||||
|
||||
loop ({TIMES}, util) {
|
||||
const times = Math.round(Scratch.Cast.toNumber(TIMES));
|
||||
if (typeof util.stackFrame.loopCounter === 'undefined') {
|
||||
util.stackFrame.loopCounter = times;
|
||||
}
|
||||
util.stackFrame.loopCounter--;
|
||||
if (util.stackFrame.loopCounter >= 0) {
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
}
|
||||
|
||||
testPromise ({VALUE}) {
|
||||
return Promise.resolve(VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
for (const Extension of [TestExtensionUsingReturn, TestExtensionUsingStartBranch]) {
|
||||
for (const compilerEnabled of [false, true]) {
|
||||
test(`CONDITIONAL - ${Extension.name} - ${compilerEnabled ? 'compiled' : 'interpreted'}`, t => {
|
||||
t.plan(1);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions({
|
||||
enabled: compilerEnabled
|
||||
});
|
||||
vm.extensionManager.addBuiltinExtension('loopsAndThings', Extension);
|
||||
vm.runtime.on('COMPILE_ERROR', () => {
|
||||
t.fail('Compile error');
|
||||
});
|
||||
|
||||
vm.loadProject(fs.readFileSync(path.join(__dirname, '../fixtures/tw-conditional.sb3'))).then(() => {
|
||||
let okayCount = 0;
|
||||
vm.runtime.on('SAY', (target, type, text) => {
|
||||
if (text === 'OK!') {
|
||||
okayCount++;
|
||||
} else if (text === 'end') {
|
||||
vm.quit();
|
||||
t.equal(okayCount, 5);
|
||||
t.end();
|
||||
} else {
|
||||
t.fail(`Unexpected text: ${text}`);
|
||||
}
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
vm.start();
|
||||
});
|
||||
});
|
||||
|
||||
test(`LOOP - ${Extension.name} - ${compilerEnabled ? 'compiled' : 'interpreted'}`, t => {
|
||||
t.plan(1);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions({
|
||||
enabled: compilerEnabled
|
||||
});
|
||||
vm.extensionManager.addBuiltinExtension('loopsAndThings', Extension);
|
||||
vm.runtime.on('COMPILE_ERROR', () => {
|
||||
t.fail('Compile error');
|
||||
});
|
||||
|
||||
vm.loadProject(fs.readFileSync(path.join(__dirname, '../fixtures/tw-loop.sb3'))).then(() => {
|
||||
vm.runtime.on('SAY', (target, type, text) => {
|
||||
vm.quit();
|
||||
t.equal(text, 'a 3 b 12 c 48 frames 64');
|
||||
t.end();
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
vm.start();
|
||||
});
|
||||
});
|
||||
|
||||
test(`beyond branchCount - ${Extension.name} - ${compilerEnabled ? 'compiled' : 'interpreted'}`, t => {
|
||||
t.plan(1);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions({
|
||||
enabled: compilerEnabled
|
||||
});
|
||||
vm.extensionManager.addBuiltinExtension('loopsAndThings', Extension);
|
||||
vm.runtime.on('COMPILE_ERROR', () => {
|
||||
t.fail('Compile error');
|
||||
});
|
||||
|
||||
vm.loadProject(fs.readFileSync(path.join(__dirname, '../fixtures/tw-beyond-branchCount.sb3'))).then(() => {
|
||||
vm.runtime.on('SAY', (target, type, text) => {
|
||||
if (text === 'BeyondBranchCount') {
|
||||
t.pass('BeyondBranchCount');
|
||||
} else if (text === 'end') {
|
||||
vm.quit();
|
||||
t.end();
|
||||
}
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
vm.start();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
104
scratch-vm/test/integration/tw_default_extension_url.js
Normal file
104
scratch-vm/test/integration/tw_default_extension_url.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
test('Loading project uses default extension URLs', t => {
|
||||
t.plan(1);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
const events = [];
|
||||
vm.securityManager.canLoadExtensionFromProject = url => {
|
||||
events.push(`canLoadExtensionFromProject ${url}`);
|
||||
return true;
|
||||
};
|
||||
vm.extensionManager.loadExtensionURL = url => {
|
||||
events.push(`loadExtensionURL ${url}`);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
vm.loadProject({
|
||||
targets: [
|
||||
{
|
||||
isStage: true,
|
||||
name: 'Stage',
|
||||
variables: {},
|
||||
lists: {},
|
||||
broadcasts: {},
|
||||
blocks: {
|
||||
a: {
|
||||
opcode: 'text_clearText',
|
||||
next: null,
|
||||
parent: null,
|
||||
inputs: {},
|
||||
fields: {},
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 203,
|
||||
y: 250
|
||||
},
|
||||
b: {
|
||||
opcode: 'pen_clear',
|
||||
next: null,
|
||||
parent: null,
|
||||
inputs: {},
|
||||
fields: {},
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 203,
|
||||
y: 250
|
||||
},
|
||||
c: {
|
||||
opcode: 'griffpatch_doTick',
|
||||
next: null,
|
||||
parent: null,
|
||||
inputs: {},
|
||||
fields: {},
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 203,
|
||||
y: 250
|
||||
}
|
||||
},
|
||||
comments: {},
|
||||
currentCostume: 0,
|
||||
costumes: [
|
||||
{
|
||||
assetId: 'cd21514d0531fdffb22204e0ec5ed84a',
|
||||
dataFormat: 'svg',
|
||||
md5ext: 'cd21514d0531fdffb22204e0ec5ed84a.svg',
|
||||
name: 'backdrop1',
|
||||
rotationCenterX: 240,
|
||||
rotationCenterY: 180
|
||||
}
|
||||
],
|
||||
sounds: [],
|
||||
volume: 100,
|
||||
layerOrder: 0,
|
||||
tempo: 60,
|
||||
videoTransparency: 50,
|
||||
videoState: 'on',
|
||||
textToSpeechLanguage: null
|
||||
}
|
||||
],
|
||||
monitors: [],
|
||||
extensions: [
|
||||
// this list intentionally wrong to make sure we don't rely on its contents
|
||||
],
|
||||
extensionURLs: {
|
||||
griffpatch: 'https://example.com/box2d.js'
|
||||
},
|
||||
meta: {
|
||||
semver: '3.0.0',
|
||||
vm: '0.2.0',
|
||||
agent: ''
|
||||
}
|
||||
}).then(() => {
|
||||
t.same(events, [
|
||||
'canLoadExtensionFromProject https://extensions.turbowarp.org/lab/text.js',
|
||||
'loadExtensionURL https://extensions.turbowarp.org/lab/text.js',
|
||||
'canLoadExtensionFromProject https://example.com/box2d.js',
|
||||
'loadExtensionURL https://example.com/box2d.js'
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
14
scratch-vm/test/integration/tw_deterministic_sb3.js
Normal file
14
scratch-vm/test/integration/tw_deterministic_sb3.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const {test} = require('tap');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
|
||||
test('saveProjectSb3 is deterministic over time', t => {
|
||||
const vm = new VM();
|
||||
Promise.all([
|
||||
vm.saveProjectSb3('nodebuffer'),
|
||||
// Zip modification time is only accurate to the second
|
||||
new Promise(resolve => setTimeout(resolve, 1000)).then(() => vm.saveProjectSb3('nodebuffer'))
|
||||
]).then(([a, b]) => {
|
||||
t.same(a, b);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const compilerAndInterpreter = (name, callback) => {
|
||||
test(`${name} - interpreted`, t => {
|
||||
callback(t, {
|
||||
enabled: false
|
||||
});
|
||||
});
|
||||
test(`${name} - compiled`, t => {
|
||||
callback(t, {
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
compilerAndInterpreter('edge activated hats returning promises work properly', async (t, co) => {
|
||||
const vm = new VM();
|
||||
vm.setCompilerOptions(co);
|
||||
|
||||
// Modify event_whengreaterthan to return a Promise (like a custom extension would) and allow us
|
||||
// to replace the value. This is a bit of a hack.
|
||||
let hatValue = false;
|
||||
vm.runtime._primitives.event_whengreaterthan = () => Promise.resolve(hatValue);
|
||||
|
||||
// Track how many times the script was executed.
|
||||
let sayCounter = 0;
|
||||
vm.runtime.on('SAY', () => {
|
||||
sayCounter++;
|
||||
});
|
||||
|
||||
const projectPath = path.join(__dirname, '..', 'fixtures', 'tw-edge-activated-hat-returns-promise.sb3');
|
||||
await vm.loadProject(fs.readFileSync(projectPath));
|
||||
|
||||
const step = async (count = 1) => {
|
||||
for (let i = 0; i < count; i++) {
|
||||
vm.runtime._step();
|
||||
// Give promises returned by blocks a chance to resolve.
|
||||
await Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
hatValue = false;
|
||||
await step(10);
|
||||
t.equal(sayCounter, 0);
|
||||
|
||||
hatValue = true;
|
||||
await step();
|
||||
// promise can't resolve in this tick, so block shouldn't run yet
|
||||
t.equal(sayCounter, 0);
|
||||
await step();
|
||||
t.equal(sayCounter, 1);
|
||||
await step(10);
|
||||
t.equal(sayCounter, 1);
|
||||
|
||||
hatValue = false;
|
||||
await step(10);
|
||||
t.equal(sayCounter, 1);
|
||||
|
||||
hatValue = true;
|
||||
await step();
|
||||
t.equal(sayCounter, 1);
|
||||
await step();
|
||||
t.equal(sayCounter, 2);
|
||||
await step(10);
|
||||
t.equal(sayCounter, 2);
|
||||
|
||||
t.end();
|
||||
});
|
||||
332
scratch-vm/test/integration/tw_extension_and_block_xml.js
Normal file
332
scratch-vm/test/integration/tw_extension_and_block_xml.js
Normal file
@@ -0,0 +1,332 @@
|
||||
const fs = require('fs');
|
||||
const pathUtil = require('path');
|
||||
const htmlparser = require('htmlparser2');
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const ArgumentType = require('../../src/extension-support/argument-type');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
const baseExtensionInfo = {
|
||||
id: 'xmltest',
|
||||
name: `<>"'&& Name`,
|
||||
docsURI: `https://example.com/&''""<<>>`,
|
||||
menuIconURI: `data:<>&"' category icon`,
|
||||
blocks: [
|
||||
{
|
||||
blockType: `block type <>&"'`,
|
||||
opcode: `opcode <>&"'`,
|
||||
text: `<>&"' [string argument <>&"'] [inputMenu <"'&>] [fieldMenu <"'&>] [image <"'&>]`,
|
||||
blockIconURI: `'data:<>&"' block icon`,
|
||||
arguments: {
|
||||
[`string argument <>&"'`]: {
|
||||
type: ArgumentType.STRING,
|
||||
defaultValue: `default string <>&"'`
|
||||
},
|
||||
[`inputMenu <"'&>`]: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: `input <>&"'`,
|
||||
defaultValue: `default input <>&"'`
|
||||
},
|
||||
[`fieldMenu <"'&>`]: {
|
||||
type: `argument type <>&"'`,
|
||||
menu: `field <>&"'`,
|
||||
defaultValue: `default field <>&"'`
|
||||
},
|
||||
[`image <"'&>`]: {
|
||||
type: ArgumentType.IMAGE,
|
||||
dataURI: `data:<>&"' image input`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'button',
|
||||
blockType: BlockType.BUTTON,
|
||||
text: `'"><& button text`,
|
||||
func: `'"><& func`
|
||||
}
|
||||
],
|
||||
menus: {
|
||||
[`input <>&"'`]: {
|
||||
acceptReporters: true,
|
||||
items: [
|
||||
`1 <>&"`,
|
||||
`2 <>&"`,
|
||||
`3 <>&"`
|
||||
]
|
||||
},
|
||||
[`field <>&"'`]: {
|
||||
acceptReporters: false,
|
||||
items: [
|
||||
`1 <>&"`,
|
||||
`2 <>&"`,
|
||||
`3 <>&"`
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test('XML escaped in Runtime.getBlocksXML()', t => {
|
||||
// While these changes will make the extension unusable in a real editor environment, we still
|
||||
// want to make sure that these fields are actually being escaped.
|
||||
const mangledExtension = JSON.parse(JSON.stringify(baseExtensionInfo));
|
||||
mangledExtension.color1 = `<"'&amp;color1>`;
|
||||
mangledExtension.color2 = `<"'&amp;color2>`;
|
||||
mangledExtension.color3 = `<"'&amp;color3>`;
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => mangledExtension
|
||||
});
|
||||
|
||||
const xmlList = vm.runtime.getBlocksXML();
|
||||
t.type(xmlList, Array, 'getBlocksXML returns array');
|
||||
t.equal(xmlList.length, 1, 'array has 1 item');
|
||||
|
||||
const xmlEntry = xmlList[0];
|
||||
t.equal(xmlEntry.id, `xmltest`, 'id worked');
|
||||
|
||||
const parsedXml = htmlparser.parseDOM(xmlEntry.xml);
|
||||
t.equal(parsedXml.length, 1, 'xml has 1 root node');
|
||||
|
||||
/*
|
||||
Expected XML structure:
|
||||
|
||||
<category name="..." colour="..." secondaryColour="..." iconURI="...">
|
||||
<button ... web-class="..."></button>
|
||||
<block type="...">
|
||||
<value name="...">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">default value</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="...">
|
||||
<shadow type="...">
|
||||
<field name="...">default value</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<field name="...">default value</field>
|
||||
</block>
|
||||
<button text="..." callbackKey="..."></button>
|
||||
</category>
|
||||
*/
|
||||
|
||||
const category = parsedXml[0];
|
||||
t.equal(category.name, 'category', 'has <category>');
|
||||
t.equal(category.attribs.name, '<>"'&& Name', 'escaped category name');
|
||||
t.equal(category.attribs.id, 'xmltest', 'category id');
|
||||
t.equal(category.attribs.colour, '<"'&amp;amp;color1>', 'escaped category color');
|
||||
t.equal(category.attribs.secondarycolour, '<"'&amp;amp;color2>', 'escaped category color 2');
|
||||
t.equal(category.attribs.iconuri, 'data:<>&"' category icon', 'escaped category icon');
|
||||
t.equal(category.children.length, 3, 'category has 3 children');
|
||||
|
||||
// Check docsURI
|
||||
const docsButton = category.children[0];
|
||||
t.equal(docsButton.name, 'button', 'has docs <button>');
|
||||
t.equal(docsButton.attribs.callbackkey, 'OPEN_EXTENSION_DOCS');
|
||||
t.equal(
|
||||
docsButton.attribs.callbackdata,
|
||||
'https://example.com/&''""<<>>',
|
||||
'escaped docs callback data'
|
||||
);
|
||||
t.equal(docsButton.children.length, 0, 'docs button has 0 children');
|
||||
|
||||
// Check the block
|
||||
const block = category.children[1];
|
||||
t.equal(block.name, 'block', 'has <block>');
|
||||
t.equal(
|
||||
block.attribs.type,
|
||||
'xmltest_opcode <>&"'',
|
||||
'escaped block id'
|
||||
);
|
||||
t.equal(block.children.length, 3, 'block has 3 children');
|
||||
|
||||
// Check the block's string input
|
||||
const stringInput = block.children[0];
|
||||
t.equal(stringInput.name, 'value', 'string input is <value>');
|
||||
t.equal(stringInput.attribs.name, 'string argument <>&"'', 'escaped string input id');
|
||||
t.equal(stringInput.children.length, 1, 'string input has 1 child');
|
||||
|
||||
const stringInputShadow = stringInput.children[0];
|
||||
t.equal(stringInputShadow.name, 'shadow', 'string input shadow is <shadow>');
|
||||
t.equal(stringInputShadow.attribs.type, 'text', 'string input shadow is of type text');
|
||||
t.equal(stringInputShadow.children.length, 1, 'string input shadow has 1 child');
|
||||
|
||||
const stringInputField = stringInputShadow.children[0];
|
||||
t.equal(stringInputField.name, 'field', 'string input field is <field>');
|
||||
t.equal(stringInputField.children.length, 1, 'field input has 1 child');
|
||||
|
||||
const stringInputFieldContent = stringInputField.children[0];
|
||||
t.equal(
|
||||
stringInputFieldContent.data,
|
||||
'default string <>&"'',
|
||||
'escaped string input default value'
|
||||
);
|
||||
|
||||
// Check the block's menu input
|
||||
const menuInput = block.children[1];
|
||||
t.equal(menuInput.name, 'value', 'menu input is <value>');
|
||||
t.equal(menuInput.attribs.name, 'inputMenu <"'&>', 'escaped menu input id');
|
||||
t.equal(menuInput.children.length, 1, 'menu input has 1 child');
|
||||
|
||||
const inputShadow = menuInput.children[0];
|
||||
t.equal(inputShadow.name, 'shadow', 'input shadow is <shadow>');
|
||||
t.equal(
|
||||
inputShadow.attribs.type,
|
||||
'xmltest_menu_input <>&"'',
|
||||
'escaped menu id'
|
||||
);
|
||||
t.equal(inputShadow.children.length, 1, 'input shadow has 1 child');
|
||||
|
||||
const inputField = inputShadow.children[0];
|
||||
t.equal(inputField.name, 'field', 'input field is <field>');
|
||||
t.equal(inputField.children.length, 1, 'input field has 1 child');
|
||||
|
||||
const inputFieldContent = inputField.children[0];
|
||||
t.equal(inputFieldContent.data, 'default input <>&"'', 'escaped input default value');
|
||||
|
||||
// Check the block's menu field
|
||||
const menuField = block.children[2];
|
||||
t.equal(menuField.name, 'field', 'menu field is <field>');
|
||||
t.equal(menuField.attribs.name, 'fieldMenu <"'&>', 'escaped field menu id');
|
||||
t.equal(menuField.children.length, 1, 'menu field has 1 child');
|
||||
|
||||
const menuFieldContent = menuField.children[0];
|
||||
t.equal(menuFieldContent.data, 'default field <>&"'', 'escaped field default value');
|
||||
|
||||
// Check the button block
|
||||
const button = category.children[2];
|
||||
t.equal(button.name, 'button', 'button is <button>');
|
||||
t.equal(button.attribs.text, ''"><& button text', 'escaped button text');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('ID escaped in Runtime.getBlocksXML()', t => {
|
||||
// Previous test needs to use an actually valid extension ID. For this test we will
|
||||
// register an invalid extension just to make sure that the ID ends up being escaped.
|
||||
|
||||
const rt = new Runtime();
|
||||
rt._registerExtensionPrimitives({
|
||||
id: `id <>&"'`,
|
||||
name: 'name',
|
||||
blocks: []
|
||||
});
|
||||
|
||||
const xmlList = rt.getBlocksXML();
|
||||
const xmlEntry = xmlList[0];
|
||||
t.equal(xmlEntry.id, `id <>&"'`, 'extension id outside of xml unchanged');
|
||||
|
||||
const parsedXML = htmlparser.parseDOM(xmlEntry.xml);
|
||||
t.equal(parsedXML.length, 1, 'XML has 1 root node');
|
||||
|
||||
const category = parsedXML[0];
|
||||
t.equal(category.name, 'category', 'category is <category>');
|
||||
t.equal(category.attribs.id, 'id <>&"'', 'escaped extension id');
|
||||
t.equal(category.children.length, 0, 'category has no children');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('XML escaped in Blocks.toXML()', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
const serviceName = vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => baseExtensionInfo
|
||||
});
|
||||
vm.extensionManager._loadedExtensions.set(baseExtensionInfo.id, serviceName);
|
||||
|
||||
const fixturePath = pathUtil.join(__dirname, '..', 'fixtures', 'tw-project-using-xml-extension.sb3');
|
||||
|
||||
const checkVM = () => {
|
||||
const generatedXML = vm.runtime.targets[0].blocks.toXML();
|
||||
const parsedXML = htmlparser.parseDOM(generatedXML);
|
||||
|
||||
/*
|
||||
Example expected XML:
|
||||
|
||||
<block id="..." type="xmltest_opcode" x="..." y="...">
|
||||
<value name="string argument">
|
||||
<shadow id="..." type="text">
|
||||
<field name="TEXT">default string</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="inputMenu">
|
||||
<shadow id="..." type="xmltest_menu_input">
|
||||
<field name="input">default input</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<field name="fieldMenu">default field</field>
|
||||
</block>
|
||||
*/
|
||||
|
||||
t.equal(parsedXML.length, 1, 'XML has 1 root');
|
||||
|
||||
// Check the block itself
|
||||
const block = parsedXML[0];
|
||||
t.equal(block.name, 'block', 'block is <block>');
|
||||
t.equal(
|
||||
block.attribs.type,
|
||||
'xmltest_opcode <>&"'',
|
||||
'escaped block opcode'
|
||||
);
|
||||
t.equal(block.children.length, 3, 'block has 3 children');
|
||||
|
||||
// Check the string input
|
||||
const stringInputValue = block.children[0];
|
||||
t.equal(stringInputValue.name, 'value', 'string input is <value>');
|
||||
t.equal(
|
||||
stringInputValue.attribs.name,
|
||||
'string argument <>&"'',
|
||||
'escaped string input name'
|
||||
);
|
||||
t.equal(stringInputValue.children.length, 1, 'string input has 1 child');
|
||||
|
||||
const stringInputShadow = stringInputValue.children[0];
|
||||
t.equal(stringInputShadow.name, 'shadow', 'string input shadow is <shadow>');
|
||||
t.equal(stringInputValue.children.length, 1, 'string input shadow has 1 child');
|
||||
|
||||
const stringInputField = stringInputShadow.children[0];
|
||||
t.equal(stringInputField.name, 'field', 'string input field is <field>');
|
||||
t.equal(stringInputField.children.length, 1, 'string input field has 1 child');
|
||||
|
||||
const stringInputFieldContent = stringInputField.children[0];
|
||||
t.equal(stringInputFieldContent.data, `default string <>&"'`, 'escaped string input value');
|
||||
|
||||
// Check the input menu
|
||||
const inputMenuValue = block.children[1];
|
||||
t.equal(inputMenuValue.name, 'value', 'input menu is <value>');
|
||||
t.equal(inputMenuValue.attribs.name, 'inputMenu <"'&>', 'escaped input menu name');
|
||||
t.equal(inputMenuValue.children.length, 1, 'input menu has 1 child');
|
||||
|
||||
const inputMenuShadow = inputMenuValue.children[0];
|
||||
t.equal(inputMenuShadow.name, 'shadow', 'input menu shadow is <shadow>');
|
||||
t.equal(inputMenuValue.children.length, 1, 'input menu shadow has 1 child');
|
||||
|
||||
const inputMenuField = inputMenuShadow.children[0];
|
||||
t.equal(inputMenuField.name, 'field', 'input menu field is <field>');
|
||||
t.equal(inputMenuField.children.length, 1, 'input menu field has 1 child');
|
||||
|
||||
const inputMenuFieldContent = inputMenuField.children[0];
|
||||
t.equal(inputMenuFieldContent.data, `default input <>&"'`, 'escaped input menu value');
|
||||
|
||||
// Check the field menu
|
||||
const fieldMenu = block.children[2];
|
||||
t.equal(fieldMenu.name, 'field', 'field menu is <field>');
|
||||
t.equal(fieldMenu.attribs.name, 'fieldMenu <"'&>', 'escaped field menu name');
|
||||
t.equal(fieldMenu.children.length, 1, 'field menu has 1 child');
|
||||
|
||||
const fieldMenuContent = fieldMenu.children[0];
|
||||
t.equal(fieldMenuContent.data, `default field <>&"'`, 'escaped input menu value');
|
||||
};
|
||||
|
||||
// Check that we can deserialize a project using this extension.
|
||||
await vm.loadProject(fs.readFileSync(fixturePath));
|
||||
checkVM();
|
||||
|
||||
// Check that it still works after serialization and deserialization.
|
||||
const serialized = await vm.saveProjectSb3('uint8array');
|
||||
await vm.loadProject(serialized);
|
||||
checkVM();
|
||||
|
||||
t.end();
|
||||
});
|
||||
83
scratch-vm/test/integration/tw_extension_buttons.js
Normal file
83
scratch-vm/test/integration/tw_extension_buttons.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const {test} = require('tap');
|
||||
const htmlparser = require('htmlparser2');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
test('buttons', t => {
|
||||
const vm = new VM();
|
||||
let buttonRunCount = 0;
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
'getInfo': () => ({
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.BUTTON,
|
||||
text: 'button text <>',
|
||||
func: 'callback &"\'<>'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.BUTTON,
|
||||
text: 'make variable <>',
|
||||
func: 'MAKE_A_VARIABLE'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.BUTTON,
|
||||
text: 'make procedure ""',
|
||||
func: 'MAKE_A_PROCEDURE'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.BUTTON,
|
||||
text: 'make list &&',
|
||||
func: 'MAKE_A_LIST'
|
||||
}
|
||||
]
|
||||
}),
|
||||
'callback &"\'<>': () => {
|
||||
buttonRunCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const xml = vm.runtime.getBlocksXML();
|
||||
t.equal(xml.length, 1);
|
||||
|
||||
const parsed = htmlparser.parseDOM(xml[0].xml, {
|
||||
decodeEntities: true
|
||||
});
|
||||
t.equal(parsed.length, 1);
|
||||
|
||||
const category = parsed[0];
|
||||
t.equal(category.children.length, 4);
|
||||
|
||||
const customButton = category.children[0];
|
||||
t.equal(customButton.name, 'button');
|
||||
t.equal(customButton.attribs.text, 'button text <>');
|
||||
t.equal(customButton.attribs.callbackkey, 'EXTENSION_CALLBACK');
|
||||
t.equal(customButton.attribs.callbackdata, 'test_callback &"\'<>');
|
||||
|
||||
const makeVariable = category.children[1];
|
||||
t.equal(makeVariable.attribs.text, 'make variable <>');
|
||||
t.equal(makeVariable.attribs.callbackkey, 'MAKE_A_VARIABLE');
|
||||
|
||||
const makeProcedure = category.children[2];
|
||||
t.equal(makeProcedure.attribs.text, 'make procedure ""');
|
||||
t.equal(makeProcedure.attribs.callbackkey, 'MAKE_A_PROCEDURE');
|
||||
|
||||
const makeList = category.children[3];
|
||||
t.equal(makeList.attribs.text, 'make list &&');
|
||||
t.equal(makeList.attribs.callbackkey, 'MAKE_A_LIST');
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const builtinButton = category.children[i];
|
||||
t.equal(builtinButton.name, 'button');
|
||||
t.equal(builtinButton.attribs.callbackdata, undefined);
|
||||
}
|
||||
|
||||
t.equal(buttonRunCount, 0);
|
||||
vm.handleExtensionButtonPress(customButton.attribs.callbackdata);
|
||||
t.equal(buttonRunCount, 1);
|
||||
vm.handleExtensionButtonPress(customButton.attribs.callbackdata);
|
||||
t.equal(buttonRunCount, 2);
|
||||
|
||||
t.end();
|
||||
});
|
||||
178
scratch-vm/test/integration/tw_extension_storage.js
Normal file
178
scratch-vm/test/integration/tw_extension_storage.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const sb3 = require('../../src/serialization/sb3');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('serialize data', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const rt = vm.runtime;
|
||||
|
||||
const target1 = new RenderedTarget(new Sprite(null, rt), rt);
|
||||
const target2 = new RenderedTarget(new Sprite(null, rt), rt);
|
||||
rt.addTarget(target1);
|
||||
rt.addTarget(target2);
|
||||
|
||||
t.same(sb3.serialize(rt).extensionStorage, undefined, 'global - nothing when no extensions');
|
||||
t.same(
|
||||
sb3.serialize(rt).targets.map(i => i.extensionStorage),
|
||||
[undefined, undefined],
|
||||
'sprites - nothing when no extensions'
|
||||
);
|
||||
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'test1',
|
||||
blocks: []
|
||||
})
|
||||
});
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'test2',
|
||||
blocks: []
|
||||
})
|
||||
});
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'test3',
|
||||
blocks: []
|
||||
})
|
||||
});
|
||||
|
||||
t.same(sb3.serialize(rt).extensionStorage, undefined, 'global - nothing when no storage');
|
||||
t.same(
|
||||
sb3.serialize(rt).targets.map(i => i.extensionStorage),
|
||||
[undefined, undefined],
|
||||
'sprites - nothing when no storage'
|
||||
);
|
||||
|
||||
const topLevelBlockBase = {
|
||||
// this is not interesting for this test
|
||||
inputs: {},
|
||||
fields: {},
|
||||
topLevel: true,
|
||||
next: null,
|
||||
parent: null
|
||||
};
|
||||
|
||||
target1.blocks.createBlock({
|
||||
...topLevelBlockBase,
|
||||
id: 'block1',
|
||||
opcode: 'test1_whatever'
|
||||
});
|
||||
target2.blocks.createBlock({
|
||||
...topLevelBlockBase,
|
||||
id: 'block2',
|
||||
opcode: 'test2_whatever'
|
||||
});
|
||||
|
||||
target1.extensionStorage.test1 = 1234321;
|
||||
t.same(sb3.serialize(rt, target1.id).extensionStorage, {
|
||||
test1: 1234321
|
||||
}, 'target1 alone has test1');
|
||||
t.same(sb3.serialize(rt, target2.id).extensionStorage, undefined, 'target2 alone does not have test1');
|
||||
|
||||
target1.extensionStorage.test1 = null;
|
||||
t.same(sb3.serialize(rt, target1.id).extensionStorage, undefined, 'null is not serialized');
|
||||
|
||||
target1.extensionStorage.test1 = undefined;
|
||||
t.same(sb3.serialize(rt, target1.id).extensionStorage, undefined, 'undefined is not serialized');
|
||||
|
||||
target1.extensionStorage.test1 = {it: 'works'};
|
||||
target1.extensionStorage.test2 = true;
|
||||
target1.extensionStorage.test3 = {should_not: 'be_saved'};
|
||||
|
||||
target2.extensionStorage.test1 = ['ok'];
|
||||
delete target2.extensionStorage.test2;
|
||||
delete target2.extensionStorage.test3;
|
||||
|
||||
rt.extensionStorage.test1 = 'global ok';
|
||||
delete rt.extensionStorage.test2;
|
||||
rt.extensionStorage.test3 = ['dont save this'];
|
||||
|
||||
const json = sb3.serialize(rt);
|
||||
t.same(json.extensionStorage, {
|
||||
test1: 'global ok'
|
||||
}, 'final - global has test1');
|
||||
t.same(json.targets.map(i => i.extensionStorage), [
|
||||
{
|
||||
test1: {
|
||||
it: 'works'
|
||||
},
|
||||
test2: true
|
||||
},
|
||||
{
|
||||
test1: ['ok']
|
||||
}
|
||||
], 'final - targets ok');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deserialize project with data', t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'test1',
|
||||
blocks: []
|
||||
})
|
||||
});
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'test2',
|
||||
blocks: []
|
||||
})
|
||||
});
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'test3',
|
||||
blocks: []
|
||||
})
|
||||
});
|
||||
|
||||
// trick it into thinking the extensions are real and loaded...
|
||||
vm.extensionManager._loadedExtensions.set('test1', 'invalid');
|
||||
vm.extensionManager._loadedExtensions.set('test2', 'invalid');
|
||||
vm.extensionManager._loadedExtensions.set('test3', 'invalid');
|
||||
|
||||
const fixture = fs.readFileSync(path.resolve(__dirname, '../fixtures/tw-extension-storage.sb3'));
|
||||
vm.loadProject(fixture).then(() => {
|
||||
t.same(vm.runtime.extensionStorage, {
|
||||
test1: 'global ok'
|
||||
}, 'deserialized global');
|
||||
t.same(vm.runtime.targets[0].extensionStorage, {
|
||||
test1: {
|
||||
it: 'works'
|
||||
},
|
||||
test2: true
|
||||
}, 'deserialized target 0');
|
||||
t.same(vm.runtime.targets[1].extensionStorage, {
|
||||
test1: ['ok']
|
||||
}, 'deserialized target 1');
|
||||
t.same(vm.runtime.targets[2].extensionStorage, {}, 'deserialized target 2');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserialize project with no data', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const fixture = fs.readFileSync(path.resolve(__dirname, '../fixtures/tw-extension-storage-no-data.sb3'));
|
||||
vm.loadProject(fixture).then(() => {
|
||||
t.same(vm.runtime.extensionStorage, {}, 'deserialized global');
|
||||
t.same(vm.runtime.targets[0].extensionStorage, {}, 'deserialized target 0');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('dispose resets storage', t => {
|
||||
const runtime = new Runtime();
|
||||
runtime.extensionStorage.something = 3;
|
||||
runtime.dispose();
|
||||
t.same(runtime.extensionStorage, {});
|
||||
t.end();
|
||||
});
|
||||
589
scratch-vm/test/integration/tw_font_manager.js
Normal file
589
scratch-vm/test/integration/tw_font_manager.js
Normal file
@@ -0,0 +1,589 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {test} = require('tap');
|
||||
const _makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
const emptyProjectFixture = path.join(__dirname, '..', 'fixtures', 'tw-empty-project.sb3');
|
||||
|
||||
const makeTestStorage = () => {
|
||||
const storage = _makeTestStorage();
|
||||
storage.DataFormat.TTF = 'ttf';
|
||||
storage.AssetType.Font = {
|
||||
contentType: 'font/ttf',
|
||||
name: 'Font',
|
||||
runtimeFormat: storage.DataFormat.TTF,
|
||||
immutable: true
|
||||
};
|
||||
return storage;
|
||||
};
|
||||
|
||||
test('isValidFamily', t => {
|
||||
const {fontManager} = new Runtime();
|
||||
t.ok(fontManager.isValidFamily('Roboto'));
|
||||
t.ok(fontManager.isValidFamily('sans-serif'));
|
||||
t.ok(fontManager.isValidFamily('helvetica neue'));
|
||||
t.notOk(fontManager.isValidFamily('Roboto;Bold'));
|
||||
t.notOk(fontManager.isValidFamily('Arial, sans-serif'));
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getSafeName', t => {
|
||||
const {fontManager} = new Runtime();
|
||||
t.equal(fontManager.getSafeName('Arial'), 'Arial');
|
||||
fontManager.addSystemFont('Arial', 'sans-serif');
|
||||
t.equal(fontManager.getSafeName('Arial'), 'Arial2');
|
||||
t.equal(fontManager.getSafeName('Weird123!@"<>?'), 'Weird123');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('system font', t => {
|
||||
const mockRenderer = {
|
||||
setLayerGroupOrdering: () => {},
|
||||
setCustomFonts: () => {
|
||||
t.fail('Should not call renderer.setCustomFonts()');
|
||||
}
|
||||
};
|
||||
|
||||
const rt = new Runtime();
|
||||
rt.attachRenderer(mockRenderer);
|
||||
const {fontManager} = rt;
|
||||
|
||||
let changed = false;
|
||||
fontManager.on('change', () => {
|
||||
changed = true;
|
||||
});
|
||||
|
||||
fontManager.addSystemFont('Noto Sans Mono', 'monospace');
|
||||
t.ok(changed, 'addSystemFont() emits change');
|
||||
t.ok(fontManager.hasFont('Noto Sans Mono'), 'updated hasFont()');
|
||||
t.same(fontManager.getFonts(), [
|
||||
{
|
||||
system: true,
|
||||
name: 'Noto Sans Mono',
|
||||
family: '"Noto Sans Mono", monospace',
|
||||
data: null,
|
||||
format: null
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeJSON(), [
|
||||
{
|
||||
system: true,
|
||||
family: 'Noto Sans Mono',
|
||||
fallback: 'monospace'
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeAssets(), []);
|
||||
|
||||
changed = false;
|
||||
fontManager.addSystemFont('Lobster', 'fantasy, sans-serif');
|
||||
t.ok(changed, 'addSystemFont() emits change');
|
||||
t.ok(fontManager.hasFont('Lobster'), 'updated hasFont()');
|
||||
t.same(fontManager.getFonts(), [
|
||||
{
|
||||
system: true,
|
||||
name: 'Noto Sans Mono',
|
||||
family: '"Noto Sans Mono", monospace',
|
||||
data: null,
|
||||
format: null
|
||||
},
|
||||
{
|
||||
system: true,
|
||||
name: 'Lobster',
|
||||
family: '"Lobster", fantasy, sans-serif',
|
||||
data: null,
|
||||
format: null
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeJSON(), [
|
||||
{
|
||||
system: true,
|
||||
family: 'Noto Sans Mono',
|
||||
fallback: 'monospace'
|
||||
},
|
||||
{
|
||||
system: true,
|
||||
family: 'Lobster',
|
||||
fallback: 'fantasy, sans-serif'
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeAssets(), []);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('system font validation', t => {
|
||||
const {fontManager} = new Runtime();
|
||||
t.throws(() => {
|
||||
fontManager.addCustomFont(';', 'monospace');
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('clear', t => {
|
||||
const setCustomFontsCalls = [];
|
||||
const mockRenderer = {
|
||||
setLayerGroupOrdering: () => {},
|
||||
setCustomFonts: fonts => {
|
||||
setCustomFontsCalls.push(fonts);
|
||||
}
|
||||
};
|
||||
|
||||
const rt = new Runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
rt.attachRenderer(mockRenderer);
|
||||
const {fontManager, storage} = rt;
|
||||
|
||||
fontManager.addSystemFont('Arial', 'sans-serif');
|
||||
t.equal(fontManager.getFonts().length, 1);
|
||||
|
||||
let changed = false;
|
||||
fontManager.on('change', () => {
|
||||
changed = true;
|
||||
});
|
||||
fontManager.clear();
|
||||
t.ok(changed, 'clear() emits change');
|
||||
t.equal(fontManager.getFonts().length, 0, 'removed font');
|
||||
t.same(setCustomFontsCalls, [], 'clear() does not call setCustomFonts() if only system fonts');
|
||||
|
||||
fontManager.addCustomFont('Wingdings', 'monospace', storage.createAsset(
|
||||
storage.AssetType.Font,
|
||||
'ttf',
|
||||
new Uint8Array([11, 12, 13]),
|
||||
null,
|
||||
true
|
||||
));
|
||||
changed = false;
|
||||
setCustomFontsCalls.length = 0;
|
||||
fontManager.clear();
|
||||
t.ok(changed, 'clear() emits change');
|
||||
t.same(setCustomFontsCalls, [{}], 'clear() clears setCustomFonts()');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('custom fonts', t => {
|
||||
const setCustomFontsCalls = [];
|
||||
const mockRenderer = {
|
||||
setLayerGroupOrdering: () => {},
|
||||
setCustomFonts: customFonts => {
|
||||
setCustomFontsCalls.push(customFonts);
|
||||
}
|
||||
};
|
||||
|
||||
const rt = new Runtime();
|
||||
rt.attachRenderer(mockRenderer);
|
||||
rt.attachStorage(makeTestStorage());
|
||||
const {fontManager, storage} = rt;
|
||||
|
||||
let changed = false;
|
||||
fontManager.on('change', () => {
|
||||
changed = true;
|
||||
});
|
||||
|
||||
fontManager.addCustomFont('Arial', 'sans-serif', storage.createAsset(
|
||||
storage.AssetType.Font,
|
||||
storage.DataFormat.TTF,
|
||||
new Uint8Array([1, 2, 3]),
|
||||
null,
|
||||
true
|
||||
));
|
||||
t.ok(changed, 'addCustomFont() emits change');
|
||||
t.ok(fontManager.hasFont('Arial'), 'updated hasFont()');
|
||||
t.same(fontManager.getFonts(), [
|
||||
{
|
||||
system: false,
|
||||
name: 'Arial',
|
||||
family: '"Arial", sans-serif',
|
||||
data: new Uint8Array([1, 2, 3]),
|
||||
format: 'ttf'
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeJSON(), [
|
||||
{
|
||||
system: false,
|
||||
family: 'Arial',
|
||||
fallback: 'sans-serif',
|
||||
md5ext: '5289df737df57326fcdd22597afb1fac.ttf'
|
||||
}
|
||||
]);
|
||||
t.same(setCustomFontsCalls, [
|
||||
{
|
||||
// eslint-disable-next-line max-len
|
||||
'"Arial", sans-serif': '@font-face { font-family: "Arial"; src: url("data:font/ttf;base64,AQID"); }'
|
||||
}
|
||||
]);
|
||||
|
||||
const assets = fontManager.serializeAssets();
|
||||
t.equal(assets.length, 1);
|
||||
t.same(assets[0].data, new Uint8Array([1, 2, 3]));
|
||||
|
||||
changed = false;
|
||||
setCustomFontsCalls.length = 0;
|
||||
const asset = storage.createAsset(
|
||||
storage.AssetType.Font,
|
||||
'woff2',
|
||||
new Uint8Array([4, 5, 6]),
|
||||
null,
|
||||
true
|
||||
);
|
||||
fontManager.addCustomFont('Comic Sans MS', 'serif', asset);
|
||||
t.ok(changed, 'addCustomFont() emits change');
|
||||
t.ok(fontManager.hasFont('Comic Sans MS'), 'updated hasFont()');
|
||||
t.same(fontManager.getFonts(), [
|
||||
{
|
||||
system: false,
|
||||
name: 'Arial',
|
||||
family: '"Arial", sans-serif',
|
||||
data: new Uint8Array([1, 2, 3]),
|
||||
format: 'ttf'
|
||||
},
|
||||
{
|
||||
system: false,
|
||||
name: 'Comic Sans MS',
|
||||
family: '"Comic Sans MS", serif',
|
||||
data: new Uint8Array([4, 5, 6]),
|
||||
format: 'woff2'
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeJSON(), [
|
||||
{
|
||||
system: false,
|
||||
family: 'Arial',
|
||||
fallback: 'sans-serif',
|
||||
md5ext: '5289df737df57326fcdd22597afb1fac.ttf'
|
||||
},
|
||||
{
|
||||
system: false,
|
||||
family: 'Comic Sans MS',
|
||||
fallback: 'serif',
|
||||
md5ext: 'b4a3ba90641372b4e4eaa841a5a400ec.woff2'
|
||||
}
|
||||
]);
|
||||
t.same(setCustomFontsCalls, [
|
||||
{
|
||||
// eslint-disable-next-line max-len
|
||||
'"Arial", sans-serif': '@font-face { font-family: "Arial"; src: url("data:font/ttf;base64,AQID"); }',
|
||||
// eslint-disable-next-line max-len
|
||||
'"Comic Sans MS", serif': '@font-face { font-family: "Comic Sans MS"; src: url("data:font/ttf;base64,BAUG"); }'
|
||||
}
|
||||
]);
|
||||
|
||||
const assets2 = fontManager.serializeAssets();
|
||||
t.equal(assets2.length, 2);
|
||||
t.equal(assets2[0], assets[0]);
|
||||
t.equal(assets2[1], asset);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('custom font validation', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
const {fontManager, storage} = rt;
|
||||
|
||||
t.throws(() => {
|
||||
fontManager.addCustomFont('family;', 'sans-serif', storage.createAsset(
|
||||
storage.DataFormat.Font,
|
||||
storage.DataFormat.TTF,
|
||||
new Uint8Array([1]),
|
||||
null,
|
||||
true
|
||||
));
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteFont', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
const {fontManager, storage} = rt;
|
||||
|
||||
fontManager.addSystemFont('Liberation Mono', 'monospace');
|
||||
fontManager.addCustomFont('Noto Sans Mono', 'monospace', storage.createAsset(
|
||||
storage.AssetType.Font,
|
||||
storage.DataFormat.TTF,
|
||||
new Uint8Array([17, 18, 19]),
|
||||
null,
|
||||
true
|
||||
));
|
||||
|
||||
t.ok(fontManager.hasFont('Liberation Mono'), 'has font initially');
|
||||
t.ok(fontManager.hasFont('Noto Sans Mono'), 'has font initially');
|
||||
|
||||
let changed = false;
|
||||
fontManager.on('change', () => {
|
||||
changed = true;
|
||||
});
|
||||
|
||||
const setCustomFontsCalls = [];
|
||||
const mockRenderer = {
|
||||
setLayerGroupOrdering: () => {},
|
||||
setCustomFonts: customFonts => {
|
||||
setCustomFontsCalls.push(customFonts);
|
||||
}
|
||||
};
|
||||
rt.attachRenderer(mockRenderer);
|
||||
|
||||
fontManager.deleteFont(1);
|
||||
t.ok(changed, 'deleteFont() emits change');
|
||||
t.ok(fontManager.hasFont('Liberation Mono'), 'kept font');
|
||||
t.notOk(fontManager.hasFont('Noto Sans Mono'), 'deleted font');
|
||||
t.same(setCustomFontsCalls, [{}], 'called setCustomFonts() after deleting non-system font');
|
||||
t.same(fontManager.getFonts(), [
|
||||
{
|
||||
system: true,
|
||||
name: 'Liberation Mono',
|
||||
family: '"Liberation Mono", monospace',
|
||||
data: null,
|
||||
format: null
|
||||
}
|
||||
], 'updated getFonts() after deleting');
|
||||
|
||||
changed = false;
|
||||
fontManager.deleteFont(0);
|
||||
t.ok(changed, 'deleteFont() emits change');
|
||||
t.same(setCustomFontsCalls, [{}], 'did not call setCustomFonts() again after deleting system font');
|
||||
t.same(fontManager.getFonts(), [], 'updated getFonts() after deleting');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fonts are serialized by VM', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
const {storage, fontManager} = vm.runtime;
|
||||
|
||||
fontManager.addSystemFont('DejaVu Sans', 'sans-serif');
|
||||
const fontAsset = storage.createAsset(
|
||||
storage.AssetType.Font,
|
||||
storage.DataFormat.TTF,
|
||||
new Uint8Array([10, 11, 12]),
|
||||
null,
|
||||
true
|
||||
);
|
||||
fontManager.addCustomFont('Noto Color Emoji', 'emoji', fontAsset);
|
||||
|
||||
const assets = vm.assets;
|
||||
t.same(assets, [fontAsset], 'font is in vm.assets');
|
||||
|
||||
const serializedAssets = vm.serializeAssets();
|
||||
t.same(serializedAssets, [
|
||||
{
|
||||
fileName: '94263e4d553bcec128704e354b659526.ttf',
|
||||
fileContent: new Uint8Array([10, 11, 12])
|
||||
}
|
||||
], 'font is in vm.serializeAssets()');
|
||||
|
||||
const notZippedProject = vm.saveProjectSb3DontZip();
|
||||
t.equal(
|
||||
notZippedProject['94263e4d553bcec128704e354b659526.ttf'],
|
||||
fontAsset.data,
|
||||
'font is in saveProjectSb3DontZip()'
|
||||
);
|
||||
|
||||
const projectJSON = JSON.parse(vm.toJSON());
|
||||
t.same(projectJSON.customFonts, [
|
||||
{
|
||||
system: true,
|
||||
family: 'DejaVu Sans',
|
||||
fallback: 'sans-serif'
|
||||
},
|
||||
{
|
||||
system: false,
|
||||
family: 'Noto Color Emoji',
|
||||
fallback: 'emoji',
|
||||
md5ext: '94263e4d553bcec128704e354b659526.ttf'
|
||||
}
|
||||
], 'font is in vm.toJSON()');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('does not serialize fonts if there are none', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const json = JSON.parse(vm.toJSON());
|
||||
t.not('customFonts' in json);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serialization and deserialization roundtrip - project', t => {
|
||||
const originalVM = new VirtualMachine();
|
||||
originalVM.attachStorage(makeTestStorage());
|
||||
const {storage, fontManager} = originalVM.runtime;
|
||||
|
||||
originalVM.loadProject(fs.readFileSync(emptyProjectFixture)).then(() => {
|
||||
// Add our custom fonts here
|
||||
fontManager.addSystemFont('Ubuntu Mono', 'monospace');
|
||||
const fontAsset = storage.createAsset(
|
||||
storage.AssetType.Font,
|
||||
storage.DataFormat.TTF,
|
||||
new Uint8Array([20, 21, 22, 23, 24]),
|
||||
null,
|
||||
true
|
||||
);
|
||||
fontManager.addCustomFont('Inter', 'sans-serif', fontAsset);
|
||||
|
||||
originalVM.saveProjectSb3('arraybuffer').then(projectSb3 => {
|
||||
const newVM = new VirtualMachine();
|
||||
newVM.attachStorage(makeTestStorage());
|
||||
|
||||
const newFontManager = newVM.runtime.fontManager;
|
||||
newFontManager.addSystemFont('ShouldBeRemoved', 'sans-serif');
|
||||
|
||||
let changed = false;
|
||||
newFontManager.on('change', () => {
|
||||
changed = true;
|
||||
});
|
||||
|
||||
newVM.loadProject(projectSb3).then(() => {
|
||||
t.ok(changed, 'loadProject() emits change');
|
||||
|
||||
t.same(newFontManager.getFonts(), [
|
||||
{
|
||||
system: true,
|
||||
name: 'Ubuntu Mono',
|
||||
family: '"Ubuntu Mono", monospace',
|
||||
data: null,
|
||||
format: null
|
||||
},
|
||||
{
|
||||
system: false,
|
||||
name: 'Inter',
|
||||
family: '"Inter", sans-serif',
|
||||
data: new Uint8Array([20, 21, 22, 23, 24]),
|
||||
format: 'ttf'
|
||||
}
|
||||
], 'preserved in getFonts()');
|
||||
t.same(newFontManager.serializeJSON(), [
|
||||
{
|
||||
system: true,
|
||||
family: 'Ubuntu Mono',
|
||||
fallback: 'monospace'
|
||||
},
|
||||
{
|
||||
system: false,
|
||||
family: 'Inter',
|
||||
fallback: 'sans-serif',
|
||||
md5ext: '316f84429ec778137b2f5c6f893c7e41.ttf'
|
||||
}
|
||||
], 'preserved in serializeJSON()');
|
||||
const assets = newFontManager.serializeAssets();
|
||||
t.equal(assets.length, 1);
|
||||
t.same(assets[0].data, new Uint8Array([20, 21, 22, 23, 24]), 'preserved in serializeAssets()');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('serialization and deserialization roundtrip - target', t => {
|
||||
const originalVM = new VirtualMachine();
|
||||
originalVM.attachStorage(makeTestStorage());
|
||||
const {fontManager, storage} = originalVM.runtime;
|
||||
|
||||
originalVM.loadProject(fs.readFileSync(emptyProjectFixture)).then(() => {
|
||||
// The fixture we use only contains a stage. We'll convert it to a sprite so we can
|
||||
// addSprite() it later.
|
||||
const sprite = originalVM.runtime.targets[0];
|
||||
sprite.isStage = false;
|
||||
|
||||
const noFontsJSON = JSON.parse(originalVM.toJSON(sprite.id));
|
||||
t.notOk('customFonts' in noFontsJSON, 'does not serialize fonts in target if no fonts');
|
||||
|
||||
fontManager.addCustomFont('Noto Sans Traditional Chinese', 'sans-serif', storage.createAsset(
|
||||
storage.AssetType.Font,
|
||||
storage.DataFormat.TTF,
|
||||
new Uint8Array([97, 98, 99]),
|
||||
null,
|
||||
true
|
||||
));
|
||||
fontManager.addSystemFont('FreeSans', 'sans-serif');
|
||||
|
||||
const spriteJSON = JSON.parse(originalVM.toJSON(sprite.id));
|
||||
t.same(spriteJSON.customFonts, [
|
||||
{
|
||||
system: false,
|
||||
family: 'Noto Sans Traditional Chinese',
|
||||
fallback: 'sans-serif',
|
||||
md5ext: '900150983cd24fb0d6963f7d28e17f72.ttf'
|
||||
},
|
||||
{
|
||||
system: true,
|
||||
family: 'FreeSans',
|
||||
fallback: 'sans-serif'
|
||||
}
|
||||
], 'serializes custom fonts to target');
|
||||
|
||||
originalVM.exportSprite(sprite.id, 'uint8array').then(exportedSprite => {
|
||||
const newVM = new VirtualMachine();
|
||||
newVM.attachStorage(makeTestStorage());
|
||||
const newFontManager = newVM.runtime.fontManager;
|
||||
|
||||
newVM.loadProject(fs.readFileSync(emptyProjectFixture)).then(() => {
|
||||
// The existing fonts should not be removed or overwritten
|
||||
newFontManager.addSystemFont('Liberation Sans', 'sans-serif');
|
||||
newFontManager.addSystemFont('FreeSans', 'monospace');
|
||||
|
||||
let changed = false;
|
||||
newFontManager.on('change', () => {
|
||||
changed = true;
|
||||
});
|
||||
|
||||
newVM.addSprite(exportedSprite).then(() => {
|
||||
t.ok(changed, 'addSprite() emits change');
|
||||
|
||||
t.same(newFontManager.getFonts(), [
|
||||
// Importing a sprite should not overwrite old fonts.
|
||||
{
|
||||
system: true,
|
||||
name: 'Liberation Sans',
|
||||
family: '"Liberation Sans", sans-serif',
|
||||
data: null,
|
||||
format: null
|
||||
},
|
||||
{
|
||||
system: true,
|
||||
name: 'FreeSans',
|
||||
family: '"FreeSans", monospace',
|
||||
data: null,
|
||||
format: null
|
||||
},
|
||||
{
|
||||
system: false,
|
||||
name: 'Noto Sans Traditional Chinese',
|
||||
family: '"Noto Sans Traditional Chinese", sans-serif',
|
||||
data: new Uint8Array([97, 98, 99]),
|
||||
format: 'ttf'
|
||||
}
|
||||
], 'imported fonts from sprite');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('deserializes ignores invalid fonts', t => {
|
||||
const {fontManager} = new Runtime();
|
||||
fontManager.deserialize([
|
||||
{
|
||||
system: true,
|
||||
family: ';} body { display: none; }',
|
||||
fallback: 'sans-serif'
|
||||
},
|
||||
{
|
||||
system: true,
|
||||
family: 'Source Code Pro',
|
||||
fallback: 'monospace'
|
||||
}
|
||||
], null, false).then(() => {
|
||||
t.equal(fontManager.getFonts().length, 1);
|
||||
t.equal(fontManager.getFonts()[0].name, 'Source Code Pro');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
35
scratch-vm/test/integration/tw_get_exported_costume.js
Normal file
35
scratch-vm/test/integration/tw_get_exported_costume.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const {test} = require('tap');
|
||||
|
||||
// The actual logic of the costume exporting and importing is tested elsewhere.
|
||||
// This is just to make sure that the VM's shims are going to the right place.
|
||||
|
||||
test('getExportedCostume', t => {
|
||||
const vm = new VM();
|
||||
t.same(
|
||||
vm.getExportedCostume({
|
||||
asset: {
|
||||
data: new Uint8Array([97, 98, 99])
|
||||
},
|
||||
dataFormat: 'png'
|
||||
}),
|
||||
new Uint8Array([97, 98, 99])
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getExportedCostumeBase64', t => {
|
||||
// We'll just make sure that the output is being base64 encoded.
|
||||
const vm = new VM();
|
||||
t.same(
|
||||
vm.getExportedCostumeBase64({
|
||||
asset: {
|
||||
data: new Uint8Array([97, 98, 99])
|
||||
},
|
||||
dataFormat: 'png'
|
||||
}),
|
||||
// btoa("abc")
|
||||
'YWJj'
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
226
scratch-vm/test/integration/tw_hats_and_events.js
Normal file
226
scratch-vm/test/integration/tw_hats_and_events.js
Normal file
@@ -0,0 +1,226 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {test} = require('tap');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
const ArgumentType = require('../../src/extension-support/argument-type');
|
||||
|
||||
const compilerAndInterpreter = (name, callback) => {
|
||||
test(`${name} - interpreted`, t => {
|
||||
callback(t, {
|
||||
enabled: false
|
||||
});
|
||||
});
|
||||
test(`${name} - compiled`, t => {
|
||||
callback(t, {
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const fixture = fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'tw-hats-and-events.sb3'));
|
||||
|
||||
compilerAndInterpreter('hats and events', (t, co) => {
|
||||
const vm = new VM();
|
||||
vm.setCompilerOptions(co);
|
||||
|
||||
let log = [];
|
||||
let hatReturns = false;
|
||||
class TestExtension {
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'testpredicate',
|
||||
name: 'Test Predicate',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'event',
|
||||
blockType: BlockType.EVENT,
|
||||
text: 'event block',
|
||||
isEdgeActivated: false
|
||||
},
|
||||
{
|
||||
opcode: 'hat',
|
||||
blockType: BlockType.HAT,
|
||||
text: 'hat block',
|
||||
isEdgeActivated: false
|
||||
},
|
||||
{
|
||||
opcode: 'complexhat',
|
||||
blockType: BlockType.HAT,
|
||||
text: 'complex hat [MENU] if [INPUT]',
|
||||
isEdgeActivated: false,
|
||||
arguments: {
|
||||
MENU: {
|
||||
menu: 'test'
|
||||
},
|
||||
INPUT: {
|
||||
type: ArgumentType.STRING,
|
||||
defaultValue: 'default'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'clickme',
|
||||
blockType: BlockType.HAT,
|
||||
text: 'stack click test [INPUT]',
|
||||
isEdgeActivated: false,
|
||||
arguments: {
|
||||
INPUT: {
|
||||
type: ArgumentType.STRING,
|
||||
defaultValue: 'default'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
menus: {
|
||||
test: {
|
||||
acceptReporters: false,
|
||||
items: ['a', 'b', 'c']
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
event () {
|
||||
log.push('this should never run');
|
||||
}
|
||||
hat () {
|
||||
log.push(`hat ${hatReturns}`);
|
||||
return hatReturns;
|
||||
}
|
||||
complexhat ({INPUT}) {
|
||||
log.push(`complex hat ${INPUT}`);
|
||||
return !!INPUT;
|
||||
}
|
||||
clickme () {
|
||||
log.push('clickme');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
vm.extensionManager.addBuiltinExtension('testpredicate', TestExtension);
|
||||
|
||||
vm.on('COMPILE_ERROR', () => {
|
||||
t.fail('Compile error');
|
||||
});
|
||||
|
||||
vm.loadProject(fixture).then(async () => {
|
||||
log = vm.runtime.getTargetForStage().lookupVariableByNameAndType('log', 'list').value;
|
||||
t.same(log, [], 'sanity check - log starts empty');
|
||||
|
||||
// Let it run for a bit. Nothing should happen on its own.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vm.runtime._step();
|
||||
}
|
||||
t.same(log, [], 'nothing happens initially');
|
||||
|
||||
// See if events work
|
||||
vm.runtime.startHats('testpredicate_event');
|
||||
t.same(log, [], 'event function does not get called, even if it exists');
|
||||
vm.runtime._step();
|
||||
t.same(log, ['event'], 'ran event script');
|
||||
|
||||
log.length = 0;
|
||||
|
||||
// Test hat that returns false
|
||||
hatReturns = false;
|
||||
vm.runtime.startHats('testpredicate_hat');
|
||||
t.same(log, ['hat false'], 'ran hat function');
|
||||
vm.runtime._step();
|
||||
t.same(log, ['hat false'], 'did not run hat script');
|
||||
|
||||
// Test hat that returns true
|
||||
hatReturns = true;
|
||||
vm.runtime.startHats('testpredicate_hat');
|
||||
t.same(log, [
|
||||
'hat false',
|
||||
'hat true'
|
||||
], 'ran hat function');
|
||||
vm.runtime._step();
|
||||
t.same(log, [
|
||||
'hat false',
|
||||
'hat true',
|
||||
'hat'
|
||||
], 'ran hat script');
|
||||
|
||||
log.length = 0;
|
||||
|
||||
// Test hat that returns false in a Promise
|
||||
hatReturns = Promise.resolve(false);
|
||||
vm.runtime.startHats('testpredicate_hat');
|
||||
t.same(log, ['hat [object Promise]'], 'ran hat function');
|
||||
vm.runtime._step();
|
||||
t.same(log, ['hat [object Promise]'], 'hat script does not run before promise finishes');
|
||||
await Promise.resolve(); // Allow promise to be processed
|
||||
vm.runtime._step();
|
||||
t.same(log, ['hat [object Promise]'], 'hat script still does not run');
|
||||
|
||||
log.length = 0;
|
||||
|
||||
// Test hat that returns true in a Promise
|
||||
hatReturns = Promise.resolve(true);
|
||||
vm.runtime.startHats('testpredicate_hat');
|
||||
t.same(log, ['hat [object Promise]'], 'ran hat function');
|
||||
vm.runtime._step();
|
||||
t.same(log, ['hat [object Promise]'], 'hat script does not run before promise finishes');
|
||||
await Promise.resolve();
|
||||
vm.runtime._step();
|
||||
t.same(log, ['hat [object Promise]', 'hat'], 'hat script runs after promise finishes');
|
||||
|
||||
log.length = 0;
|
||||
|
||||
// Test complex hat
|
||||
vm.runtime.startHats('testpredicate_complexhat', {
|
||||
MENU: 'a'
|
||||
});
|
||||
t.same(log, [
|
||||
'complex hat ',
|
||||
'complex hat 1'
|
||||
], 'ran complex hat functions');
|
||||
vm.runtime._step();
|
||||
t.same(log, [
|
||||
'complex hat ',
|
||||
'complex hat 1',
|
||||
'complex hat a 2'
|
||||
], 'ran complex hat script');
|
||||
|
||||
log.length = 0;
|
||||
|
||||
// Test complex hat with a complex input
|
||||
vm.runtime.startHats('testpredicate_complexhat', {
|
||||
MENU: 'b'
|
||||
});
|
||||
t.same(log, [], 'control flow in complex inputs is not run immediately');
|
||||
vm.runtime._step();
|
||||
t.same(log, [
|
||||
'evaluated block ',
|
||||
'evaluated block 1'
|
||||
], 'evaluated complex inputs but not hat function');
|
||||
vm.runtime._step();
|
||||
t.same(log, [
|
||||
'evaluated block ',
|
||||
'evaluated block 1',
|
||||
'complex hat ',
|
||||
'complex hat 1',
|
||||
'complex hat b 2'
|
||||
], 'evaluated complex hat functions and scripts');
|
||||
|
||||
log.length = 0;
|
||||
|
||||
// Test that in stackClick mode, the hat block still gets run, but the result is ignored
|
||||
const sprite = vm.runtime.targets[1];
|
||||
const allBlocks = Object.values(sprite.sprite.blocks._blocks);
|
||||
const clickBlockId = allBlocks.find(i => i.opcode === 'testpredicate_clickme').id;
|
||||
vm.runtime._pushThread(clickBlockId, sprite, {
|
||||
stackClick: true
|
||||
});
|
||||
t.same(log, [], 'stackClick does not run anything immediately');
|
||||
vm.runtime._step();
|
||||
t.same(log, ['evaluated block something'], 'stackClick hat ran input');
|
||||
vm.runtime._step();
|
||||
t.same(log, ['evaluated block something', 'clickme'], 'stackClick hat input evaluated');
|
||||
await Promise.resolve(); // Allow promise to be processed
|
||||
vm.runtime._step();
|
||||
t.same(log, ['evaluated block something', 'clickme', 'stack click'], 'stackClick hat script ran');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
79
scratch-vm/test/integration/tw_import_sbx.js
Normal file
79
scratch-vm/test/integration/tw_import_sbx.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const sb2 = require('../../src/serialization/sb2');
|
||||
const {test} = require('tap');
|
||||
|
||||
test('importing ScratchX/.sbx project', async t => {
|
||||
const rt = new Runtime();
|
||||
|
||||
const deserialized = await sb2.deserialize(
|
||||
{
|
||||
objName: 'Stage',
|
||||
scripts: [
|
||||
[0, 0, [['Text to Speech.speak_text', 'Hello!']]],
|
||||
[788, 33, [['Spotify\u001feveryBar']]],
|
||||
[100, 10, [['Weather extension\u001fgetWeather', 'temperature', 'Cambridge, MA']]],
|
||||
[60, 40, [['Synth Extension.setEffect', 'glide', 10]]]
|
||||
],
|
||||
sounds: [],
|
||||
costumes: [],
|
||||
children: [],
|
||||
info: {
|
||||
savedExtensions: [
|
||||
{
|
||||
menus: {
|
||||
// not important for this test
|
||||
},
|
||||
extensionName: 'Spotify',
|
||||
javascriptURL: 'https://ericrosenbaum.github.io/spotify-extension/extension.js',
|
||||
blockSpecs: [
|
||||
// not important for this test
|
||||
]
|
||||
},
|
||||
{
|
||||
extensionName: 'Weather extension',
|
||||
javascriptURL: 'http://khanning.github.io/scratch-weather-extension/weather_extension.js'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
rt
|
||||
);
|
||||
|
||||
const extensionIDs = deserialized.extensions.extensionIDs;
|
||||
t.equal(extensionIDs.size, 4);
|
||||
t.ok(extensionIDs.has('sbxtexttospeech'));
|
||||
t.ok(extensionIDs.has('sbxspotify'));
|
||||
t.ok(extensionIDs.has('sbxweatherextension'));
|
||||
t.ok(extensionIDs.has('sbxsynthextension'));
|
||||
|
||||
const extensionURLs = deserialized.extensions.extensionURLs;
|
||||
t.equal(extensionURLs.size, 2);
|
||||
t.equal(extensionURLs.get('sbxspotify'), 'https://ericrosenbaum.github.io/spotify-extension/extension.js');
|
||||
t.equal(extensionURLs.get('sbxweatherextension'), 'http://khanning.github.io/scratch-weather-extension/weather_extension.js');
|
||||
|
||||
const stage = deserialized.targets[0];
|
||||
const blocks = Object.values(stage.blocks._blocks);
|
||||
|
||||
const textToSpeech = blocks.find(i => i.opcode === 'sbxtexttospeech_speak_text');
|
||||
t.type(textToSpeech, 'object');
|
||||
t.type(textToSpeech.inputs['0'], 'object');
|
||||
t.type(textToSpeech.inputs['1'], 'undefined');
|
||||
|
||||
const spotify = blocks.find(i => i.opcode === 'sbxspotify_everyBar');
|
||||
t.type(spotify, 'object');
|
||||
t.type(spotify.inputs['0'], 'undefined');
|
||||
|
||||
const weather = blocks.find(i => i.opcode === 'sbxweatherextension_getWeather');
|
||||
t.type(weather, 'object');
|
||||
t.type(weather.inputs['0'], 'object');
|
||||
t.type(weather.inputs['1'], 'object');
|
||||
t.type(weather.inputs['2'], 'undefined');
|
||||
|
||||
const synth = blocks.find(i => i.opcode === 'sbxsynthextension_setEffect');
|
||||
t.type(synth, 'object');
|
||||
t.type(synth.inputs['0'], 'object');
|
||||
t.type(synth.inputs['1'], 'object');
|
||||
t.type(synth.inputs['2'], 'undefined');
|
||||
|
||||
t.end();
|
||||
});
|
||||
41
scratch-vm/test/integration/tw_label_block.js
Normal file
41
scratch-vm/test/integration/tw_label_block.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const htmlparser = require('htmlparser2');
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
test('Label blocks', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testlabel',
|
||||
name: `Label Test`,
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.LABEL,
|
||||
text: 'test <>&"\''
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const xmlList = vm.runtime.getBlocksXML();
|
||||
t.equal(xmlList.length, 1);
|
||||
|
||||
const parsedXML = htmlparser.parseDOM(xmlList[0].xml);
|
||||
// Expecting something like this:
|
||||
// <category name="Label Test" id="testlabel" colour="#0FBD8C" secondaryColour="#0DA57A">
|
||||
// <label text="<>&"'"></label>
|
||||
// </category>
|
||||
t.equal(parsedXML.length, 1);
|
||||
|
||||
const category = parsedXML[0];
|
||||
t.equal(category.name, 'category');
|
||||
t.equal(category.children.length, 1);
|
||||
|
||||
const label = category.children[0];
|
||||
t.equal(label.name, 'label');
|
||||
t.equal(label.children.length, 0);
|
||||
t.equal(label.attribs.text, 'test <>&"'');
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {test} = require('tap');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
|
||||
const compilerAndInterpreter = (name, callback) => {
|
||||
test(`${name} - interpreted`, t => {
|
||||
callback(t, {
|
||||
enabled: false
|
||||
});
|
||||
});
|
||||
test(`${name} - compiled`, t => {
|
||||
callback(t, {
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
global.document = {
|
||||
hidden: true
|
||||
};
|
||||
|
||||
compilerAndInterpreter('last block in loop returns Promise', (t, co) => {
|
||||
t.plan(1);
|
||||
|
||||
const vm = new VM();
|
||||
vm.setCompilerOptions(co);
|
||||
|
||||
const fixturePath = path.join(__dirname, '../fixtures/tw-last-block-in-loop-returns-promise.sb3');
|
||||
vm.loadProject(fs.readFileSync(fixturePath)).then(async () => {
|
||||
// This is a stand-in for a block like "move 10 steps"
|
||||
// This is just easier than attaching a real mock renderer and everything that ends up requiring
|
||||
vm.addAddonBlock({
|
||||
procedureCode: 'something to simulate a redraw request',
|
||||
callback: () => {
|
||||
vm.runtime.requestRedraw();
|
||||
},
|
||||
arguments: []
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
let iterations = 0;
|
||||
while (vm.runtime.threads.length > 0) {
|
||||
iterations++;
|
||||
vm.runtime._step();
|
||||
// Give Promises a chance to resolve
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
t.equal(iterations, 16);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
const {loadCostume} = require('../../src/import/load-costume');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const {test} = require('tap');
|
||||
|
||||
test('importing SVG with stored rotation center', async t => {
|
||||
t.plan(3);
|
||||
const runtime = new Runtime();
|
||||
const storage = makeTestStorage();
|
||||
runtime.attachStorage(storage);
|
||||
const renderer = new FakeRenderer();
|
||||
renderer.createSVGSkin = (svgText, rotationCenter) => {
|
||||
// Make sure that the rotation center given to the renderer is correct
|
||||
t.same(rotationCenter, [106.62300344745225, -11.822572945859918]);
|
||||
// just need to return a valid skin ID, doesn't matter
|
||||
return 1;
|
||||
};
|
||||
runtime.attachRenderer(renderer);
|
||||
const svg = new TextEncoder().encode(
|
||||
`<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg><!--rotationCenter:106.62300344745225:-11.822572945859918-->`
|
||||
);
|
||||
const asset = storage.createAsset(storage.AssetType.ImageVector, storage.DataFormat.SVG, svg, null, true);
|
||||
const costume = await loadCostume(`${asset.assetId}.svg`, {
|
||||
asset
|
||||
}, runtime);
|
||||
t.equal(costume.rotationCenterX, 106.62300344745225);
|
||||
t.equal(costume.rotationCenterY, -11.822572945859918);
|
||||
t.end();
|
||||
});
|
||||
121
scratch-vm/test/integration/tw_packaged_runtime.js
Normal file
121
scratch-vm/test/integration/tw_packaged_runtime.js
Normal file
@@ -0,0 +1,121 @@
|
||||
const {loadCostume} = require('../../src/import/load-costume');
|
||||
const {loadSound} = require('../../src/import/load-sound');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||
const {test} = require('tap');
|
||||
|
||||
global.Image = function () {
|
||||
const image = {
|
||||
width: 10,
|
||||
height: 10
|
||||
};
|
||||
setTimeout(() => {
|
||||
if (image.onload) {
|
||||
image.onload();
|
||||
}
|
||||
});
|
||||
return image;
|
||||
};
|
||||
|
||||
class FakeAudioEngine {
|
||||
decodeSoundPlayer () {
|
||||
return Promise.resolve({
|
||||
id: 0,
|
||||
buffer: {
|
||||
sampleRate: 1,
|
||||
length: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('load bitmap in packaged runtime', async t => {
|
||||
const rt = new Runtime();
|
||||
rt.convertToPackagedRuntime();
|
||||
rt.attachRenderer(new FakeRenderer());
|
||||
rt.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||
const storage = makeTestStorage();
|
||||
rt.attachStorage(storage);
|
||||
const asset = storage.createAsset(
|
||||
storage.AssetType.ImageBitmap,
|
||||
storage.DataFormat.PNG,
|
||||
new ArrayBuffer(10),
|
||||
null,
|
||||
true
|
||||
);
|
||||
const costume = await loadCostume(`${asset.assetId}.png`, {asset}, rt);
|
||||
t.equal(costume.asset, null);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load vector in packaged runtime', async t => {
|
||||
const rt = new Runtime();
|
||||
rt.convertToPackagedRuntime();
|
||||
rt.attachRenderer(new FakeRenderer());
|
||||
const storage = makeTestStorage();
|
||||
rt.attachStorage(storage);
|
||||
const asset = storage.createAsset(
|
||||
storage.AssetType.ImageVector,
|
||||
storage.DataFormat.SVG,
|
||||
new ArrayBuffer(10),
|
||||
null,
|
||||
true
|
||||
);
|
||||
const costume = await loadCostume(`${asset.assetId}.svg`, {asset}, rt);
|
||||
t.equal(costume.asset, null);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load sound in packaged runtime', async t => {
|
||||
const rt = new Runtime();
|
||||
rt.convertToPackagedRuntime();
|
||||
const storage = makeTestStorage();
|
||||
rt.attachStorage(storage);
|
||||
rt.attachAudioEngine(new FakeAudioEngine());
|
||||
const asset = storage.createAsset(
|
||||
storage.AssetType.Sound,
|
||||
storage.DataFormat.MP3,
|
||||
new ArrayBuffer(10),
|
||||
null,
|
||||
true
|
||||
);
|
||||
const costume = await loadSound({
|
||||
asset,
|
||||
md5: `${asset.assetId}.mp3`
|
||||
}, rt, null);
|
||||
t.equal(costume.asset, null);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('storage.createAsset never generates real asset IDs', t => {
|
||||
const rt = new Runtime();
|
||||
rt.convertToPackagedRuntime();
|
||||
const storage = makeTestStorage();
|
||||
rt.attachStorage(storage);
|
||||
const shouldUseGivenID = storage.createAsset(
|
||||
storage.AssetType.ImageBitmap,
|
||||
storage.DataFormat.PNG,
|
||||
new ArrayBuffer(10),
|
||||
'a'.repeat(32)
|
||||
);
|
||||
t.equal(shouldUseGivenID.assetId, 'a'.repeat(32));
|
||||
const shouldUseFakeID = storage.createAsset(
|
||||
storage.AssetType.ImageBitmap,
|
||||
storage.DataFormat.PNG,
|
||||
new ArrayBuffer(11),
|
||||
null,
|
||||
true
|
||||
);
|
||||
t.equal(shouldUseFakeID.assetId, '1');
|
||||
const shouldUseDifferentFakeID = storage.createAsset(
|
||||
storage.AssetType.ImageBitmap,
|
||||
storage.DataFormat.PNG,
|
||||
new ArrayBuffer(12),
|
||||
null,
|
||||
true
|
||||
);
|
||||
t.equal(shouldUseDifferentFakeID.assetId, '2');
|
||||
t.end();
|
||||
});
|
||||
161
scratch-vm/test/integration/tw_platform.js
Normal file
161
scratch-vm/test/integration/tw_platform.js
Normal file
@@ -0,0 +1,161 @@
|
||||
const {test} = require('tap');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const platform = require('../../src/engine/tw-platform');
|
||||
const Clone = require('../../src/util/clone');
|
||||
|
||||
test('the internal object', t => {
|
||||
// the idea with this test is to make it harder for forks to screw up modifying the file
|
||||
t.type(platform.name, 'string');
|
||||
t.type(platform.url, 'string');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('vm property', t => {
|
||||
const vm = new VM();
|
||||
t.same(vm.runtime.platform, platform, 'copy of tw-platform.js');
|
||||
t.not(vm.runtime.platform, platform, 'not the same object as tw-platform.js');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('sanitize', t => {
|
||||
const vm = new VM();
|
||||
vm.runtime.platform.name += ' - test';
|
||||
const json = JSON.parse(vm.toJSON());
|
||||
t.same(json.meta.platform, vm.runtime.platform, 'copy of runtime.platform');
|
||||
t.not(json.meta.platform, vm.runtime.platform, 'not the same object as runtime.platform');
|
||||
t.end();
|
||||
});
|
||||
|
||||
const vanillaProject = {
|
||||
targets: [
|
||||
{
|
||||
isStage: true,
|
||||
name: 'Stage',
|
||||
variables: {},
|
||||
lists: {},
|
||||
broadcasts: {},
|
||||
blocks: {},
|
||||
comments: {},
|
||||
currentCostume: 0,
|
||||
costumes: [
|
||||
{
|
||||
name: 'backdrop1',
|
||||
dataFormat: 'svg',
|
||||
assetId: 'cd21514d0531fdffb22204e0ec5ed84a',
|
||||
md5ext: 'cd21514d0531fdffb22204e0ec5ed84a.svg',
|
||||
rotationCenterX: 240,
|
||||
rotationCenterY: 180
|
||||
}
|
||||
],
|
||||
sounds: [],
|
||||
volume: 100,
|
||||
layerOrder: 0,
|
||||
tempo: 60,
|
||||
videoTransparency: 50,
|
||||
videoState: 'on',
|
||||
textToSpeechLanguage: null
|
||||
}
|
||||
],
|
||||
monitors: [],
|
||||
extensions: [],
|
||||
meta: {
|
||||
semver: '3.0.0',
|
||||
vm: '0.2.0',
|
||||
agent: ''
|
||||
}
|
||||
};
|
||||
|
||||
test('deserialize no platform', t => {
|
||||
const vm = new VM();
|
||||
vm.runtime.on('PLATFORM_MISMATCH', () => {
|
||||
t.fail('Called PLATFORM_MISMATCH');
|
||||
});
|
||||
vm.loadProject(vanillaProject).then(() => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserialize matching platform', t => {
|
||||
const vm = new VM();
|
||||
vm.runtime.on('PLATFORM_MISMATCH', () => {
|
||||
t.fail('Called PLATFORM_MISMATCH');
|
||||
});
|
||||
const project = Clone.simple(vanillaProject);
|
||||
project.meta.platform = Object.assign({}, platform);
|
||||
vm.loadProject(project).then(() => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserialize mismatching platform with no listener', t => {
|
||||
const vm = new VM();
|
||||
const project = Clone.simple(vanillaProject);
|
||||
project.meta.platform = {
|
||||
name: '3tw4ergo980uitegr5hoijuk;'
|
||||
};
|
||||
vm.loadProject(project).then(() => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserialize mismatching platform with 1 listener', t => {
|
||||
t.plan(2);
|
||||
const vm = new VM();
|
||||
vm.runtime.on('PLATFORM_MISMATCH', (pl, callback) => {
|
||||
t.same(pl, {
|
||||
name: 'aa',
|
||||
url: '...'
|
||||
});
|
||||
t.ok('called PLATFORM_MISMATCH');
|
||||
callback();
|
||||
});
|
||||
const project = Clone.simple(vanillaProject);
|
||||
project.meta.platform = {
|
||||
name: 'aa',
|
||||
url: '...'
|
||||
};
|
||||
vm.loadProject(project).then(() => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserialize mismatching platform with 3 listeners', t => {
|
||||
t.plan(2);
|
||||
|
||||
const calls = [];
|
||||
let expectedToLoad = false;
|
||||
const vm = new VM();
|
||||
vm.runtime.on('PLATFORM_MISMATCH', (_, callback) => {
|
||||
calls.push([1, callback]);
|
||||
});
|
||||
vm.runtime.on('PLATFORM_MISMATCH', (_, callback) => {
|
||||
calls.push([2, callback]);
|
||||
});
|
||||
vm.runtime.on('PLATFORM_MISMATCH', (_, callback) => {
|
||||
calls.push([3, callback]);
|
||||
});
|
||||
|
||||
const project = Clone.simple(vanillaProject);
|
||||
project.meta.platform = {
|
||||
name: ''
|
||||
};
|
||||
vm.loadProject(project).then(() => {
|
||||
t.ok(expectedToLoad);
|
||||
t.end();
|
||||
});
|
||||
|
||||
// loadProject is async, may need to wait a bit
|
||||
setTimeout(async () => {
|
||||
t.same(calls.map(i => i[0]), [1, 2, 3], 'listeners called in correct order');
|
||||
|
||||
// loadProject should not finish until we call all of the listeners' callbacks
|
||||
calls[0][1]();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
calls[1][1]();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expectedToLoad = true;
|
||||
calls[2][1]();
|
||||
}, 0);
|
||||
});
|
||||
116
scratch-vm/test/integration/tw_privacy.js
Normal file
116
scratch-vm/test/integration/tw_privacy.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const {test} = require('tap');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
|
||||
const mockRenderer = () => ({
|
||||
setLayerGroupOrdering: () => {
|
||||
// not relevant to this test
|
||||
},
|
||||
|
||||
privateSkinAccess: true,
|
||||
setPrivateSkinAccess (enabled) {
|
||||
this.privateSkinAccess = enabled;
|
||||
}
|
||||
});
|
||||
|
||||
test('baseline: no external communication methods', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachRenderer(mockRenderer());
|
||||
t.equal(rt.renderer.privateSkinAccess, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('throws errors for unknown method', t => {
|
||||
t.plan(1);
|
||||
const rt = new Runtime();
|
||||
try {
|
||||
rt.setExternalCommunicationMethod('something fake', true);
|
||||
} catch (e) {
|
||||
t.equal(e.message, 'Unknown method: something fake');
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('communication method enabled after attaching renderer', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachRenderer(mockRenderer());
|
||||
rt.setExternalCommunicationMethod('cloudVariables', true);
|
||||
t.equal(rt.renderer.privateSkinAccess, false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('communication method enabled before attaching renderer', t => {
|
||||
const rt = new Runtime();
|
||||
rt.setExternalCommunicationMethod('cloudVariables', true);
|
||||
rt.attachRenderer(mockRenderer());
|
||||
t.equal(rt.renderer.privateSkinAccess, false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('disable enforcement', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachRenderer(mockRenderer());
|
||||
rt.setEnforcePrivacy(false);
|
||||
rt.setExternalCommunicationMethod('cloudVariables', true);
|
||||
t.equal(rt.renderer.privateSkinAccess, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('multiple features toggled', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachRenderer(mockRenderer());
|
||||
rt.setExternalCommunicationMethod('cloudVariables', true);
|
||||
t.equal(rt.renderer.privateSkinAccess, false);
|
||||
rt.setExternalCommunicationMethod('customExtensions', true);
|
||||
t.equal(rt.renderer.privateSkinAccess, false);
|
||||
rt.setExternalCommunicationMethod('cloudVariables', false);
|
||||
t.equal(rt.renderer.privateSkinAccess, false);
|
||||
rt.setExternalCommunicationMethod('customExtensions', false);
|
||||
t.equal(rt.renderer.privateSkinAccess, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('cloud variables', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachRenderer(mockRenderer());
|
||||
|
||||
rt.addCloudVariable();
|
||||
t.equal(rt.renderer.privateSkinAccess, false);
|
||||
|
||||
rt.addCloudVariable();
|
||||
t.equal(rt.renderer.privateSkinAccess, false);
|
||||
|
||||
rt.removeCloudVariable();
|
||||
t.equal(rt.renderer.privateSkinAccess, false);
|
||||
|
||||
rt.removeCloudVariable();
|
||||
t.equal(rt.renderer.privateSkinAccess, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('custom extensions', async t => {
|
||||
const vm = new VM();
|
||||
vm.attachRenderer(mockRenderer());
|
||||
|
||||
vm.extensionManager.securityManager.getSandboxMode = () => 'unsandboxed';
|
||||
global.document = {
|
||||
createElement: () => {
|
||||
const element = {};
|
||||
setTimeout(() => {
|
||||
global.Scratch.extensions.register({
|
||||
getInfo: () => ({})
|
||||
});
|
||||
});
|
||||
return element;
|
||||
},
|
||||
body: {
|
||||
appendChild: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
t.equal(vm.renderer.privateSkinAccess, true);
|
||||
await vm.extensionManager.loadExtensionURL('data:application/javascript;,');
|
||||
t.equal(vm.renderer.privateSkinAccess, false);
|
||||
t.end();
|
||||
});
|
||||
102
scratch-vm/test/integration/tw_rejected_promise.js
Normal file
102
scratch-vm/test/integration/tw_rejected_promise.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const Scratch = require('../../src/extension-support/tw-extension-api-common');
|
||||
|
||||
const commandFixture = fs.readFileSync(path.join(__dirname, '../fixtures/tw-rejected-promise-command.sb3'));
|
||||
const reporterFixture = fs.readFileSync(path.join(__dirname, '../fixtures/tw-rejected-promise-reporter.sb3'));
|
||||
|
||||
class TestExtension {
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'test123',
|
||||
name: 'test123',
|
||||
blocks: [
|
||||
{
|
||||
blockType: Scratch.BlockType.COMMAND,
|
||||
opcode: 'command',
|
||||
text: 'return rejected promise'
|
||||
},
|
||||
{
|
||||
blockType: Scratch.BlockType.REPORTER,
|
||||
opcode: 'reporter',
|
||||
text: 'return rejected promise'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
command () {
|
||||
return Promise.reject(new Error('Test error 1'));
|
||||
}
|
||||
reporter () {
|
||||
return Promise.reject(new Error('Test error 2'));
|
||||
}
|
||||
}
|
||||
|
||||
for (const enableCompiler of [true, false]) {
|
||||
test(`COMMAND returns rejected promise - ${enableCompiler ? 'compiler' : 'interpreter'}`, t => {
|
||||
const vm = new VM();
|
||||
vm.extensionManager.addBuiltinExtension('test123', TestExtension);
|
||||
|
||||
vm.setCompilerOptions({
|
||||
enabled: enableCompiler
|
||||
});
|
||||
t.equal(vm.runtime.compilerOptions.enabled, enableCompiler);
|
||||
|
||||
vm.loadProject(commandFixture).then(async () => {
|
||||
vm.greenFlag();
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
vm.runtime._step();
|
||||
|
||||
// wait for promise rejection to be handled
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
const stage = vm.runtime.getTargetForStage();
|
||||
t.equal(stage.lookupVariableByNameAndType('before', '').value, 10);
|
||||
t.equal(stage.lookupVariableByNameAndType('after', '').value, 10);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test(`REPORTER returns rejected promise - ${enableCompiler ? 'compiler' : 'interpreter'}`, t => {
|
||||
const vm = new VM();
|
||||
vm.extensionManager.addBuiltinExtension('test123', TestExtension);
|
||||
|
||||
vm.setCompilerOptions({
|
||||
enabled: enableCompiler
|
||||
});
|
||||
t.equal(vm.runtime.compilerOptions.enabled, enableCompiler);
|
||||
|
||||
vm.loadProject(reporterFixture).then(async () => {
|
||||
vm.greenFlag();
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
vm.runtime._step();
|
||||
|
||||
// wait for promise rejection to be handled
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
const stage = vm.runtime.getTargetForStage();
|
||||
t.equal(stage.lookupVariableByNameAndType('before', '').value, 10);
|
||||
t.equal(stage.lookupVariableByNameAndType('after', '').value, 10);
|
||||
t.same(stage.lookupVariableByNameAndType('values', 'list').value, [
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2',
|
||||
'Error: Test error 2'
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
84
scratch-vm/test/integration/tw_save_project_sb3.js
Normal file
84
scratch-vm/test/integration/tw_save_project_sb3.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const {test} = require('tap');
|
||||
require('../fixtures/tw_mock_blob'); // must load Blob before VM so JSZip thinks Blob is supported
|
||||
const fs = require('fs');
|
||||
const pathUtil = require('path');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const JSZip = require('@turbowarp/jszip');
|
||||
|
||||
const fixture = fs.readFileSync(pathUtil.join(__dirname, '..', 'fixtures', 'tw-save-project-sb3.sb3'));
|
||||
|
||||
test('saveProjectSb3', async t => {
|
||||
t.plan(6);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
await vm.loadProject(fixture);
|
||||
|
||||
// Test that it defaults to Blob
|
||||
// Note: we use a mock implementation of Blob in tests
|
||||
const blob = await vm.saveProjectSb3();
|
||||
t.type(blob, Blob);
|
||||
|
||||
const buffer = await vm.saveProjectSb3('arraybuffer');
|
||||
t.type(buffer, ArrayBuffer);
|
||||
|
||||
const base64 = await vm.saveProjectSb3('base64');
|
||||
t.type(base64, 'string');
|
||||
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
t.equal((await zip.file('project.json').async('string'))[0], '{');
|
||||
t.equal((await zip.file('d9c625ae1996b615a146ac2a7dbe74d7.svg').async('uint8array')).byteLength, 691);
|
||||
t.equal((await zip.file('cd21514d0531fdffb22204e0ec5ed84a.svg').async('uint8array')).byteLength, 202);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('saveProjectSb3Stream', async t => {
|
||||
t.plan(6);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
await vm.loadProject(fixture);
|
||||
|
||||
let receivedDataEvent = false;
|
||||
const stream = vm.saveProjectSb3Stream();
|
||||
stream.on('data', data => {
|
||||
if (receivedDataEvent) {
|
||||
return;
|
||||
}
|
||||
receivedDataEvent = true;
|
||||
t.type(data, Uint8Array);
|
||||
});
|
||||
stream.resume();
|
||||
const buffer = await stream.accumulate();
|
||||
t.type(buffer, ArrayBuffer);
|
||||
|
||||
const stream2 = vm.saveProjectSb3Stream('uint8array');
|
||||
const uint8array = await stream2.accumulate();
|
||||
t.type(uint8array, Uint8Array);
|
||||
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
t.equal((await zip.file('project.json').async('string'))[0], '{');
|
||||
t.equal((await zip.file('d9c625ae1996b615a146ac2a7dbe74d7.svg').async('uint8array')).byteLength, 691);
|
||||
t.equal((await zip.file('cd21514d0531fdffb22204e0ec5ed84a.svg').async('uint8array')).byteLength, 202);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('saveProjectSb3DontZip', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
await vm.loadProject(fixture);
|
||||
|
||||
const map = vm.saveProjectSb3DontZip();
|
||||
t.equal(map['project.json'][0], '{'.charCodeAt(0));
|
||||
t.equal(map['d9c625ae1996b615a146ac2a7dbe74d7.svg'].byteLength, 691);
|
||||
t.equal(map['cd21514d0531fdffb22204e0ec5ed84a.svg'].byteLength, 202);
|
||||
|
||||
// Make sure that the asset buffers returned are the exact same as the ones used internally, not copies.
|
||||
const costume = vm.runtime.targets[0].getCostumes()[0];
|
||||
t.equal(map['cd21514d0531fdffb22204e0ec5ed84a.svg'], costume.asset.data);
|
||||
|
||||
t.end();
|
||||
});
|
||||
268
scratch-vm/test/integration/tw_security_manager.js
Normal file
268
scratch-vm/test/integration/tw_security_manager.js
Normal file
@@ -0,0 +1,268 @@
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const {setupUnsandboxedExtensionAPI} = require('../../src/extension-support/tw-unsandboxed-extension-runner');
|
||||
|
||||
const testProject = fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'tw-project-with-extensions.sb3'));
|
||||
|
||||
// The test project contains two extensions: a fetch one and a bitwise one.
|
||||
const FETCH_EXTENSION = 'https://extensions.turbowarp.org/fetch.js';
|
||||
const BITWISE_EXTENSION = 'https://extensions.turbowarp.org/bitwise.js';
|
||||
|
||||
/* eslint-disable no-script-url */
|
||||
/* eslint-disable require-await */
|
||||
|
||||
test('Deny both extensions', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager.loadExtensionURL = () => {
|
||||
t.fail();
|
||||
};
|
||||
vm.securityManager.canLoadExtensionFromProject = () => false;
|
||||
try {
|
||||
await vm.loadProject(testProject);
|
||||
// loadProject() should fail because extensions were denied
|
||||
t.fail();
|
||||
} catch (e) {
|
||||
t.pass();
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deny 1 of 2 extensions', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager.loadExtensionURL = () => {
|
||||
t.fail();
|
||||
};
|
||||
vm.securityManager.canLoadExtensionFromProject = url => Promise.resolve(url === FETCH_EXTENSION);
|
||||
try {
|
||||
await vm.loadProject(testProject);
|
||||
// loadProject() should fail because extensions were denied
|
||||
t.fail();
|
||||
} catch (e) {
|
||||
t.pass();
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Allow both extensions', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
const loadedExtensions = [];
|
||||
vm.extensionManager.loadExtensionURL = url => {
|
||||
loadedExtensions.push(url);
|
||||
return Promise.resolve();
|
||||
};
|
||||
vm.securityManager.canLoadExtensionFromProject = url => {
|
||||
if (url === FETCH_EXTENSION) {
|
||||
return true;
|
||||
}
|
||||
if (url === BITWISE_EXTENSION) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
t.fail('unknown extension');
|
||||
};
|
||||
await vm.loadProject(testProject);
|
||||
t.same(new Set(loadedExtensions), new Set([FETCH_EXTENSION, BITWISE_EXTENSION]));
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canFetch', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
setupUnsandboxedExtensionAPI(vm);
|
||||
global.location = {
|
||||
href: 'https://example.com/'
|
||||
};
|
||||
|
||||
// data: and blob: are always allowed, shouldn't call security manager
|
||||
vm.securityManager.canFetch = () => t.fail('security manager should be ignored for these protocols');
|
||||
t.equal(await global.Scratch.canFetch('data:text/html,test'), true);
|
||||
t.equal(await global.Scratch.canFetch('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), true);
|
||||
|
||||
vm.securityManager.canFetch = () => false;
|
||||
t.equal(await global.Scratch.canFetch('file:///etc/hosts'), false);
|
||||
t.equal(await global.Scratch.canFetch('http://example.com/'), false);
|
||||
t.equal(await global.Scratch.canFetch('https://example.com/'), false);
|
||||
t.equal(await global.Scratch.canFetch('null'), false);
|
||||
t.equal(await global.Scratch.canFetch(null), false);
|
||||
|
||||
vm.securityManager.canFetch = () => Promise.resolve(false);
|
||||
t.equal(await global.Scratch.canFetch('file:///etc/hosts'), false);
|
||||
t.equal(await global.Scratch.canFetch('http://example.com/'), false);
|
||||
t.equal(await global.Scratch.canFetch('https://example.com/'), false);
|
||||
t.equal(await global.Scratch.canFetch('boring.html'), false);
|
||||
t.equal(await global.Scratch.canFetch('null'), false);
|
||||
t.equal(await global.Scratch.canFetch(null), false);
|
||||
|
||||
vm.securityManager.canFetch = () => true;
|
||||
t.equal(await global.Scratch.canFetch('file:///etc/hosts'), true);
|
||||
t.equal(await global.Scratch.canFetch('http://example.com/'), true);
|
||||
t.equal(await global.Scratch.canFetch('https://example.com/'), true);
|
||||
t.equal(await global.Scratch.canFetch('boring.html'), true);
|
||||
t.equal(await global.Scratch.canFetch('null'), true);
|
||||
t.equal(await global.Scratch.canFetch(null), true);
|
||||
|
||||
const calledWithURLs = [];
|
||||
vm.securityManager.canFetch = async url => {
|
||||
calledWithURLs.push(url);
|
||||
return url === 'https://example.com/null';
|
||||
};
|
||||
t.equal(await global.Scratch.canFetch('file:///etc/hosts'), false);
|
||||
t.equal(await global.Scratch.canFetch('http://example.com/'), false);
|
||||
t.equal(await global.Scratch.canFetch('https://example.com/null'), true);
|
||||
t.equal(await global.Scratch.canFetch('null'), true);
|
||||
t.equal(await global.Scratch.canFetch(null), true);
|
||||
t.same(calledWithURLs, [
|
||||
'file:///etc/hosts',
|
||||
'http://example.com/',
|
||||
'https://example.com/null',
|
||||
'https://example.com/null',
|
||||
'https://example.com/null'
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canOpenWindow', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
setupUnsandboxedExtensionAPI(vm);
|
||||
global.location = {
|
||||
href: 'https://example.com/'
|
||||
};
|
||||
|
||||
// javascript: should never be allowed, shouldn't call security manager
|
||||
vm.securityManager.canOpenWindow = () => t.fail('should not call security manager for javascript:');
|
||||
t.equal(await global.Scratch.canOpenWindow('javascript:alert(1)'), false);
|
||||
|
||||
vm.securityManager.canOpenWindow = () => false;
|
||||
t.equal(await global.Scratch.canOpenWindow('data:text/html,test'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('file:///etc/hosts'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('https://example.com/'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('index.html'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow(null), false);
|
||||
|
||||
vm.securityManager.canOpenWindow = () => Promise.resolve(false);
|
||||
t.equal(await global.Scratch.canOpenWindow('data:text/html,test'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('file:///etc/hosts'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('https://example.com/'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('index.html'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow(null), false);
|
||||
|
||||
vm.securityManager.canOpenWindow = () => true;
|
||||
t.equal(await global.Scratch.canOpenWindow('data:text/html,test'), true);
|
||||
t.equal(await global.Scratch.canOpenWindow('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), true);
|
||||
t.equal(await global.Scratch.canOpenWindow('file:///etc/hosts'), true);
|
||||
t.equal(await global.Scratch.canOpenWindow('https://example.com/'), true);
|
||||
t.equal(await global.Scratch.canOpenWindow('index.html'), true);
|
||||
t.equal(await global.Scratch.canOpenWindow(null), true);
|
||||
|
||||
const calledWithURLs = [];
|
||||
vm.securityManager.canOpenWindow = async url => {
|
||||
calledWithURLs.push(url);
|
||||
return url === 'file:///etc/hosts';
|
||||
};
|
||||
t.equal(await global.Scratch.canOpenWindow('data:text/html,test'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('file:///etc/hosts'), true);
|
||||
t.equal(await global.Scratch.canOpenWindow('https://example.com/'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow('index.html'), false);
|
||||
t.equal(await global.Scratch.canOpenWindow(null), false);
|
||||
t.same(calledWithURLs, [
|
||||
'data:text/html,test',
|
||||
'blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd',
|
||||
'file:///etc/hosts',
|
||||
'https://example.com/',
|
||||
'https://example.com/index.html',
|
||||
'https://example.com/null'
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canRedirect', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
setupUnsandboxedExtensionAPI(vm);
|
||||
global.location = {
|
||||
href: 'https://example.com/'
|
||||
};
|
||||
|
||||
// javascript: should never be allowed, shouldn't call security manager
|
||||
vm.securityManager.canRedirect = () => t.fail('should not call security manager for javascript:');
|
||||
t.equal(await global.Scratch.canRedirect('javascript:alert(1)'), false);
|
||||
|
||||
vm.securityManager.canRedirect = () => false;
|
||||
t.equal(await global.Scratch.canRedirect('data:text/html,test'), false);
|
||||
t.equal(await global.Scratch.canRedirect('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), false);
|
||||
t.equal(await global.Scratch.canRedirect('file:///etc/hosts'), false);
|
||||
t.equal(await global.Scratch.canRedirect('https://example.com/'), false);
|
||||
t.equal(await global.Scratch.canRedirect('index.html'), false);
|
||||
t.equal(await global.Scratch.canRedirect(null), false);
|
||||
|
||||
vm.securityManager.canRedirect = () => Promise.resolve(false);
|
||||
t.equal(await global.Scratch.canRedirect('data:text/html,test'), false);
|
||||
t.equal(await global.Scratch.canRedirect('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), false);
|
||||
t.equal(await global.Scratch.canRedirect('file:///etc/hosts'), false);
|
||||
t.equal(await global.Scratch.canRedirect('https://example.com/'), false);
|
||||
t.equal(await global.Scratch.canRedirect('index.html'), false);
|
||||
t.equal(await global.Scratch.canRedirect(null), false);
|
||||
|
||||
vm.securityManager.canRedirect = () => true;
|
||||
t.equal(await global.Scratch.canRedirect('data:text/html,test'), true);
|
||||
t.equal(await global.Scratch.canRedirect('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), true);
|
||||
t.equal(await global.Scratch.canRedirect('file:///etc/hosts'), true);
|
||||
t.equal(await global.Scratch.canRedirect('https://example.com/'), true);
|
||||
t.equal(await global.Scratch.canRedirect('index.html'), true);
|
||||
t.equal(await global.Scratch.canRedirect(null), true);
|
||||
|
||||
const calledWithURLs = [];
|
||||
vm.securityManager.canRedirect = async url => {
|
||||
calledWithURLs.push(url);
|
||||
return url === 'file:///etc/hosts';
|
||||
};
|
||||
t.equal(await global.Scratch.canRedirect('data:text/html,test'), false);
|
||||
t.equal(await global.Scratch.canRedirect('blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd'), false);
|
||||
t.equal(await global.Scratch.canRedirect('file:///etc/hosts'), true);
|
||||
t.equal(await global.Scratch.canRedirect('https://example.com/'), false);
|
||||
t.equal(await global.Scratch.canRedirect('index.html'), false);
|
||||
t.equal(await global.Scratch.canRedirect(null), false);
|
||||
t.same(calledWithURLs, [
|
||||
'data:text/html,test',
|
||||
'blob:https://example.com/8c071bf8-c0b6-4a48-81d7-6413c2adf3dd',
|
||||
'file:///etc/hosts',
|
||||
'https://example.com/',
|
||||
'https://example.com/index.html',
|
||||
'https://example.com/null'
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canEmbed', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
setupUnsandboxedExtensionAPI(vm);
|
||||
global.location = {
|
||||
href: 'https://example.com/'
|
||||
};
|
||||
|
||||
const calledWithURLs = [];
|
||||
vm.securityManager.canEmbed = async url => {
|
||||
calledWithURLs.push(url);
|
||||
return url === 'https://example.com/ok';
|
||||
};
|
||||
|
||||
t.equal(await global.Scratch.canEmbed('https://example.com/ok'), true);
|
||||
t.equal(await global.Scratch.canEmbed('https://example.com/bad'), false);
|
||||
t.equal(await global.Scratch.canEmbed('file:///etc/hosts'), false);
|
||||
t.equal(await global.Scratch.canEmbed('data:text/html;,<h1>test</h1>'), false);
|
||||
t.equal(await global.Scratch.canEmbed('ok'), true);
|
||||
t.same(calledWithURLs, [
|
||||
'https://example.com/ok',
|
||||
'https://example.com/bad',
|
||||
'file:///etc/hosts',
|
||||
'data:text/html;,<h1>test</h1>',
|
||||
'https://example.com/ok'
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
98
scratch-vm/test/integration/tw_serialize_asset_order.js
Normal file
98
scratch-vm/test/integration/tw_serialize_asset_order.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {test} = require('tap');
|
||||
const JSZip = require('@turbowarp/jszip');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
|
||||
const fixture = fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'tw-serialize-asset-order.sb3'));
|
||||
|
||||
test('serializeAssets serialization order', t => {
|
||||
t.plan(15);
|
||||
const vm = new VM();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
vm.loadProject(fixture).then(() => {
|
||||
const assets = vm.serializeAssets();
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
// won't deduplicate assets, so expecting 8 costumes, 7 sounds
|
||||
// 8 costumes, 6 sounds
|
||||
if (i < 8) {
|
||||
t.ok(assets[i].fileName.endsWith('.svg'), `file ${i + 1} is costume`);
|
||||
} else {
|
||||
t.ok(assets[i].fileName.endsWith('.wav'), `file ${i + 1} is sound`);
|
||||
}
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('saveProjectSb3 serialization order', t => {
|
||||
t.plan(13);
|
||||
const vm = new VM();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
vm.loadProject(fixture).then(() => {
|
||||
vm.saveProjectSb3('arraybuffer').then(serialized => {
|
||||
JSZip.loadAsync(serialized).then(zip => {
|
||||
const files = Object.keys(zip.files);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
// 6 costumes, 6 sounds
|
||||
if (i === 0) {
|
||||
t.equal(files[i], 'project.json', 'first file is project.json');
|
||||
} else if (i < 7) {
|
||||
t.ok(files[i].endsWith('.svg'), `file ${i + 1} is costume`);
|
||||
} else {
|
||||
t.ok(files[i].endsWith('.wav'), `file ${i + 1} is sound`);
|
||||
}
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('exportSprite serialization order', t => {
|
||||
t.plan(9);
|
||||
const vm = new VM();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
vm.loadProject(fixture).then(() => {
|
||||
vm.exportSprite(vm.runtime.targets[1].id, 'arraybuffer').then(serialized => {
|
||||
JSZip.loadAsync(serialized).then(zip => {
|
||||
const files = Object.keys(zip.files);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
// 4 costumes, 4 sounds
|
||||
if (i === 0) {
|
||||
t.equal(files[i], 'sprite.json', 'first file is sprite.json');
|
||||
} else if (i < 5) {
|
||||
t.ok(files[i].endsWith('.svg'), `file ${i + 1} is costume`);
|
||||
} else {
|
||||
t.ok(files[i].endsWith('.wav'), `file ${i + 1} is sound`);
|
||||
}
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('saveProjectSb3DontZip', t => {
|
||||
t.plan(13);
|
||||
const vm = new VM();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
vm.loadProject(fixture).then(() => {
|
||||
const exported = vm.saveProjectSb3DontZip();
|
||||
const files = Object.keys(exported);
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
// 6 costumes, 6 sounds
|
||||
if (i === 0) {
|
||||
t.equal(files[i], 'project.json', 'first file is project.json');
|
||||
} else if (i < 7) {
|
||||
t.ok(files[i].endsWith('.svg'), `file ${i + 1} is costume`);
|
||||
} else {
|
||||
t.ok(files[i].endsWith('.wav'), `file ${i + 1} is sound`);
|
||||
}
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
const {test} = require('tap');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
|
||||
test('Correctly serializes native extension only used by monitors', t => {
|
||||
const vm = new VM();
|
||||
vm.runtime.requestAddMonitor(MonitorRecord({
|
||||
id: 'fakeblock1',
|
||||
opcode: 'pen_fakeblock',
|
||||
visiblle: true
|
||||
}));
|
||||
vm.runtime.requestAddMonitor(MonitorRecord({
|
||||
id: 'fakeblock2',
|
||||
opcode: 'translate_fakeblock',
|
||||
visiblle: false
|
||||
}));
|
||||
const json = JSON.parse(vm.toJSON());
|
||||
t.same(json.extensions, ['pen', 'translate']);
|
||||
t.not('customExtensions' in json);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Correctly serializes custom extension only used by monitors', t => {
|
||||
const vm = new VM();
|
||||
vm.runtime.requestAddMonitor(MonitorRecord({
|
||||
id: 'fakeblock1',
|
||||
opcode: 'fetch_fakeblock',
|
||||
visible: true
|
||||
}));
|
||||
vm.runtime.requestAddMonitor(MonitorRecord({
|
||||
// should not be serialized at all
|
||||
id: 'fakeblock2',
|
||||
opcode: 'bitwise_fakeblock',
|
||||
visible: false
|
||||
}));
|
||||
vm.extensionManager.getExtensionURLs = () => ({
|
||||
fetch: 'https://extensions.turbowarp.org/fetch.js'
|
||||
});
|
||||
const json = JSON.parse(vm.toJSON());
|
||||
t.same(json.extensions, ['fetch']);
|
||||
t.same(json.extensionURLs, {
|
||||
fetch: 'https://extensions.turbowarp.org/fetch.js'
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
48
scratch-vm/test/integration/tw_serialize_extensions.js
Normal file
48
scratch-vm/test/integration/tw_serialize_extensions.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
|
||||
test('Serializes custom extensions', t => {
|
||||
t.plan(6);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
// Trick the extension manager into thinking a couple extensions are loaded.
|
||||
vm.extensionManager.workerURLs[0] = 'https://example.com/test1.js';
|
||||
vm.extensionManager.workerURLs[1] = 'https://example.com/test2.js';
|
||||
// First number in the service names corresponds to index in workerURLs
|
||||
vm.extensionManager._loadedExtensions.set('test1', 'test.0.0');
|
||||
vm.extensionManager._loadedExtensions.set('test2', 'test.1.0');
|
||||
|
||||
const targetUsingBlock = new RenderedTarget(new Sprite(null, vm.runtime), vm.runtime);
|
||||
vm.runtime.addTarget(targetUsingBlock);
|
||||
targetUsingBlock.blocks.createBlock({
|
||||
id: 'a',
|
||||
opcode: 'test1_something'
|
||||
});
|
||||
|
||||
const targetNotUsingBlock = new RenderedTarget(new Sprite(null, vm.runtime), vm.runtime);
|
||||
vm.runtime.addTarget(targetNotUsingBlock);
|
||||
|
||||
// test2 isn't used, so it shouldn't be included in the JSON
|
||||
|
||||
const serializedProject = JSON.parse(vm.toJSON());
|
||||
t.same(serializedProject.extensions, ['test1'], 'save extension IDs for project');
|
||||
t.same(serializedProject.extensionURLs, {
|
||||
test1: 'https://example.com/test1.js'
|
||||
}, 'save extension URLs for project');
|
||||
|
||||
const serializedTargetWithBlock = JSON.parse(vm.toJSON(targetUsingBlock.id));
|
||||
t.same(serializedTargetWithBlock.extensions, ['test1'], 'save extension IDs for sprite');
|
||||
t.same(serializedTargetWithBlock.extensionURLs, {
|
||||
test1: 'https://example.com/test1.js'
|
||||
}, 'save extension URLs for sprite');
|
||||
|
||||
// other sprite uses no extensions, so don't want extension stuff in the JSON
|
||||
const serializedTargetWithoutBlock = JSON.parse(vm.toJSON(targetNotUsingBlock.id));
|
||||
t.notOk('extensions' in serializedTargetWithoutBlock, 'dont save extension IDs for empty sprite');
|
||||
t.notOk('extensionURLs' in serializedTargetWithoutBlock, 'dont save extension URLs for empty sprite');
|
||||
|
||||
t.end();
|
||||
});
|
||||
31
scratch-vm/test/integration/tw_serialize_hidden_monitors.js
Normal file
31
scratch-vm/test/integration/tw_serialize_hidden_monitors.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const {test} = require('tap');
|
||||
const {serialize} = require('../../src/serialization/sb3');
|
||||
const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
|
||||
test('does not serialize hidden monitors from extensions', t => {
|
||||
const rt = new Runtime();
|
||||
rt.requestAddMonitor(MonitorRecord({
|
||||
id: 'timer',
|
||||
opcode: 'sensing_timer',
|
||||
visible: true
|
||||
}));
|
||||
rt.requestAddMonitor(MonitorRecord({
|
||||
id: 'other_monitor',
|
||||
opcode: 'tw_someOpcodeThatIsntPartOfACoreExtension',
|
||||
visible: true
|
||||
}));
|
||||
|
||||
const monitorsWhenVisible = serialize(rt).monitors;
|
||||
t.ok(monitorsWhenVisible[0].id === 'timer');
|
||||
t.ok(monitorsWhenVisible[1].id === 'other_monitor');
|
||||
t.equal(monitorsWhenVisible.length, 2);
|
||||
|
||||
rt.requestHideMonitor('timer');
|
||||
rt.requestHideMonitor('other_monitor');
|
||||
const monitorsWhenHidden = serialize(rt).monitors;
|
||||
t.ok(monitorsWhenHidden[0].id === 'timer');
|
||||
t.equal(monitorsWhenHidden.length, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
29
scratch-vm/test/integration/tw_stackframe_op.js
Normal file
29
scratch-vm/test/integration/tw_stackframe_op.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
const projectPath = path.join(__dirname, '..', 'fixtures', 'tw-stackframe-op.sb3');
|
||||
|
||||
test('util.thread.peekStackFrame().op', t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
vm.runtime.setCompilerOptions({
|
||||
enabled: false
|
||||
});
|
||||
|
||||
// Easiest way to test this is to overwrite an existing block
|
||||
vm.runtime._primitives.operator_add = function (args, util) {
|
||||
const op = util.thread.peekStackFrame().op;
|
||||
t.equal(op.id, 'c');
|
||||
t.end();
|
||||
|
||||
// return value not used
|
||||
return 4;
|
||||
};
|
||||
|
||||
vm.loadProject(fs.readFileSync(projectPath)).then(() => {
|
||||
vm.greenFlag();
|
||||
vm.runtime._step();
|
||||
});
|
||||
});
|
||||
116
scratch-vm/test/integration/tw_standalone_blocks.js
Normal file
116
scratch-vm/test/integration/tw_standalone_blocks.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
|
||||
test('Serializes standalone blocks', t => {
|
||||
t.plan(4);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
vm.extensionManager.workerURLs[0] = 'https://example.com/test1.js';
|
||||
vm.extensionManager.workerURLs[1] = 'https://example.com/test2.js';
|
||||
vm.extensionManager._loadedExtensions.set('test1', 'test.0.0');
|
||||
vm.extensionManager._loadedExtensions.set('test2', 'test.1.0');
|
||||
|
||||
const primitiveBlock = {
|
||||
id: 'donotcompress1',
|
||||
opcode: 'control_if'
|
||||
};
|
||||
const extensionBlock1 = {
|
||||
id: 'donotcompress2',
|
||||
opcode: 'test1_something'
|
||||
};
|
||||
const extensionBlock2 = {
|
||||
id: 'donotcompress3',
|
||||
opcode: 'test2_something'
|
||||
};
|
||||
|
||||
t.same(vm.exportStandaloneBlocks([]), []);
|
||||
t.same(vm.exportStandaloneBlocks([primitiveBlock]), [primitiveBlock]);
|
||||
t.same(vm.exportStandaloneBlocks([extensionBlock1, extensionBlock2]), {
|
||||
blocks: [extensionBlock1, extensionBlock2],
|
||||
extensionURLs: {
|
||||
test1: 'https://example.com/test1.js',
|
||||
test2: 'https://example.com/test2.js'
|
||||
}
|
||||
});
|
||||
t.same(vm.exportStandaloneBlocks([primitiveBlock, extensionBlock2]), {
|
||||
blocks: [primitiveBlock, extensionBlock2],
|
||||
extensionURLs: {
|
||||
test2: 'https://example.com/test2.js'
|
||||
}
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deserializes vanilla standalone blocks', t => {
|
||||
t.plan(2);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
const target = new RenderedTarget(new Sprite(null, vm.runtime), vm.runtime);
|
||||
vm.runtime.addTarget(target);
|
||||
|
||||
vm.shareBlocksToTarget([
|
||||
{
|
||||
id: 'abcdef',
|
||||
opcode: 'control_if'
|
||||
}
|
||||
], target.id).then(() => {
|
||||
const createdBlock = Object.values(target.sprite.blocks._blocks)[0];
|
||||
t.equal(createdBlock.opcode, 'control_if');
|
||||
t.not(createdBlock.id, 'abcdef', 'opcode changed');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Deserializes standalone blocks with extensions', t => {
|
||||
t.plan(3);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
const target = new RenderedTarget(new Sprite(null, vm.runtime), vm.runtime);
|
||||
vm.runtime.addTarget(target);
|
||||
|
||||
const events = [];
|
||||
vm.securityManager.canLoadExtensionFromProject = url => {
|
||||
events.push(`canLoadExtensionFromProject ${url}`);
|
||||
return true;
|
||||
};
|
||||
vm.extensionManager.loadExtensionURL = url => {
|
||||
events.push(`loadExtensionURL ${url}`);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
vm.shareBlocksToTarget({
|
||||
blocks: [
|
||||
{
|
||||
id: 'fruit',
|
||||
opcode: 'pen_clear'
|
||||
},
|
||||
{
|
||||
id: 'vegetable',
|
||||
opcode: 'test1_something'
|
||||
}
|
||||
],
|
||||
extensionURLs: {
|
||||
test1: 'https://example.com/test1.js',
|
||||
test2: 'https://example.com/should.be.discarded.js',
|
||||
pen: 'https://example.com/should.also.be.discarded.js'
|
||||
}
|
||||
}, target.id).then(() => {
|
||||
t.same(events, [
|
||||
'canLoadExtensionFromProject https://example.com/test1.js',
|
||||
'loadExtensionURL https://example.com/test1.js'
|
||||
]);
|
||||
|
||||
const penBlock = Object.values(target.sprite.blocks._blocks).find(i => i.opcode === 'pen_clear');
|
||||
t.not(penBlock.id, 'fruit', 'changed pen block id');
|
||||
|
||||
const extensionBlock = Object.values(target.sprite.blocks._blocks).find(i => i.opcode === 'test1_something');
|
||||
t.not(extensionBlock.id, 'vegetable', 'changed extension block id');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
24
scratch-vm/test/integration/tw_step_events.js
Normal file
24
scratch-vm/test/integration/tw_step_events.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const {test} = require('tap');
|
||||
|
||||
test('step events', t => {
|
||||
const events = [];
|
||||
const rt = new Runtime();
|
||||
rt.sequencer.stepThreads = () => {
|
||||
events.push('sequencer.stepThreads()');
|
||||
return [];
|
||||
};
|
||||
rt.on('BEFORE_EXECUTE', () => {
|
||||
events.push('BEFORE_EXECUTE');
|
||||
});
|
||||
rt.on('AFTER_EXECUTE', () => {
|
||||
events.push('AFTER_EXECUTE');
|
||||
});
|
||||
rt._step();
|
||||
t.same(events, [
|
||||
'BEFORE_EXECUTE',
|
||||
'sequencer.stepThreads()',
|
||||
'AFTER_EXECUTE'
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
82
scratch-vm/test/integration/tw_very_long_comments.js
Normal file
82
scratch-vm/test/integration/tw_very_long_comments.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const {test} = require('tap');
|
||||
const fs = require('fs');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const sb3 = require('../../src/serialization/sb3');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const path = require('path');
|
||||
|
||||
test('serializes very long comments', t => {
|
||||
const rt = new Runtime();
|
||||
const sprite = new Sprite();
|
||||
const target = new RenderedTarget(sprite);
|
||||
rt.addTarget(target);
|
||||
|
||||
target.createComment('id_short', null, 'short comment', 0, 0, 20, 20, false);
|
||||
target.createComment('id_max_length', null, `start${'0'.repeat(8000 - 5)}`, 0, 0, 20, 20, false);
|
||||
target.createComment('id_max_length_plus_1', null, `start${'0'.repeat(8000 - 5)}a`, 0, 0, 20, 20, false);
|
||||
target.createComment(
|
||||
'id_way_too_long',
|
||||
null,
|
||||
`start${'0'.repeat(8000 - 5 - 3)}endthis should be the truncated part${'0'.repeat(25000)}`,
|
||||
0,
|
||||
0,
|
||||
20,
|
||||
20,
|
||||
false
|
||||
);
|
||||
|
||||
const serialized = sb3.serialize(rt, target.id);
|
||||
const common = {
|
||||
// same for every block, already tested elsewhere, not interesting for here
|
||||
blockId: null,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 20,
|
||||
height: 20,
|
||||
minimized: false
|
||||
};
|
||||
t.same(serialized.comments, {
|
||||
id_short: {
|
||||
...common,
|
||||
text: 'short comment'
|
||||
},
|
||||
id_max_length: {
|
||||
...common,
|
||||
text: `start${'0'.repeat(8000 - 5)}`
|
||||
},
|
||||
id_max_length_plus_1: {
|
||||
...common,
|
||||
text: `start${'0'.repeat(8000 - 5)}`,
|
||||
extraText: 'a'
|
||||
},
|
||||
id_way_too_long: {
|
||||
...common,
|
||||
text: `start${'0'.repeat(8000 - 5 - 3)}end`,
|
||||
extraText: `this should be the truncated part${'0'.repeat(25000)}`
|
||||
}
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deserializes very long comments', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const fixture = fs.readFileSync(path.resolve(__dirname, '../fixtures/tw-very-long-comments.sb3'));
|
||||
vm.loadProject(fixture).then(() => {
|
||||
const comments = vm.runtime.targets[0].comments;
|
||||
|
||||
// note that comment IDs may change each time the test project is saved
|
||||
t.equal(comments.b.text, '');
|
||||
t.equal(comments.a.text, 'short');
|
||||
t.equal(comments.c.text, `exactly length limit${'0'.repeat(8000 - 'exactly length limit'.length)}`);
|
||||
t.equal(comments.d.text, `length limit + 1${':'.repeat(8000 - 'length limit + 1'.length)}1`);
|
||||
t.equal(
|
||||
comments.e.text,
|
||||
`unreasonably long${'!'.repeat(8000 - 'unreasonably long'.length)}${'123456789'.repeat(2000)}`
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
38
scratch-vm/test/integration/tw_xml_block.js
Normal file
38
scratch-vm/test/integration/tw_xml_block.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const htmlparser = require('htmlparser2');
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
test('XML blocks', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testxml',
|
||||
name: 'XML Test',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.XML,
|
||||
xml: '<test it="works">!</test>'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const xmlList = vm.runtime.getBlocksXML();
|
||||
t.equal(xmlList.length, 1);
|
||||
|
||||
const parsedXML = htmlparser.parseDOM(xmlList[0].xml);
|
||||
t.equal(parsedXML.length, 1);
|
||||
|
||||
const category = parsedXML[0];
|
||||
t.equal(category.name, 'category');
|
||||
t.equal(category.children.length, 1);
|
||||
|
||||
const label = category.children[0];
|
||||
t.equal(label.name, 'test');
|
||||
t.equal(label.attribs.it, 'works');
|
||||
t.equal(label.children.length, 1);
|
||||
t.equal(label.children[0].data, '!');
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/unknown-opcode-as-reporter-block.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('unknown opcode', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// The project has 4 blocks in a single stack:
|
||||
// when green flag
|
||||
// if "unknown block"
|
||||
// set volume to "unknown block"
|
||||
// play sound "unknown block"
|
||||
// the "unknown block" has unknown opcode and was created by
|
||||
// dragging a discontinued extension.
|
||||
// It should be parsed in without error and a shadow block
|
||||
// should be created where appropriate.
|
||||
const blocks = vm.runtime.targets[0].blocks;
|
||||
const topBlockId = blocks.getScripts()[0];
|
||||
const secondBlockId = blocks.getNextBlock(topBlockId);
|
||||
const thirdBlockId = blocks.getNextBlock(secondBlockId);
|
||||
const fourthBlockId = blocks.getNextBlock(thirdBlockId);
|
||||
|
||||
t.equal(blocks.getBlock(topBlockId).opcode, 'event_whenflagclicked');
|
||||
t.equal(blocks.getBlock(secondBlockId).opcode, 'control_wait_until');
|
||||
t.equal(blocks.getBlock(thirdBlockId).opcode, 'sound_setvolumeto');
|
||||
t.equal(blocks.getBlock(fourthBlockId).opcode, 'sound_play');
|
||||
|
||||
const secondBlockInputId = blocks.getBlock(secondBlockId).inputs.CONDITION.block;
|
||||
const thirdBlockInputId = blocks.getBlock(thirdBlockId).inputs.VOLUME.block;
|
||||
const fourthBlockInputId = blocks.getBlock(fourthBlockId).inputs.SOUND_MENU.block;
|
||||
|
||||
t.equal(secondBlockInputId, null);
|
||||
t.true(blocks.getBlock(thirdBlockInputId).shadow);
|
||||
t.equal(blocks.getBlock(thirdBlockInputId).opcode, 'math_number');
|
||||
t.true(blocks.getBlock(fourthBlockInputId).shadow);
|
||||
t.equal(blocks.getBlock(fourthBlockInputId).opcode, 'sound_sounds_menu');
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
39
scratch-vm/test/integration/unknown-opcode-in-c-block.js
Normal file
39
scratch-vm/test/integration/unknown-opcode-in-c-block.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/unknown-opcode-in-c-block.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('unknown opcode', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// The project has 3 blocks in a single stack:
|
||||
// when green flag => forever [ => "undefined"]
|
||||
// the "undefined" block has opcode "foo" and was created by dragging
|
||||
// a custom procedure caller named foo from the backpack into a project.
|
||||
// It should be parsed in without error and it should
|
||||
// leave the forever block empty.
|
||||
const blocks = vm.runtime.targets[1].blocks;
|
||||
const topBlockId = blocks.getScripts()[0];
|
||||
const secondBlockId = blocks.getNextBlock(topBlockId);
|
||||
const innerBlockId = blocks.getBranch(secondBlockId, 0);
|
||||
|
||||
t.equal(blocks.getBlock(topBlockId).opcode, 'event_whenflagclicked');
|
||||
t.equal(blocks.getBlock(secondBlockId).opcode, 'control_forever');
|
||||
t.equal(innerBlockId, null);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user