Merge origin/develop and adapt the specialized compiler
This commit is contained in:
486 files changed
+14097
-12257
No files matched your search
@@ -21,6 +21,7 @@
|
||||
*.vert text eol=lf
|
||||
*.xml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.tw-snapshot text eol=lf
|
||||
|
||||
# Prefer LF for these files
|
||||
.editorconfig text eol=lf
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Deploy playground
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [develop]
|
||||
|
||||
concurrency:
|
||||
group: "deploy"
|
||||
|
||||
@@ -19,4 +19,4 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
- run: npm run tap -- --timeout=60
|
||||
- run: npm run test:node
|
||||
Generated
+11
-3220
File diff suppressed because it is too large.
Load diff
+4
-12
@@ -16,26 +16,19 @@
|
||||
"browser": "./src/index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --progress --colors --bail",
|
||||
"coverage": "tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
|
||||
"docs": "jsdoc -c .jsdoc.json",
|
||||
"i18n:src": "mkdirp translations/core && format-message extract --out-file translations/core/en.json src/extensions/**/index.js",
|
||||
"i18n:push": "tx-push-src scratch-editor extensions translations/core/en.json",
|
||||
"lint": "eslint .",
|
||||
"prepublish": "in-publish && npm run build || not-in-publish",
|
||||
"start": "webpack-dev-server",
|
||||
"tap": "tap ./test/{unit,integration}/*.js",
|
||||
"tap:unit": "tap ./test/unit/*.js",
|
||||
"tap:integration": "tap ./test/integration/*.js",
|
||||
"test": "npm run lint && npm run tap",
|
||||
"test": "npm run lint && npm run test:node",
|
||||
"test:node": "node --test --test-force-exit --test-timeout=30000 test/unit/*.js test/integration/*.js",
|
||||
"test:unit": "node --test --test-force-exit --test-timeout=30000 test/unit/*.js",
|
||||
"test:integration": "node --test --test-force-exit --test-timeout=30000 test/integration/*.js",
|
||||
"watch": "webpack --progress --colors --watch",
|
||||
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
||||
},
|
||||
"tap": {
|
||||
"branches": 60,
|
||||
"functions": 70,
|
||||
"lines": 70,
|
||||
"statements": 70
|
||||
},
|
||||
"dependencies": {
|
||||
"@turbowarp/json": "^0.1.2",
|
||||
"@turbowarp/jszip": "^3.11.0",
|
||||
@@ -87,7 +80,6 @@
|
||||
"scratch-storage": "2.3.1",
|
||||
"script-loader": "0.7.2",
|
||||
"stats.js": "0.17.0",
|
||||
"tap": "16.2.0",
|
||||
"tiny-worker": "2.3.0",
|
||||
"uglifyjs-webpack-plugin": "1.2.7",
|
||||
"webpack": "4.47.0",
|
||||
|
||||
@@ -36,6 +36,7 @@ const compile = (/** @type {import("../engine/thread")} */ thread) => {
|
||||
|
||||
return {
|
||||
startingFunction: entry,
|
||||
yields: ir.entry.yields,
|
||||
procedures,
|
||||
executableHat: ir.entry.executableHat
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ class ContinuationGenerator extends JSGenerator {
|
||||
effects: {
|
||||
color: 0, fisheye: 0, whirl: 0, pixelate: 0, mosaic: 0, brightness: 0, ghost: 0
|
||||
},
|
||||
runtime: {debug: false}});
|
||||
runtime: {debug: false, sampler: {enableCommands: false}}});
|
||||
this.program = program;
|
||||
this.targetIndex = target.index;
|
||||
this.procedureEntries = procedures;
|
||||
|
||||
@@ -36,6 +36,12 @@ class IntermediateStackBlock {
|
||||
*/
|
||||
this.yields = yields;
|
||||
|
||||
/**
|
||||
* ID of the Scratch block this was generated from, if any.
|
||||
* @type {string?}
|
||||
*/
|
||||
this.blockId = null;
|
||||
|
||||
/**
|
||||
* Should state changes made by this stack block be ignored? Used for testing.
|
||||
* @type {boolean}
|
||||
@@ -174,7 +180,7 @@ class IntermediateInput {
|
||||
case InputOpcode.CAST_NUMBER:
|
||||
case InputOpcode.CAST_NUMBER_INDEX:
|
||||
case InputOpcode.CAST_NUMBER_OR_NAN: {
|
||||
if (this.isAlwaysType(InputType.BOOLEAN_INTERPRETABLE)) {
|
||||
if (this.isAlwaysType(InputType.BOOLEAN)) {
|
||||
this.type = InputType.NUMBER;
|
||||
this.inputs.value = +Cast.toBoolean(this.inputs.value);
|
||||
}
|
||||
@@ -317,7 +323,7 @@ class IntermediateScript {
|
||||
* If true, this script will be compiled as a generator function (function*)
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.yields = true;
|
||||
this.yields = false;
|
||||
|
||||
/**
|
||||
* Whether this script should use the "warp timer"
|
||||
|
||||
+14
-10
@@ -111,7 +111,6 @@ class ScriptTreeGenerator {
|
||||
this.script.procedureVariant = procedureVariant;
|
||||
this.script.procedureCode = procedureCode;
|
||||
this.script.isProcedure = true;
|
||||
this.script.yields = false;
|
||||
|
||||
const paramNamesIdsAndDefaults = this.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode);
|
||||
if (paramNamesIdsAndDefaults === null) {
|
||||
@@ -225,7 +224,7 @@ class ScriptTreeGenerator {
|
||||
case 'text':
|
||||
return this.createConstantInput(block.fields.TEXT.value, preserveStrings);
|
||||
case 'argument_reporter_string_number': {
|
||||
const name = block.fields.VALUE.value;
|
||||
const name = String(block.fields.VALUE.value);
|
||||
// lastIndexOf because multiple parameters with the same name will use the value of the last definition
|
||||
const index = this.script.arguments.lastIndexOf(name);
|
||||
if (index === -1) {
|
||||
@@ -241,7 +240,7 @@ class ScriptTreeGenerator {
|
||||
}
|
||||
case 'argument_reporter_boolean': {
|
||||
// see argument_reporter_string_number above
|
||||
const name = block.fields.VALUE.value;
|
||||
const name = String(block.fields.VALUE.value);
|
||||
const index = this.script.arguments.lastIndexOf(name);
|
||||
if (index === -1) {
|
||||
if (name.toLowerCase() === 'is compiled?' || name.toLowerCase() === 'is turbowarp?') {
|
||||
@@ -629,7 +628,7 @@ class ScriptTreeGenerator {
|
||||
target: this.descendInputOfBlock(block, 'CLONE_OPTION').toType(InputType.STRING)
|
||||
});
|
||||
case 'control_delete_this_clone':
|
||||
return new IntermediateStackBlock(StackOpcode.CONTROL_CLONE_DELETE, {}, true);
|
||||
return new IntermediateStackBlock(StackOpcode.CONTROL_CLONE_DELETE);
|
||||
case 'control_forever':
|
||||
return new IntermediateStackBlock(StackOpcode.CONTROL_WHILE, {
|
||||
condition: this.createConstantInput(true).toType(InputType.BOOLEAN),
|
||||
@@ -675,7 +674,7 @@ class ScriptTreeGenerator {
|
||||
case 'control_stop': {
|
||||
const level = block.fields.STOP_OPTION.value;
|
||||
if (level === 'all') {
|
||||
return new IntermediateStackBlock(StackOpcode.CONTROL_STOP_ALL, {}, true);
|
||||
return new IntermediateStackBlock(StackOpcode.CONTROL_STOP_ALL);
|
||||
} else if (level === 'other scripts in sprite' || level === 'other scripts in stage') {
|
||||
return new IntermediateStackBlock(StackOpcode.CONTROL_STOP_OTHERS);
|
||||
} else if (level === 'this script') {
|
||||
@@ -1019,6 +1018,8 @@ class ScriptTreeGenerator {
|
||||
}
|
||||
|
||||
const node = this.descendStackedBlock(block);
|
||||
node.blockId = blockId;
|
||||
|
||||
this.script.yields = this.script.yields || node.yields;
|
||||
result.blocks.push(node);
|
||||
|
||||
@@ -1126,9 +1127,10 @@ class ScriptTreeGenerator {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const input = this.descendInput(block);
|
||||
return new IntermediateStackBlock(StackOpcode.VISUAL_REPORT, {
|
||||
input: this.descendInput(block)
|
||||
});
|
||||
input
|
||||
}, input.yields);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
@@ -1225,7 +1227,7 @@ class ScriptTreeGenerator {
|
||||
}
|
||||
|
||||
// Create it locally...
|
||||
const newVariable = new Variable(id, name, type, false);
|
||||
const newVariable = Variable.create(id, name, type, false);
|
||||
|
||||
// Intentionally not using newVariable.id so that this matches vanilla Scratch quirks regarding
|
||||
// handling of null variable IDs.
|
||||
@@ -1237,7 +1239,7 @@ class ScriptTreeGenerator {
|
||||
// sprite.clones has all instances of this sprite including the original and all clones
|
||||
for (const clone of target.sprite.clones) {
|
||||
if (!Object.prototype.hasOwnProperty.call(clone.variables, id)) {
|
||||
clone.variables[String(id)] = new Variable(id, name, type, false);
|
||||
clone.variables[String(id)] = Variable.createSibling(newVariable, id, name, type, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1366,8 +1368,10 @@ class ScriptTreeGenerator {
|
||||
// interpreter parity, but the reuslt is ignored.
|
||||
const opcodeFunction = this.runtime.getOpcodeFunction(opcode);
|
||||
if (opcodeFunction) {
|
||||
const hat = this.descendCompatLayerStack(hatBlock);
|
||||
this.script.yields = this.script.yields || hat.yields;
|
||||
return new IntermediateStack([
|
||||
this.descendCompatLayerStack(hatBlock),
|
||||
hat,
|
||||
...this.walkStack(nextBlock).blocks
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -160,31 +160,31 @@ class IROptimizer {
|
||||
|
||||
case InputOpcode.CAST_BOOLEAN: {
|
||||
const innerType = inputs.target.type;
|
||||
if (innerType & InputType.BOOLEAN) return innerType;
|
||||
if ((innerType & InputType.BOOLEAN) === innerType) return innerType;
|
||||
return InputType.BOOLEAN;
|
||||
}
|
||||
|
||||
case InputOpcode.CAST_NUMBER: {
|
||||
const innerType = inputs.target.type;
|
||||
if (innerType & InputType.NUMBER) return innerType;
|
||||
if ((innerType & InputType.NUMBER) === innerType) return innerType;
|
||||
return InputType.NUMBER;
|
||||
}
|
||||
|
||||
case InputOpcode.CAST_NUMBER_INDEX: {
|
||||
const innerType = inputs.target.type;
|
||||
if (innerType & InputType.NUMBER_INDEX) return innerType;
|
||||
if ((innerType & InputType.NUMBER_INDEX) === innerType) return innerType;
|
||||
return InputType.NUMBER_INDEX;
|
||||
}
|
||||
|
||||
case InputOpcode.CAST_NUMBER_OR_NAN: {
|
||||
const innerType = inputs.target.type;
|
||||
if (innerType & InputType.NUMBER_OR_NAN) return innerType;
|
||||
if ((innerType & InputType.NUMBER_OR_NAN) === innerType) return innerType;
|
||||
return InputType.NUMBER_OR_NAN;
|
||||
}
|
||||
|
||||
case InputOpcode.CAST_STRING: {
|
||||
const innerType = inputs.target.type;
|
||||
if (innerType & InputType.STRING) return innerType;
|
||||
if ((innerType & InputType.STRING) === innerType) return innerType;
|
||||
return InputType.STRING;
|
||||
}
|
||||
|
||||
@@ -594,7 +594,8 @@ class IROptimizer {
|
||||
modified = state.or(trueState) || modified;
|
||||
break;
|
||||
}
|
||||
case StackOpcode.CONTROL_STOP_SCRIPT: {
|
||||
case StackOpcode.CONTROL_STOP_SCRIPT:
|
||||
case StackOpcode.PROCEDURE_RETURN: {
|
||||
modified = this.analyzeInputs(inputs, state) || modified;
|
||||
this.addPossibleExitState(state);
|
||||
break;
|
||||
@@ -797,10 +798,10 @@ class IROptimizer {
|
||||
*/
|
||||
optimizeScript (script, alreadyOptimized) {
|
||||
if (script.isProcedure) {
|
||||
if (alreadyOptimized.has(script.procedureCode)) {
|
||||
if (alreadyOptimized.has(script.procedureVariant)) {
|
||||
return;
|
||||
}
|
||||
alreadyOptimized.add(script.procedureCode);
|
||||
alreadyOptimized.add(script.procedureVariant);
|
||||
}
|
||||
|
||||
for (const procVariant of script.dependedProcedures) {
|
||||
|
||||
+87
-45
@@ -8,6 +8,13 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
/* eslint-disable max-len */
|
||||
|
||||
/**
|
||||
* Magic value thrown when a procedure needs to end the whole thread's execution.
|
||||
* Must be a number because this is inlined directly into compiled JS.
|
||||
* @type {number}
|
||||
*/
|
||||
const RETIRE_MAGIC = 1;
|
||||
|
||||
const globalState = {
|
||||
Timer: require('../util/timer'),
|
||||
Cast: require('../util/cast'),
|
||||
@@ -223,15 +230,6 @@ runtimeFunctions.retire = `const retire = () => {
|
||||
thread.target.runtime.sequencer.retireThread(thread);
|
||||
}`;
|
||||
|
||||
/**
|
||||
* Converts NaN to zero. Used to match Scratch's string-to-number.
|
||||
* Unlike (x || 0), -0 stays as -0 and is not converted to 0.
|
||||
* This function needs to be written such that it's very easy for browsers to inline it.
|
||||
* @param {number} value A number. Might be NaN.
|
||||
* @returns {number} A number. Never NaN.
|
||||
*/
|
||||
runtimeFunctions.toNotNaN = `const toNotNaN = value => Number.isNaN(value) ? 0 : value`;
|
||||
|
||||
/**
|
||||
* Scratch cast to boolean.
|
||||
* Similar to Cast.toBoolean()
|
||||
@@ -272,7 +270,7 @@ baseRuntime += `const isNotActuallyZero = val => {
|
||||
if (typeof val !== 'string') return false;
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
const code = val.charCodeAt(i);
|
||||
if (code === 48 || code === 9) {
|
||||
if ((code >= 48 && code <= 57) || code === 9) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -287,12 +285,44 @@ baseRuntime += `const isNotActuallyZero = val => {
|
||||
*/
|
||||
baseRuntime += `const compareEqualSlow = (v1, v2) => {
|
||||
const n1 = +v1;
|
||||
if (Number.isNaN(n1) || (n1 === 0 && isNotActuallyZero(v1))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
|
||||
if (n1 !== n1 || (n1 === 0 && isNotActuallyZero(v1))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
|
||||
const n2 = +v2;
|
||||
if (Number.isNaN(n2) || (n2 === 0 && isNotActuallyZero(v2))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
|
||||
if (n2 !== n2 || (n2 === 0 && isNotActuallyZero(v2))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
|
||||
return n1 === n2;
|
||||
};
|
||||
const compareEqual = (v1, v2) => (typeof v1 === 'number' && typeof v2 === 'number' && !Number.isNaN(v1) && !Number.isNaN(v2) || v1 === v2) ? v1 === v2 : compareEqualSlow(v1, v2);`;
|
||||
const compareEqual = (v1, v2) => (typeof v1 === 'number' && typeof v2 === 'number' && v1 === v1 && v2 === v2 || v1 === v2) ? v1 === v2 : compareEqualSlow(v1, v2);`;
|
||||
|
||||
/**
|
||||
* Find the first item in a list that is equal to a value according to compareEqual.
|
||||
* @param {unknown[]} list The list's array.
|
||||
* @param {any} item The value to search for.
|
||||
* @returns {number} The 0-indexed index of the item, otherwise -1.
|
||||
*/
|
||||
baseRuntime += `const listFind = (list, item) => {
|
||||
const itemNumber = +item;
|
||||
const itemString = ('' + item).toLowerCase();
|
||||
if (itemNumber !== itemNumber || (itemNumber === 0 && isNotActuallyZero(item))) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const value = list[i];
|
||||
if (value === item || ('' + value).toLowerCase() === itemString) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const value = list[i];
|
||||
const n = +value;
|
||||
if (n !== n || (n === 0 && isNotActuallyZero(value))) {
|
||||
if (('' + value).toLowerCase() === itemString) {
|
||||
return i;
|
||||
}
|
||||
} else if (n === itemNumber) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};`;
|
||||
|
||||
/**
|
||||
* Determine if one value is greater than another.
|
||||
@@ -308,14 +338,14 @@ runtimeFunctions.compareGreaterThan = `const compareGreaterThanSlow = (v1, v2) =
|
||||
} else if (n2 === 0 && isNotActuallyZero(v2)) {
|
||||
n2 = NaN;
|
||||
}
|
||||
if (Number.isNaN(n1) || Number.isNaN(n2)) {
|
||||
if (n1 !== n1 || n2 !== n2) {
|
||||
const s1 = ('' + v1).toLowerCase();
|
||||
const s2 = ('' + v2).toLowerCase();
|
||||
return s1 > s2;
|
||||
}
|
||||
return n1 > n2;
|
||||
};
|
||||
const compareGreaterThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && !Number.isNaN(v1) ? v1 > v2 : compareGreaterThanSlow(v1, v2)`;
|
||||
const compareGreaterThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && v1 === v1 ? v1 > v2 : compareGreaterThanSlow(v1, v2)`;
|
||||
|
||||
/**
|
||||
* Determine if one value is less than another.
|
||||
@@ -331,14 +361,14 @@ runtimeFunctions.compareLessThan = `const compareLessThanSlow = (v1, v2) => {
|
||||
} else if (n2 === 0 && isNotActuallyZero(v2)) {
|
||||
n2 = NaN;
|
||||
}
|
||||
if (Number.isNaN(n1) || Number.isNaN(n2)) {
|
||||
if (n1 !== n1 || n2 !== n2) {
|
||||
const s1 = ('' + v1).toLowerCase();
|
||||
const s2 = ('' + v2).toLowerCase();
|
||||
return s1 < s2;
|
||||
}
|
||||
return n1 < n2;
|
||||
};
|
||||
const compareLessThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && !Number.isNaN(v2) ? v1 < v2 : compareLessThanSlow(v1, v2)`;
|
||||
const compareLessThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && v2 === v2 ? v1 < v2 : compareLessThanSlow(v1, v2)`;
|
||||
|
||||
/**
|
||||
* Generate a random integer.
|
||||
@@ -439,12 +469,19 @@ const listIndex = (index, length) => {
|
||||
* @param {*} idx The 1-indexed index in the list.
|
||||
* @returns {*} The list item, otherwise empty string if it does not exist.
|
||||
*/
|
||||
runtimeFunctions.listGet = `const listGet = (list, idx) => {
|
||||
runtimeFunctions.listGet = `const listGetSlow = (list, idx) => {
|
||||
const index = listIndex(idx, list.length);
|
||||
if (index === -1) {
|
||||
return '';
|
||||
}
|
||||
return list[index];
|
||||
};
|
||||
const listGet = (list, idx) => {
|
||||
if (typeof idx === 'number' && idx >= 1) {
|
||||
const item = list[(idx | 0) - 1];
|
||||
return item === undefined ? '' : item;
|
||||
}
|
||||
return listGetSlow(list, idx);
|
||||
}`;
|
||||
|
||||
/**
|
||||
@@ -453,13 +490,21 @@ runtimeFunctions.listGet = `const listGet = (list, idx) => {
|
||||
* @param {*} idx List index, Scratch style.
|
||||
* @param {*} value The new value.
|
||||
*/
|
||||
runtimeFunctions.listReplace = `const listReplace = (list, idx, value) => {
|
||||
runtimeFunctions.listReplace = `const listReplaceSlow = (list, idx, value) => {
|
||||
const index = listIndex(idx, list.value.length);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
list.value[index] = value;
|
||||
list._monitorUpToDate = false;
|
||||
};
|
||||
const listReplace = (list, idx, value) => {
|
||||
if (typeof idx === 'number' && idx >= 1 && idx <= list.value.length) {
|
||||
list.value[(idx | 0) - 1] = value;
|
||||
list._monitorUpToDate = false;
|
||||
} else {
|
||||
listReplaceSlow(list, idx, value);
|
||||
}
|
||||
}`;
|
||||
|
||||
/**
|
||||
@@ -491,7 +536,13 @@ runtimeFunctions.listDelete = `const listDelete = (list, idx) => {
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
list.value.splice(index, 1);
|
||||
if (index === 0) {
|
||||
list.value.shift();
|
||||
} else if (index === list.value.length - 1) {
|
||||
list.value.pop();
|
||||
} else {
|
||||
list.value.splice(index, 1);
|
||||
}
|
||||
list._monitorUpToDate = false;
|
||||
}`;
|
||||
|
||||
@@ -506,27 +557,7 @@ runtimeFunctions.listContains = `const listContains = (list, item) => {
|
||||
if (list.value.indexOf(item) !== -1) {
|
||||
return true;
|
||||
}
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
if (compareEqual(list.value[i], item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}`;
|
||||
|
||||
/**
|
||||
* Find the 1-indexed index of an item in a list.
|
||||
* @param {import('../engine/variable')} list The list.
|
||||
* @param {*} item The item to search for
|
||||
* @returns {number} The 1-indexed index of the item in the list, otherwise 0
|
||||
*/
|
||||
runtimeFunctions.listIndexOf = `const listIndexOf = (list, item) => {
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
if (compareEqual(list.value[i], item)) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return listFind(list.value, item) !== -1;
|
||||
}`;
|
||||
|
||||
/**
|
||||
@@ -571,7 +602,8 @@ runtimeFunctions.mod = `const mod = (n, modulus) => {
|
||||
* @returns {number} value of tangent or Infinity or -Infinity
|
||||
*/
|
||||
runtimeFunctions.tan = `const tan = (angle) => {
|
||||
switch (angle % 360) {
|
||||
angle = angle % 360;
|
||||
switch (angle) {
|
||||
case -270: case 90: return Infinity;
|
||||
case -90: case 270: return -Infinity;
|
||||
}
|
||||
@@ -603,8 +635,18 @@ runtimeFunctions.yieldThenCallGenerator = `const yieldThenCallGenerator = functi
|
||||
* @param {import("../engine/thread")} thread The thread to step.
|
||||
*/
|
||||
const execute = thread => {
|
||||
globalState.thread = thread;
|
||||
thread.generator.next();
|
||||
try {
|
||||
globalState.thread = thread;
|
||||
if (thread.compiledFunction) {
|
||||
thread.compiledFunction();
|
||||
} else {
|
||||
thread.generator.next();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== RETIRE_MAGIC) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const threadStack = [];
|
||||
@@ -645,6 +687,6 @@ execute.scopedEval = scopedEval;
|
||||
execute.runtimeFunctions = runtimeFunctions;
|
||||
execute.saveGlobalState = saveGlobalState;
|
||||
execute.restoreGlobalState = restoreGlobalState;
|
||||
// Increment whenever generated code requires incompatible runtime behavior.
|
||||
execute.RETIRE_MAGIC = RETIRE_MAGIC;
|
||||
|
||||
module.exports = execute;
|
||||
+55
-9
@@ -7,6 +7,7 @@ const jsexecute = require('./jsexecute');
|
||||
const environment = require('./environment');
|
||||
const {StackOpcode, InputOpcode, InputType} = require('./enums.js');
|
||||
const oldCompilerCompatibility = require('./old-compiler-compatibility.js');
|
||||
const Sampler = require('../engine/tw-sampler.js');
|
||||
|
||||
// These imports are used by jsdoc comments but eslint doesn't know that
|
||||
/* eslint-disable no-unused-vars */
|
||||
@@ -120,12 +121,24 @@ class JSGenerator {
|
||||
this.localVariables = new VariablePool('a');
|
||||
this._setupVariablesPool = new VariablePool('b');
|
||||
this._setupVariables = {};
|
||||
this.usesTemporaryLocal = false;
|
||||
|
||||
this.descendedIntoModulo = false;
|
||||
this.isInHat = false;
|
||||
|
||||
this.debug = this.target.runtime.debug;
|
||||
|
||||
const sampler = this.target.runtime.sampler;
|
||||
/**
|
||||
* The current sampler, iff sampling is enabled.
|
||||
* @type {import('../engine/tw-sampler.js')|null}
|
||||
*/
|
||||
this.sampler = sampler.enableCommands ? sampler : null;
|
||||
/**
|
||||
* ID of the current command block being sampled.
|
||||
*/
|
||||
this.currentSamplerLocation = -1;
|
||||
|
||||
this.oldCompilerStub = new oldCompilerCompatibility.JSGeneratorStub(this);
|
||||
}
|
||||
|
||||
@@ -181,13 +194,14 @@ class JSGenerator {
|
||||
case InputOpcode.CAST_BOOLEAN:
|
||||
return `toBoolean(${this.descendInput(node.target)})`;
|
||||
case InputOpcode.CAST_NUMBER:
|
||||
if (node.target.isAlwaysType(InputType.BOOLEAN_INTERPRETABLE)) {
|
||||
return `(+${this.descendInput(node.target.toType(InputType.BOOLEAN))})`;
|
||||
if (node.target.isAlwaysType(InputType.BOOLEAN)) {
|
||||
return `(+${this.descendInput(node.target)})`;
|
||||
}
|
||||
this.usesTemporaryLocal = true;
|
||||
if (node.target.isAlwaysType(InputType.NUMBER_OR_NAN)) {
|
||||
return `toNotNaN(${this.descendInput(node.target)})`;
|
||||
return `((t = ${this.descendInput(node.target)}) === t ? t : 0)`;
|
||||
}
|
||||
return `toNotNaN(+${this.descendInput(node.target)})`;
|
||||
return `((t = +${this.descendInput(node.target)}) === t ? t : 0)`;
|
||||
case InputOpcode.CAST_NUMBER_OR_NAN:
|
||||
return `(+${this.descendInput(node.target)})`;
|
||||
case InputOpcode.CAST_NUMBER_INDEX:
|
||||
@@ -244,7 +258,7 @@ class JSGenerator {
|
||||
return `listGet(${this.referenceVariable(node.list)}.value, ${this.descendInput(node.index)})`;
|
||||
}
|
||||
case InputOpcode.LIST_INDEX_OF:
|
||||
return `listIndexOf(${this.referenceVariable(node.list)}, ${this.descendInput(node.item)})`;
|
||||
return `(listFind(${this.referenceVariable(node.list)}.value, ${this.descendInput(node.item)}) + 1)`;
|
||||
case InputOpcode.LIST_LENGTH:
|
||||
return `${this.referenceVariable(node.list)}.value.length`;
|
||||
|
||||
@@ -966,11 +980,27 @@ class JSGenerator {
|
||||
// TODO: allow if/else to inherit values
|
||||
this.pushFrame(frame);
|
||||
|
||||
const parentSamplerLocation = this.currentSamplerLocation;
|
||||
|
||||
for (let i = 0; i < stack.blocks.length; i++) {
|
||||
frame.isLastBlock = i === stack.blocks.length - 1;
|
||||
this.descendStackedBlock(stack.blocks[i]);
|
||||
const block = stack.blocks[i];
|
||||
|
||||
if (this.sampler && block.blockId) {
|
||||
const originalTargetId = this.target.sprite.clones[0].id;
|
||||
this.currentSamplerLocation = this.sampler.blockToLocation(originalTargetId, block.blockId);
|
||||
this.source += `pc[0] = ${this.currentSamplerLocation};\n`;
|
||||
}
|
||||
|
||||
this.descendStackedBlock(block);
|
||||
}
|
||||
|
||||
// Attribute the loop condition and yield back to the loop block itself.
|
||||
if (this.sampler && frame.isLoop) {
|
||||
this.source += `pc[0] = ${parentSamplerLocation};\n`;
|
||||
}
|
||||
this.currentSamplerLocation = parentSamplerLocation;
|
||||
|
||||
// Leaving a stack -- any assumptions made in the current stack do not apply outside of it
|
||||
// TODO: in if/else this might create an extra unused object
|
||||
this.popFrame();
|
||||
@@ -1013,10 +1043,10 @@ class JSGenerator {
|
||||
|
||||
retire () {
|
||||
// After running retire() (sets thread status and cleans up some unused data), we need to return to the event loop.
|
||||
// When in a procedure, return will only send us back to the previous procedure, so instead we yield back to the sequencer.
|
||||
// Outside of a procedure, return will correctly bring us back to the sequencer.
|
||||
// In procedures return just goes back 1 layer of the stack, so instead we throw a magic value that the runtime will
|
||||
// know to ignore.
|
||||
if (this.isProcedure) {
|
||||
this.source += 'retire(); yield;\n';
|
||||
this.source += `retire(); throw ${jsexecute.RETIRE_MAGIC};\n`;
|
||||
} else {
|
||||
this.source += 'retire(); return;\n';
|
||||
}
|
||||
@@ -1037,6 +1067,9 @@ class JSGenerator {
|
||||
if (!this.isWarp) {
|
||||
this.source += 'yield;\n';
|
||||
this.yielded();
|
||||
if (this.sampler) {
|
||||
this.source += `pc[0] = ${this.currentSamplerLocation};\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1045,11 +1078,17 @@ class JSGenerator {
|
||||
*/
|
||||
yieldStuckOrNotWarp () {
|
||||
if (this.isWarp) {
|
||||
if (this.sampler) {
|
||||
this.source += `pc[0] = ${Sampler.WARP_TIMER};\n`;
|
||||
}
|
||||
this.source += 'if (isStuck()) yield;\n';
|
||||
} else {
|
||||
this.source += 'yield;\n';
|
||||
}
|
||||
this.yielded();
|
||||
if (this.sampler) {
|
||||
this.source += `pc[0] = ${this.currentSamplerLocation};\n`;
|
||||
}
|
||||
}
|
||||
|
||||
yielded () {
|
||||
@@ -1142,6 +1181,9 @@ class JSGenerator {
|
||||
script += 'const target = thread.target; ';
|
||||
script += 'const runtime = target.runtime; ';
|
||||
script += 'const stage = runtime.getTargetForStage();\n';
|
||||
if (this.sampler) {
|
||||
script += 'const pc = runtime.sampler.pc;\n';
|
||||
}
|
||||
for (const varValue of Object.keys(this._setupVariables)) {
|
||||
const varName = this._setupVariables[varValue];
|
||||
script += `const ${varName} = ${varValue};\n`;
|
||||
@@ -1165,6 +1207,10 @@ class JSGenerator {
|
||||
}
|
||||
script += ') {\n';
|
||||
|
||||
if (this.usesTemporaryLocal) {
|
||||
script += 'let t;\n';
|
||||
}
|
||||
|
||||
script += this.source;
|
||||
|
||||
script += '}; })';
|
||||
|
||||
@@ -645,6 +645,10 @@ class Blocks {
|
||||
if (Object.prototype.hasOwnProperty.call(this._blocks, block.id)) {
|
||||
return;
|
||||
}
|
||||
// No prototype pollution.
|
||||
if (block.id === '__proto__') {
|
||||
return;
|
||||
}
|
||||
// Create new block.
|
||||
this._blocks[block.id] = block;
|
||||
// Push block id to scripts array.
|
||||
|
||||
+37
-4
@@ -24,6 +24,7 @@ const fetchWithTimeout = require('../util/fetch-with-timeout');
|
||||
const platform = require('./tw-platform.js');
|
||||
const safeStringify = require('../util/tw-safe-stringify.js');
|
||||
const MonitorState = require('./tw-monitor-state.js');
|
||||
const Sampler = require('./tw-sampler.js');
|
||||
|
||||
// Virtual I/O devices.
|
||||
const Clock = require('../io/clock');
|
||||
@@ -380,11 +381,19 @@ class Runtime extends EventEmitter {
|
||||
|
||||
/**
|
||||
* A runtime profiler that records timed events for later playback to
|
||||
* diagnose Scratch performance.
|
||||
* diagnose Scratch performance. This uses an instrumentation approach
|
||||
* and gathers limited data. The sampler is more useful for analyzing
|
||||
* performance on a per-block basis.
|
||||
* @type {Profiler}
|
||||
*/
|
||||
this.profiler = null;
|
||||
|
||||
/**
|
||||
* Sampling profiler.
|
||||
* @type {Sampler}
|
||||
*/
|
||||
this.sampler = new Sampler(this);
|
||||
|
||||
this.cloudOptions = {
|
||||
limit: 10
|
||||
};
|
||||
@@ -2496,7 +2505,9 @@ class Runtime extends EventEmitter {
|
||||
interpolate.interpolate(this, progressInFrame);
|
||||
|
||||
if (this.renderer) {
|
||||
if (this.sampler.enableBasic) this.sampler.pc[0] = Sampler.RENDERER;
|
||||
this.renderer.draw();
|
||||
if (this.sampler.enableBasic) this.sampler.pc[0] = Sampler.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2514,6 +2525,8 @@ class Runtime extends EventEmitter {
|
||||
* inactive threads after each iteration.
|
||||
*/
|
||||
_step () {
|
||||
if (this.sampler.enableBasic) this.sampler.pc[0] = Sampler.RUNTIME;
|
||||
|
||||
if (this.interpolationEnabled) {
|
||||
interpolate.setupInitialState(this);
|
||||
}
|
||||
@@ -2572,7 +2585,9 @@ class Runtime extends EventEmitter {
|
||||
// Checking for the animation frame loop is more reliable than using
|
||||
// interpolationEnabled in some edge cases
|
||||
if (!document.hidden && !this.frameLoop._interpolationAnimation) {
|
||||
if (this.sampler.enableBasic) this.sampler.pc[0] = Sampler.RENDERER;
|
||||
this.renderer.draw();
|
||||
if (this.sampler.enableBasic) this.sampler.pc[0] = Sampler.RUNTIME;
|
||||
}
|
||||
if (this.profiler !== null) {
|
||||
this.profiler.stop();
|
||||
@@ -2597,6 +2612,8 @@ class Runtime extends EventEmitter {
|
||||
if (this.interpolationEnabled) {
|
||||
this._lastStepTime = Date.now();
|
||||
}
|
||||
|
||||
if (this.sampler.enableBasic) this.sampler.pc[0] = Sampler.IDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2776,6 +2793,7 @@ class Runtime extends EventEmitter {
|
||||
* @param {string} options.procedureCode The ID of the block
|
||||
* @param {function} options.callback The callback, called with (args, BlockUtility). May return a promise.
|
||||
* @param {string[]} [options.arguments] Names of the arguments accepted. Optional if no arguments.
|
||||
* @param {string[]} [options.argumentDefaults] Default values of the arguments.
|
||||
* @param {boolean} [options.hidden] True to not include this block in the block palette
|
||||
* @param {1|2} [options.return] 1 for round reporter, 2 for boolean reported, leave empty for statement.
|
||||
*/
|
||||
@@ -2785,7 +2803,10 @@ class Runtime extends EventEmitter {
|
||||
const argumentNames = options.arguments || [];
|
||||
const names = argumentNames;
|
||||
const ids = argumentNames.map((_, i) => `arg${i}`);
|
||||
const defaults = argumentNames.map(() => '');
|
||||
const argumentDefaults = options.argumentDefaults || [];
|
||||
const defaults = argumentNames.map((_, i) => (
|
||||
typeof argumentDefaults[i] === 'undefined' ? '' : `${argumentDefaults[i]}`
|
||||
));
|
||||
this.addonBlocks[procedureCode] = {
|
||||
namesIdsDefaults: [names, ids, defaults],
|
||||
...options
|
||||
@@ -2814,6 +2835,18 @@ class Runtime extends EventEmitter {
|
||||
};
|
||||
this._blockInfo.unshift(blockInfo);
|
||||
}
|
||||
|
||||
const argumentTypes = procedureCode.match(/%[nbs]/g) || [];
|
||||
const shadows = argumentTypes.map((type, i) => {
|
||||
if (defaults[i] === '' || (type !== '%s' && type !== '%n')) {
|
||||
return '';
|
||||
}
|
||||
const shadowType = type === '%n' ? 'math_number' : 'text';
|
||||
const fieldName = type === '%n' ? 'NUM' : 'TEXT';
|
||||
return `<value name="${xmlEscape(ids[i])}"><shadow type="${shadowType}">` +
|
||||
`<field name="${fieldName}">${xmlEscape(defaults[i])}</field></shadow></value>`;
|
||||
}).join('');
|
||||
|
||||
blockInfo.blocks.push({
|
||||
info: {},
|
||||
xml:
|
||||
@@ -2823,7 +2856,7 @@ class Runtime extends EventEmitter {
|
||||
` argumentids="${xmlEscape(JSON.stringify(ids))}"` +
|
||||
` argumentdefaults="${xmlEscape(JSON.stringify(defaults))}"` +
|
||||
`${options.return ? ` return="${xmlEscape(options.return.toString())}"` : ''}` +
|
||||
'></mutation></block>'
|
||||
`></mutation>${shadows}</block>`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3340,7 +3373,7 @@ class Runtime extends EventEmitter {
|
||||
const varType = (typeof optVarType === 'string') ? optVarType : Variable.SCALAR_TYPE;
|
||||
const allVariableNames = this.getAllVarNamesOfType(varType);
|
||||
const newName = StringUtil.unusedName(variableName, allVariableNames);
|
||||
const variable = new Variable(optVarId || uid(), newName, varType);
|
||||
const variable = Variable.create(optVarId || uid(), newName, varType);
|
||||
const stage = this.getTargetForStage();
|
||||
stage.variables[variable.id] = variable;
|
||||
return variable;
|
||||
|
||||
@@ -2,6 +2,7 @@ const Timer = require('../util/timer');
|
||||
const Thread = require('./thread');
|
||||
const execute = require('./execute.js');
|
||||
const compilerExecute = require('../compiler/jsexecute');
|
||||
const Sampler = require('./tw-sampler');
|
||||
|
||||
/**
|
||||
* Profiler frame name for stepping a single thread.
|
||||
@@ -178,6 +179,7 @@ class Sequencer {
|
||||
stepThread (thread) {
|
||||
if (thread.isCompiled) {
|
||||
compilerExecute(thread);
|
||||
if (this.runtime.sampler.enableBasic) this.runtime.sampler.pc[0] = Sampler.RUNTIME;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -370,6 +372,7 @@ class Sequencer {
|
||||
if (thread.isCompiled) {
|
||||
thread.procedures = null;
|
||||
thread.generator = null;
|
||||
thread.compiledFunction = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ class Target extends EventEmitter {
|
||||
if (variable) return variable;
|
||||
|
||||
// No variable with this name exists - create it locally.
|
||||
const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false);
|
||||
const newVariable = Variable.create(id, name, Variable.SCALAR_TYPE, false);
|
||||
this.variables[id] = newVariable;
|
||||
return newVariable;
|
||||
}
|
||||
@@ -254,7 +254,7 @@ class Target extends EventEmitter {
|
||||
if (list) return list;
|
||||
|
||||
// No variable with this name exists - create it locally.
|
||||
const newList = new Variable(id, name, Variable.LIST_TYPE, false);
|
||||
const newList = Variable.create(id, name, Variable.LIST_TYPE, false);
|
||||
this.variables[id] = newList;
|
||||
return newList;
|
||||
}
|
||||
@@ -270,7 +270,7 @@ class Target extends EventEmitter {
|
||||
*/
|
||||
createVariable (id, name, type, isCloud) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.variables, id)) {
|
||||
const newVariable = new Variable(id, name, type, false);
|
||||
const newVariable = Variable.create(id, name, type, false);
|
||||
if (isCloud && this.isStage && this.runtime.canAddCloudVariable()) {
|
||||
newVariable.isCloud = true;
|
||||
this.runtime.addCloudVariable();
|
||||
@@ -415,7 +415,8 @@ class Target extends EventEmitter {
|
||||
duplicateVariable (id, optKeepOriginalId) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
|
||||
const originalVariable = this.variables[id];
|
||||
const newVariable = new Variable(
|
||||
const newVariable = Variable.createSibling(
|
||||
originalVariable,
|
||||
optKeepOriginalId ? id : null, // conditionally keep original id or generate a new one
|
||||
originalVariable.name,
|
||||
originalVariable.type,
|
||||
@@ -600,7 +601,7 @@ class Target extends EventEmitter {
|
||||
if (existingLocalVar) {
|
||||
newVarId = existingLocalVar.id;
|
||||
} else {
|
||||
const newVar = new Variable(null, varName, varType);
|
||||
const newVar = Variable.create(null, varName, varType);
|
||||
newVarId = newVar.id;
|
||||
sprite.variables[newVarId] = newVar;
|
||||
}
|
||||
|
||||
+14
-2
@@ -207,9 +207,14 @@ class Thread {
|
||||
this.timer = null;
|
||||
/**
|
||||
* The thread's generator.
|
||||
* @type {Generator}
|
||||
* @type {?Generator}
|
||||
*/
|
||||
this.generator = null;
|
||||
/**
|
||||
* The entry point of a compiled thread which does not yield.
|
||||
* @type {?Function}
|
||||
*/
|
||||
this.compiledFunction = null;
|
||||
/**
|
||||
* @type {Object.<string, import('../compiler/compile').CompiledScript>}
|
||||
*/
|
||||
@@ -513,7 +518,14 @@ class Thread {
|
||||
this.procedures[procedureCode] = result.procedures[procedureCode](this);
|
||||
}
|
||||
|
||||
this.generator = result.startingFunction(this)();
|
||||
const startingFunction = result.startingFunction(this);
|
||||
if (result.yields) {
|
||||
this.generator = startingFunction();
|
||||
this.compiledFunction = null;
|
||||
} else {
|
||||
this.generator = null;
|
||||
this.compiledFunction = startingFunction;
|
||||
}
|
||||
|
||||
this.executableHat = result.executableHat;
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ class FontManager extends EventEmitter {
|
||||
* @param {string} family
|
||||
* @param {string} fallback
|
||||
* @param {Asset} asset scratch-storage asset
|
||||
* @returns {Promise<unknown>} Resolves when the font is ready.
|
||||
*/
|
||||
addCustomFont (family, fallback, asset) {
|
||||
if (!this.isValidCustomFont(family)) {
|
||||
@@ -171,8 +172,9 @@ class FontManager extends EventEmitter {
|
||||
fallback,
|
||||
asset
|
||||
});
|
||||
this.updateRenderer();
|
||||
const promise = this.updateRenderer();
|
||||
this.changed();
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,9 +210,12 @@ class FontManager extends EventEmitter {
|
||||
this.changed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<unknown>} Resolves when fonts are ready.
|
||||
*/
|
||||
updateRenderer () {
|
||||
if (!this.runtime.renderer || !this.runtime.renderer.setCustomFonts) {
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const fontfaces = {};
|
||||
@@ -222,7 +227,7 @@ class FontManager extends EventEmitter {
|
||||
fontfaces[family] = fontface;
|
||||
}
|
||||
}
|
||||
this.runtime.renderer.setCustomFonts(fontfaces);
|
||||
return this.runtime.renderer.setCustomFonts(fontfaces);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,6 +278,7 @@ class FontManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
const fontPromises = [];
|
||||
for (const font of json) {
|
||||
if (!font || typeof font !== 'object') {
|
||||
continue;
|
||||
@@ -305,12 +311,14 @@ class FontManager extends EventEmitter {
|
||||
this.runtime.storage.AssetType.Font,
|
||||
md5ext
|
||||
);
|
||||
this.addCustomFont(family, fallback, asset);
|
||||
fontPromises.push(this.addCustomFont(family, fallback, asset));
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('could not add font', e);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(fontPromises);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/* global SharedArrayBuffer */
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
*
|
||||
* Sampler is the more accurate performance profiling tool. It's more meaningful than the legacy Profiler.
|
||||
*
|
||||
* It also revolved around one SharedArrayBuffer that stores a single integer. That integer represents the block being
|
||||
* executed and can be converted back to block ID / sprite ID via simple lookup later. This approach keeps the
|
||||
* performance impact of profiling extremely minimal and avoids structurally changing the generated JS enough that the
|
||||
* benchmark data becomes meaningless.
|
||||
*
|
||||
* To measure performance, a worker is running a high frequency loop checking the latest value from the
|
||||
* SharedArrayBuffer and counting how often it sees each number. Workers can use Atomics instead of setInterval/Timeout
|
||||
* to make a high frequency timer.
|
||||
*
|
||||
* Previous approaches tried to do a `performance.now()` between every block. The end result of that was all you were
|
||||
* measuring was "how often does this block run" because the overhead of getting the time was orders of magnitude
|
||||
* larger than even slow lines in the typical projec.t
|
||||
*
|
||||
* This class only implements the tracking part. Implementors that want to collect data are responsible for spinning
|
||||
* up their worker with an appropriate frequency.
|
||||
*
|
||||
* To use SharedArrayBuffer, browsers force the web page to set some headers. The least invasive way to do that is
|
||||
* by setting Document-Isolation-Policy: isolate-and-credentialless. COOP/COEP are too invasive for most websites to
|
||||
* try opting into, unfortunately, but this new header has minimal downsides for us since we generally won't have any
|
||||
* third-party credentials to be sending anyway.
|
||||
*/
|
||||
|
||||
class Sampler {
|
||||
// Reserved values get negative location IDs.
|
||||
// Blocks get everything else.
|
||||
static IDLE = -1;
|
||||
static RUNTIME = -2;
|
||||
static RENDERER = -3;
|
||||
static WARP_TIMER = -4;
|
||||
|
||||
static LEVEL_BASIC = 1;
|
||||
static LEVEL_COMMANDS = 2;
|
||||
|
||||
constructor (runtime) {
|
||||
this.runtime = runtime;
|
||||
|
||||
/**
|
||||
* The shared array buffer that store the current block. Stores 1 integer. Created lazily.
|
||||
* @type {Int32Array|null}
|
||||
* @public
|
||||
*/
|
||||
this.pc = null;
|
||||
|
||||
/**
|
||||
* General "runtime" vs "renderer" sampling.
|
||||
*/
|
||||
this.enableBasic = false;
|
||||
|
||||
/**
|
||||
* Per-command sampling. Implies basic.
|
||||
*/
|
||||
this.enableCommands = false;
|
||||
|
||||
/**
|
||||
* Maps location ID to what it represents.
|
||||
* @type {Array<{blockId: string; spriteId: string;}>}
|
||||
*/
|
||||
this.locations = [];
|
||||
|
||||
/**
|
||||
* Maps sprite ID -> block ID -> location ID.
|
||||
* @type {Map<string, Map<string, number>>}
|
||||
*/
|
||||
this.blockLocations = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only checks sampling-time requirements. It doesn't check that a Worker will have required high-precision
|
||||
* wait APIs for doing a reasonable timer, or that performance.now() will be precise enough.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAvailable () {
|
||||
return typeof SharedArrayBuffer === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Change enableness for newly-started scripts.
|
||||
* Do not call during a VM step. It must be called between.
|
||||
* @param {number} level See LEVEL_* constants.
|
||||
*/
|
||||
setEnabled (level) {
|
||||
if (level > 0 && !this.isAvailable()) {
|
||||
throw new Error('Sampler is not available');
|
||||
}
|
||||
|
||||
if (this.pc === null && level > 0) {
|
||||
this.pc = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
||||
}
|
||||
|
||||
this.enableBasic = level >= Sampler.LEVEL_BASIC;
|
||||
this.enableCommands = level >= Sampler.LEVEL_COMMANDS;
|
||||
|
||||
this.runtime.resetAllCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} spriteId ID of the original target (not a clone) that owns the block.
|
||||
* @param {string} blockId
|
||||
* @returns {number} Location ID. Same block always gets the same ID.
|
||||
*/
|
||||
blockToLocation (spriteId, blockId) {
|
||||
let spriteLocations = this.blockLocations.get(spriteId);
|
||||
if (!spriteLocations) {
|
||||
spriteLocations = new Map();
|
||||
this.blockLocations.set(spriteId, spriteLocations);
|
||||
}
|
||||
|
||||
let location = spriteLocations.get(blockId);
|
||||
if (typeof location === 'undefined') {
|
||||
this.locations.push({
|
||||
blockId,
|
||||
spriteId
|
||||
});
|
||||
location = this.locations.length - 1;
|
||||
spriteLocations.set(blockId, location);
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} location
|
||||
* @returns {{blockId: string; spriteId: string;}}
|
||||
*/
|
||||
locationToBlock (location) {
|
||||
if (location < 0) {
|
||||
throw new Error(`Tried to get block from a reserved location ID`);
|
||||
}
|
||||
return this.locations[location];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Sampler;
|
||||
@@ -8,6 +8,7 @@ const xmlEscape = require('../util/xml-escape');
|
||||
|
||||
class Variable {
|
||||
/**
|
||||
* Do not new() this class directly - use Variable.create instead.
|
||||
* @param {string | null} id Id of the variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @param {string} type Type of the variable, one of '' or 'list'
|
||||
@@ -26,6 +27,7 @@ class Variable {
|
||||
break;
|
||||
case Variable.LIST_TYPE:
|
||||
this.value = [];
|
||||
this._monitorUpToDate = false;
|
||||
break;
|
||||
case Variable.BROADCAST_MESSAGE_TYPE:
|
||||
this.value = this.name;
|
||||
@@ -35,6 +37,47 @@ class Variable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new variable.
|
||||
* @param {string | null} id Id of the variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @param {string} type Type of the variable, one of '' or 'list'
|
||||
* @param {boolean} isCloud Whether the variable is stored in the cloud.
|
||||
* @returns {Variable} The new variable.
|
||||
*/
|
||||
static create (id, name, type, isCloud) {
|
||||
// This looks silly, but all the JavaScript engines handle this by making a new hidden class for this variable.
|
||||
// That means that the browser engine can specialize compiled JS to take advantage of "this variable is always
|
||||
// a small integer" instead of having to use the much slower generic paths that one shared hidden class forces.
|
||||
// This is most relevant for V8/Chrome.
|
||||
|
||||
if (type === Variable.LIST_TYPE) {
|
||||
// For lists, there is no benefit to using specialized-per-variable hidden class.
|
||||
// Reusing one is the same speed but avoids wasting some memory on those extra classes.
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
return new ListSpecializedVariable(id, name, type, isCloud);
|
||||
}
|
||||
|
||||
const SpecializedVariable = class extends Variable {};
|
||||
return new SpecializedVariable(id, name, type, isCloud);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a clone of a variable to give to a clone of a sprite.
|
||||
* @param {Variable} original The variable whose class to reuse.
|
||||
* @param {string | null} id Id of the variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @param {string} type Type of the variable, one of '' or 'list'
|
||||
* @param {boolean} isCloud Whether the variable is stored in the cloud.
|
||||
* @returns {Variable} The new variable.
|
||||
*/
|
||||
static createSibling (original, id, name, type, isCloud) {
|
||||
// Use the same hidden class. We reuse compiled JS across clones, so swapping out to a new hidden class would
|
||||
// force that code to take slower routes instead of the specialization we want.
|
||||
const SpecializedVariable = original.constructor;
|
||||
return new SpecializedVariable(id, name, type, isCloud);
|
||||
}
|
||||
|
||||
toXML (isLocal) {
|
||||
isLocal = (isLocal === true);
|
||||
return `<variable type="${this.type}" id="${this.id}" islocal="${isLocal
|
||||
@@ -68,4 +111,9 @@ class Variable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See Variable.create()
|
||||
*/
|
||||
class ListSpecializedVariable extends Variable {}
|
||||
|
||||
module.exports = Variable;
|
||||
@@ -228,7 +228,6 @@ class ExtensionManager {
|
||||
const extensionObjects = await load(rewritten, this.vm)
|
||||
.catch(error => this._failedLoadingExtensionScript(error));
|
||||
const fakeWorkerId = this.nextExtensionWorker++;
|
||||
this.workerURLs[fakeWorkerId] = extensionURL;
|
||||
|
||||
for (const extensionObject of extensionObjects) {
|
||||
const extensionInfo = extensionObject.getInfo();
|
||||
@@ -238,6 +237,7 @@ class ExtensionManager {
|
||||
this._loadedExtensions.set(extensionInfo.id, serviceName);
|
||||
}
|
||||
|
||||
this.workerURLs[fakeWorkerId] = extensionURL;
|
||||
this._finishedLoadingExtensionScript();
|
||||
return;
|
||||
}
|
||||
@@ -355,6 +355,7 @@ class ExtensionManager {
|
||||
const workerInfo = this.pendingWorkers[id];
|
||||
delete this.pendingWorkers[id];
|
||||
if (e) {
|
||||
delete this.workerURLs[id];
|
||||
workerInfo.reject(e);
|
||||
} else {
|
||||
workerInfo.resolve();
|
||||
|
||||
@@ -6,6 +6,7 @@ const dispatch = require('../dispatch/worker-dispatch');
|
||||
const log = require('../util/log');
|
||||
const {isWorker} = require('./tw-extension-worker-context');
|
||||
const createTranslate = require('./tw-l10n');
|
||||
const loadScriptElement = require('./tw-load-script-element');
|
||||
|
||||
const translate = createTranslate(null);
|
||||
|
||||
@@ -13,15 +14,7 @@ const loadScripts = url => {
|
||||
if (isWorker) {
|
||||
importScripts(url);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => {
|
||||
reject(new Error(`Error in sandboxed script: ${url}. Check the console for more information.`));
|
||||
};
|
||||
script.src = url;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
return loadScriptElement(url);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {string}
|
||||
*/
|
||||
const urlForLogs = url => {
|
||||
if (url.startsWith('data:')) {
|
||||
return 'Custom extension';
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a script by adding a <script> element to the document, with error handling.
|
||||
* @param {string} url
|
||||
* @returns {Promise<void>} Resolves or rejects when the script's synchronous execution finishes.
|
||||
*/
|
||||
const loadScriptElement = url => new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
|
||||
// This will catch errors during synchronous JS execution, including syntax errors.
|
||||
const handleWindowError = event => {
|
||||
if (document.currentScript !== script) {
|
||||
// Some other script is running, not us.
|
||||
return;
|
||||
}
|
||||
if (event.error instanceof Error) {
|
||||
reject(new Error(`${urlForLogs(url)} encountered runtime error: ${event.error}`, {
|
||||
cause: event.error
|
||||
}));
|
||||
} else {
|
||||
// Browser hides details for cross-site scripts.
|
||||
reject(new Error(`${urlForLogs(url)} encountered runtime error: ${event.message}`));
|
||||
}
|
||||
};
|
||||
|
||||
const removeWindowErrorHandler = () => {
|
||||
window.removeEventListener('error', handleWindowError);
|
||||
};
|
||||
window.addEventListener('error', handleWindowError);
|
||||
|
||||
script.onload = () => {
|
||||
// When this fires, synchronous execution is done.
|
||||
removeWindowErrorHandler();
|
||||
resolve();
|
||||
};
|
||||
|
||||
// This will catch errors before the JS can be executed at all.
|
||||
script.onerror = () => {
|
||||
removeWindowErrorHandler();
|
||||
reject(new Error(`${urlForLogs(url)} could not be loaded, usually due to a network error`));
|
||||
};
|
||||
|
||||
script.src = url;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
module.exports = loadScriptElement;
|
||||
@@ -146,6 +146,7 @@ class SecurityManager {
|
||||
|
||||
/**
|
||||
* Determine whether an extension is allowed to embed content from a given URL.
|
||||
* javascript: URLs are always rejected (this method is never called).
|
||||
* @param {string} documentURL The URL of the embed.
|
||||
* @returns {Promise<boolean>|boolean}
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@ const createScratchX = require('./tw-scratchx-compatibility-layer');
|
||||
const AsyncLimiter = require('../util/async-limiter');
|
||||
const createTranslate = require('./tw-l10n');
|
||||
const staticFetch = require('../util/tw-static-fetch');
|
||||
const loadScriptElement = require('./tw-load-script-element');
|
||||
|
||||
/* eslint-disable require-await */
|
||||
|
||||
@@ -93,6 +94,11 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
// Embedding javascript: can execute code in the embedding page's origin.
|
||||
// eslint-disable-next-line no-script-url
|
||||
if (parsed.protocol === 'javascript:') {
|
||||
return false;
|
||||
}
|
||||
return vm.securityManager.canEmbed(parsed.href);
|
||||
};
|
||||
|
||||
@@ -179,13 +185,7 @@ const teardownUnsandboxedExtensionAPI = () => {
|
||||
*/
|
||||
const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, reject) => {
|
||||
setupUnsandboxedExtensionAPI(vm).then(resolve);
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.onerror = () => {
|
||||
reject(new Error(`Error in unsandboxed script ${extensionURL}. Check the console for more information.`));
|
||||
};
|
||||
script.src = extensionURL;
|
||||
document.body.appendChild(script);
|
||||
loadScriptElement(extensionURL).catch(reject);
|
||||
}).then(objects => {
|
||||
teardownUnsandboxedExtensionAPI();
|
||||
return objects;
|
||||
|
||||
@@ -16,6 +16,15 @@ const StageLayering = require('../../engine/stage-layering');
|
||||
// eslint-disable-next-line max-len
|
||||
const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48dGl0bGU+cGVuLWljb248L3RpdGxlPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjNTc1ZTc1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Im04Ljc1MyAzNC42MDItNC4yNSAxLjc4IDEuNzgzLTQuMjM3YzEuMjE4LTIuODkyIDIuOTA3LTUuNDIzIDUuMDMtNy41MzhMMzEuMDY2IDQuOTNjLjg0Ni0uODQyIDIuNjUtLjQxIDQuMDMyLjk2NyAxLjM4IDEuMzc1IDEuODE2IDMuMTczLjk3IDQuMDE1TDE2LjMxOCAyOS41OWMtMi4xMjMgMi4xMTYtNC42NjQgMy44LTcuNTY1IDUuMDEyIi8+PHBhdGggZD0iTTI5LjQxIDYuMTFzLTQuNDUtMi4zNzgtOC4yMDIgNS43NzJjLTEuNzM0IDMuNzY2LTQuMzUgMS41NDYtNC4zNSAxLjU0NiIvPjxwYXRoIGZpbGw9IiM0Yzk3ZmYiIGQ9Ik0zNi40MiA4LjgyNWMwIC40NjMtLjE0Ljg3My0uNDMyIDEuMTY0bC05LjMzNSA5LjNjLjI4Mi0uMjkuNDEtLjY2OC40MS0xLjEyIDAtLjg3NC0uNTA3LTEuOTYzLTEuNDA2LTIuODY4LTEuMzYyLTEuMzU4LTMuMTQ3LTEuOC00LjAwMi0uOTlMMzAuOTkgNS4wMWMuODQ0LS44NCAyLjY1LS40MSA0LjAzNS45Ni44OTguOTA0IDEuMzk2IDEuOTgyIDEuMzk2IDIuODU1TTEwLjUxNSAzMy43NzRhMjQgMjQgMCAwIDEtMS43NjQuODNMNC41IDM2LjM4MmwxLjc4Ni00LjIzNWMuMjU4LS42MDQuNTMtMS4xODYuODMzLTEuNzU3LjY5LjE4MyAxLjQ0OC42MjUgMi4xMDggMS4yODIuNjYuNjU4IDEuMTAyIDEuNDEyIDEuMjg3IDIuMTAyIi8+PHBhdGggZmlsbD0iIzU3NWU3NSIgZD0iTTM2LjQ5OCA4Ljc0OGMwIC40NjQtLjE0Ljg3NC0uNDMzIDEuMTY1bC0xOS43NDIgMTkuNjhjLTIuMTMgMi4xMS00LjY3MyAzLjc5My03LjU3MiA1LjAxTDQuNSAzNi4zOGwuOTc0LTIuMzE2IDEuOTI1LS44MDhjMi44OTgtMS4yMTggNS40NC0yLjkgNy41Ny01LjAxbDE5Ljc0My0xOS42OGMuMjkyLS4yOTIuNDMyLS43MDIuNDMyLTEuMTY1IDAtLjY0Ni0uMjctMS40LS43OC0yLjEyMi4yNS4xNzIuNS4zNzcuNzM3LjYxNC44OTguOTA1IDEuMzk2IDEuOTgzIDEuMzk2IDIuODU2IiBvcGFjaXR5PSIuMTUiLz48cGF0aCBmaWxsPSIjNTc1ZTc1IiBkPSJNMTguNDUgMTIuODNhLjkwNC45MDQgMCAxIDEtLjkwMy0uOTAyYy41IDAgLjkwNC40MDQuOTA0LjkwNHoiLz48L2c+PC9zdmc+';
|
||||
|
||||
/**
|
||||
* LUT for integer / 255 as a float.
|
||||
* @type {Float64Array}
|
||||
*/
|
||||
const DIVIDE_255_LUT = new Float64Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
DIVIDE_255_LUT[i] = i / 255;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum for pen color parameter values.
|
||||
* @readonly
|
||||
@@ -594,17 +603,55 @@ class Scratch3PenBlocks {
|
||||
}
|
||||
_setPenColorToColor (color, target) { // used by compiler
|
||||
const penState = this._getPenState(target);
|
||||
const rgb = Cast.toRgbColorObject(color);
|
||||
const hsv = Color.rgbToHsv(rgb);
|
||||
penState.color = (hsv.h / 360) * 100;
|
||||
penState.saturation = hsv.s * 100;
|
||||
penState.brightness = hsv.v * 100;
|
||||
if (Object.prototype.hasOwnProperty.call(rgb, 'a')) {
|
||||
penState.transparency = 100 * (1 - (rgb.a / 255.0));
|
||||
} else {
|
||||
|
||||
let r;
|
||||
let g;
|
||||
let b;
|
||||
if (typeof color === 'string' && color.charCodeAt(0) === 35 /* # */) {
|
||||
const rgb = Color.hexToRgb(color);
|
||||
if (rgb) {
|
||||
r = rgb.r;
|
||||
g = rgb.g;
|
||||
b = rgb.b;
|
||||
} else {
|
||||
// Invalid hex colors are black
|
||||
r = 0;
|
||||
g = 0;
|
||||
b = 0;
|
||||
}
|
||||
penState.transparency = 0;
|
||||
} else {
|
||||
const decimal = Cast.toNumber(color);
|
||||
const a = (decimal >> 24) & 0xFF;
|
||||
r = (decimal >> 16) & 0xFF;
|
||||
g = (decimal >> 8) & 0xFF;
|
||||
b = decimal & 0xFF;
|
||||
penState.transparency = 100 * (1 - ((a > 0 ? a : 255) / 255.0));
|
||||
}
|
||||
|
||||
const rFloat = DIVIDE_255_LUT[r];
|
||||
const gFloat = DIVIDE_255_LUT[g];
|
||||
const bFloat = DIVIDE_255_LUT[b];
|
||||
|
||||
// Now, we convert to HSV.
|
||||
const x = Math.min(Math.min(rFloat, gFloat), bFloat);
|
||||
const v = Math.max(Math.max(rFloat, gFloat), bFloat);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
if (x !== v) {
|
||||
const f = (rFloat === x) ? gFloat - bFloat : ((gFloat === x) ? bFloat - rFloat : rFloat - gFloat);
|
||||
const i = (rFloat === x) ? 3 : ((gFloat === x) ? 5 : 1);
|
||||
h = (i - (f / (v - x))) * 60;
|
||||
// h is in [0, 360] here, so this is equivalent to h % 360 without the slow floating point modulo.
|
||||
// Pure reds give exactly 360.
|
||||
if (h === 360) h = 0;
|
||||
s = (v - x) / v;
|
||||
}
|
||||
|
||||
penState.color = (h / 360) * 100;
|
||||
penState.saturation = s * 100;
|
||||
penState.brightness = v * 100;
|
||||
|
||||
// Set the legacy "shade" value the same way scratch 2 did.
|
||||
penState._shade = penState.brightness / 2;
|
||||
|
||||
|
||||
+46
-26
@@ -564,9 +564,10 @@ const parseScratchAssets = function (object, runtime, topLevel, zip) {
|
||||
* @param {?object} zip - Optional zipped assets for local file import
|
||||
* @param {object} assets - Promises for assets of this scratch object grouped
|
||||
* into costumes and sounds
|
||||
* @param {Map.<Target, number>} targetPaneOrders - (out) position of each parsed target in the sprite pane.
|
||||
* @return {!Promise.<Array.<Target>>} Promise for the loaded targets when ready, or null for unsupported objects.
|
||||
*/
|
||||
const parseScratchObject = function (object, runtime, extensions, topLevel, zip, assets) {
|
||||
const parseScratchObject = function (object, runtime, extensions, topLevel, zip, assets, targetPaneOrders) {
|
||||
if (!Object.prototype.hasOwnProperty.call(object, 'objName')) {
|
||||
if (Object.prototype.hasOwnProperty.call(object, 'listName')) {
|
||||
// Shim these objects so they can be processed as monitors
|
||||
@@ -618,7 +619,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
|
||||
// - it's a stage variable, and
|
||||
// - the runtime can support another cloud variable
|
||||
const isCloud = variable.isPersistent && topLevel && runtime.canAddCloudVariable();
|
||||
const newVariable = new Variable(
|
||||
const newVariable = Variable.create(
|
||||
getVariableId(variable.name, Variable.SCALAR_TYPE),
|
||||
variable.name,
|
||||
Variable.SCALAR_TYPE,
|
||||
@@ -711,7 +712,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
|
||||
if (Object.prototype.hasOwnProperty.call(object, 'lists')) {
|
||||
for (let k = 0; k < object.lists.length; k++) {
|
||||
const list = object.lists[k];
|
||||
const newVariable = new Variable(
|
||||
const newVariable = Variable.create(
|
||||
getVariableId(list.listName, Variable.LIST_TYPE),
|
||||
list.listName,
|
||||
Variable.LIST_TYPE,
|
||||
@@ -777,8 +778,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
|
||||
if (Object.prototype.hasOwnProperty.call(object, 'indexInLibrary')) {
|
||||
// Temporarily store the 'indexInLibrary' property from the sb2 file
|
||||
// so that we can correctly order sprites in the target pane.
|
||||
// This will be deleted after we are done parsing and ordering the targets list.
|
||||
target.targetPaneOrder = object.indexInLibrary;
|
||||
targetPaneOrders.set(target, object.indexInLibrary);
|
||||
}
|
||||
|
||||
target.isStage = topLevel;
|
||||
@@ -798,7 +798,15 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
|
||||
if (object.children) {
|
||||
for (let m = 0; m < object.children.length; m++) {
|
||||
childrenPromises.push(
|
||||
parseScratchObject(object.children[m], runtime, extensions, false, zip, assets.children[m])
|
||||
parseScratchObject(
|
||||
object.children[m],
|
||||
runtime,
|
||||
extensions,
|
||||
false,
|
||||
zip,
|
||||
assets.children[m],
|
||||
targetPaneOrders
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -852,7 +860,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
|
||||
// target).
|
||||
for (const msgName in allBroadcastMsgs) {
|
||||
const msgId = allBroadcastMsgs[msgName];
|
||||
const newMsg = new Variable(
|
||||
const newMsg = Variable.create(
|
||||
msgId,
|
||||
msgName,
|
||||
Variable.BROADCAST_MESSAGE_TYPE,
|
||||
@@ -886,21 +894,23 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
|
||||
);
|
||||
};
|
||||
|
||||
const reorderParsedTargets = function (targets) {
|
||||
// Reorder parsed targets based on the temporary targetPaneOrder property
|
||||
// and then delete it.
|
||||
|
||||
const reordered = targets.map((t, index) => {
|
||||
t.layerOrder = index;
|
||||
return t;
|
||||
}).sort((a, b) => a.targetPaneOrder - b.targetPaneOrder);
|
||||
|
||||
// Delete the temporary target pane ordering since we shouldn't need it anymore.
|
||||
reordered.forEach(t => {
|
||||
delete t.targetPaneOrder;
|
||||
});
|
||||
|
||||
return reordered;
|
||||
/**
|
||||
* Reorder parsed targets from layer order into sprite-pane order.
|
||||
* @param {Target[]} targets layer order
|
||||
* @param {Map<Target, number>} targetPaneOrders position of targets in sprite pane
|
||||
* @returns {{targets: Target[], layerOrder: number[]}} targets in sprite-pane order and their layer order
|
||||
*/
|
||||
const reorderParsedTargets = function (targets, targetPaneOrders) {
|
||||
const reordered = targets
|
||||
.map((target, index) => ({
|
||||
target,
|
||||
layerOrder: index
|
||||
}))
|
||||
.sort((a, b) => targetPaneOrders.get(a.target) - targetPaneOrders.get(b.target));
|
||||
return {
|
||||
targets: reordered.map(i => i.target),
|
||||
layerOrder: reordered.map(i => i.layerOrder)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -918,17 +928,27 @@ const sb2import = function (json, runtime, optForceSprite, zip) {
|
||||
extensionIDs: new Set(),
|
||||
extensionURLs: new Map()
|
||||
};
|
||||
const targetPaneOrders = new Map();
|
||||
return Promise.resolve(parseScratchAssets(json, runtime, !optForceSprite, zip))
|
||||
// Force this promise to wait for the next loop in the js tick. Let
|
||||
// storage have some time to send off asset requests.
|
||||
.then(assets => Promise.resolve(assets))
|
||||
.then(assets => (
|
||||
parseScratchObject(json, runtime, extensions, !optForceSprite, zip, assets)
|
||||
parseScratchObject(
|
||||
json,
|
||||
runtime,
|
||||
extensions,
|
||||
!optForceSprite,
|
||||
zip,
|
||||
assets,
|
||||
targetPaneOrders
|
||||
)
|
||||
))
|
||||
.then(reorderParsedTargets)
|
||||
.then(targets => ({
|
||||
.then(targets => reorderParsedTargets(targets, targetPaneOrders))
|
||||
.then(({targets, layerOrder}) => ({
|
||||
targets,
|
||||
extensions
|
||||
extensions,
|
||||
layerOrder
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
+23
-26
@@ -28,6 +28,7 @@ const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
* @typedef {object} ImportedProject
|
||||
* @property {Array.<Target>} targets - the imported Scratch 3.0 target objects.
|
||||
* @property {ImportedExtensionsInfo} extensions - the ID of each extension actually used by this project.
|
||||
* @property {number[]} layerOrder the layer order of each target, in the same order as targets.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -1236,7 +1237,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
|
||||
// - the runtime can support another cloud variable
|
||||
const isCloud = (variable.length === 3) && variable[2] &&
|
||||
object.isStage && runtime.canAddCloudVariable();
|
||||
const newVariable = new Variable(
|
||||
const newVariable = Variable.create(
|
||||
varId, // var id is the index of the variable desc array in the variables obj
|
||||
variable[0], // name of the variable
|
||||
Variable.SCALAR_TYPE, // type of the variable
|
||||
@@ -1250,7 +1251,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
|
||||
if (Object.prototype.hasOwnProperty.call(object, 'lists')) {
|
||||
for (const listId in object.lists) {
|
||||
const list = object.lists[listId];
|
||||
const newList = new Variable(
|
||||
const newList = Variable.create(
|
||||
listId,
|
||||
list[0],
|
||||
Variable.LIST_TYPE,
|
||||
@@ -1263,7 +1264,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
|
||||
if (Object.prototype.hasOwnProperty.call(object, 'broadcasts')) {
|
||||
for (const broadcastId in object.broadcasts) {
|
||||
const broadcast = object.broadcasts[broadcastId];
|
||||
const newBroadcast = new Variable(
|
||||
const newBroadcast = Variable.create(
|
||||
broadcastId,
|
||||
broadcast,
|
||||
Variable.BROADCAST_MESSAGE_TYPE,
|
||||
@@ -1319,12 +1320,6 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
|
||||
if (Object.prototype.hasOwnProperty.call(object, 'isStage')) {
|
||||
target.isStage = object.isStage;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(object, 'targetPaneOrder')) {
|
||||
// Temporarily store the 'targetPaneOrder' property
|
||||
// so that we can correctly order sprites in the target pane.
|
||||
// This will be deleted after we are done parsing and ordering the targets list.
|
||||
target.targetPaneOrder = object.targetPaneOrder;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(object, 'draggable')) {
|
||||
target.draggable = object.draggable;
|
||||
}
|
||||
@@ -1409,7 +1404,9 @@ const deserializeMonitor = function (monitorData, runtime, targets, extensions)
|
||||
|
||||
// If the runtime already has a monitor block for this monitor's id,
|
||||
// update the existing block with the relevant monitor information.
|
||||
const existingMonitorBlock = runtime.monitorBlocks._blocks[monitorData.id];
|
||||
const existingMonitorBlock = hasOwnProperty.call(runtime.monitorBlocks._blocks, monitorData.id) ?
|
||||
runtime.monitorBlocks._blocks[monitorData.id] :
|
||||
null;
|
||||
if (existingMonitorBlock) {
|
||||
// A monitor block already exists if the toolbox has been loaded and
|
||||
// the monitor block is not target specific (because the block gets recycled).
|
||||
@@ -1567,6 +1564,8 @@ const deserialize = async function (json, runtime, zip, isSingleSprite) {
|
||||
|
||||
const monitorObjects = json.monitors || [];
|
||||
|
||||
let layerOrder;
|
||||
|
||||
return fontPromise.then(() => targetObjects.map(target => parseScratchAssets(target, runtime, zip)))
|
||||
// Force this promise to wait for the next loop in the js tick. Let
|
||||
// storage have some time to send off asset requests.
|
||||
@@ -1574,21 +1573,18 @@ const deserialize = async function (json, runtime, zip, isSingleSprite) {
|
||||
.then(assets => Promise.all(targetObjects
|
||||
.map((target, index) =>
|
||||
parseScratchObject(target, runtime, extensions, zip, assets[index]))))
|
||||
.then(targets => targets // Re-sort targets back into original sprite-pane ordering
|
||||
.map((t, i) => {
|
||||
// Add layer order property to deserialized targets.
|
||||
// This property is used to initialize executable targets in
|
||||
// the correct order and is deleted in VM's installTargets function
|
||||
t.layerOrder = i;
|
||||
return t;
|
||||
})
|
||||
.sort((a, b) => a.targetPaneOrder - b.targetPaneOrder)
|
||||
.map(t => {
|
||||
// Delete the temporary properties used for
|
||||
// sprite pane ordering and stage layer ordering
|
||||
delete t.targetPaneOrder;
|
||||
return t;
|
||||
}))
|
||||
.then(targets => {
|
||||
// Re-sort targets back into original sprite-pane ordering.
|
||||
const reordered = targets
|
||||
.map((target, index) => ({
|
||||
target,
|
||||
layerOrder: index,
|
||||
targetPaneOrder: targetObjects[index].targetPaneOrder
|
||||
}))
|
||||
.sort((a, b) => a.targetPaneOrder - b.targetPaneOrder);
|
||||
layerOrder = reordered.map(i => i.layerOrder);
|
||||
return reordered.map(i => i.target);
|
||||
})
|
||||
.then(targets => replaceUnsafeCharsInVariableIds(targets))
|
||||
.then(targets => {
|
||||
monitorObjects.map(monitorDesc => deserializeMonitor(monitorDesc, runtime, targets, extensions));
|
||||
@@ -1599,7 +1595,8 @@ const deserialize = async function (json, runtime, zip, isSingleSprite) {
|
||||
})
|
||||
.then(targets => ({
|
||||
targets,
|
||||
extensions
|
||||
extensions,
|
||||
layerOrder
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -506,24 +506,10 @@ class RenderedTarget extends Target {
|
||||
* this target only has one costume.
|
||||
*/
|
||||
deleteCostume (index) {
|
||||
const originalCostumeCount = this.sprite.costumes.length;
|
||||
if (originalCostumeCount === 1) return null;
|
||||
|
||||
if (index < 0 || index >= originalCostumeCount) {
|
||||
return null;
|
||||
const deletedCostume = this.sprite.deleteCostume(index);
|
||||
if (deletedCostume) {
|
||||
this.runtime.requestTargetsUpdate(this);
|
||||
}
|
||||
|
||||
const deletedCostume = this.sprite.deleteCostumeAt(index);
|
||||
|
||||
if (index === this.currentCostume && index === originalCostumeCount - 1) {
|
||||
this.setCostume(index - 1);
|
||||
} else if (index < this.currentCostume) {
|
||||
this.setCostume(this.currentCostume - 1);
|
||||
} else {
|
||||
this.setCostume(this.currentCostume);
|
||||
}
|
||||
|
||||
this.runtime.requestTargetsUpdate(this);
|
||||
return deletedCostume;
|
||||
}
|
||||
|
||||
@@ -563,13 +549,10 @@ class RenderedTarget extends Target {
|
||||
* @return {object} The deleted sound object, or null if no sound was deleted.
|
||||
*/
|
||||
deleteSound (index) {
|
||||
// Make sure the sound index is not out of bounds
|
||||
if (index < 0 || index >= this.sprite.sounds.length) {
|
||||
return null;
|
||||
const deletedSound = this.sprite.deleteSound(index);
|
||||
if (deletedSound) {
|
||||
this.runtime.requestTargetsUpdate(this);
|
||||
}
|
||||
// Delete the sound at the given index
|
||||
const deletedSound = this.sprite.sounds.splice(index, 1)[0];
|
||||
this.runtime.requestTargetsUpdate(this);
|
||||
return deletedSound;
|
||||
}
|
||||
|
||||
@@ -663,7 +646,7 @@ class RenderedTarget extends Target {
|
||||
if (newIndex === soundIndex) return false;
|
||||
|
||||
const sound = this.sprite.sounds[soundIndex];
|
||||
this.deleteSound(soundIndex);
|
||||
this.sprite.deleteSoundAt(soundIndex);
|
||||
this.addSound(sound, newIndex);
|
||||
return true;
|
||||
}
|
||||
@@ -1110,7 +1093,6 @@ class RenderedTarget extends Target {
|
||||
}
|
||||
this.runtime.stopForTarget(this);
|
||||
this.runtime.removeExecutable(this);
|
||||
this.sprite.removeClone(this);
|
||||
if (this.renderer && this.drawableID !== null) {
|
||||
this.renderer.destroyDrawable(this.drawableID, this.isStage ?
|
||||
StageLayering.BACKGROUND_LAYER :
|
||||
@@ -1120,6 +1102,7 @@ class RenderedTarget extends Target {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
this.sprite.removeClone(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+76
-2
@@ -92,14 +92,73 @@ class Sprite {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a costume by index.
|
||||
* Delete a costume by index. Does not destroy its skin.
|
||||
* @param {number} index Costume index to be deleted
|
||||
* @return {?object} The deleted costume
|
||||
* @returns {object|null} The deleted costume, if any
|
||||
*/
|
||||
deleteCostumeAt (index) {
|
||||
return this.costumes.splice(index, 1)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a costume by index, destroy its skin, and update clones' current costume.
|
||||
* @param {number} index Costume index to be deleted
|
||||
* @returns {object|null} The deleted costume, if any
|
||||
*/
|
||||
deleteCostume (index) {
|
||||
const originalCostumeCount = this.costumes_.length;
|
||||
if (
|
||||
originalCostumeCount === 1 ||
|
||||
index < 0 ||
|
||||
index >= originalCostumeCount
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deletedCostume = this.deleteCostumeAt(index);
|
||||
if (this.runtime.renderer && typeof deletedCostume.skinId === 'number') {
|
||||
this.runtime.renderer.destroySkin(deletedCostume.skinId);
|
||||
}
|
||||
|
||||
for (const clone of this.clones) {
|
||||
if (clone.currentCostume > index) {
|
||||
clone.setCostume(clone.currentCostume - 1);
|
||||
} else if (clone.currentCostume === index) {
|
||||
clone.setCostume(Math.min(index, this.costumes_.length - 1));
|
||||
}
|
||||
}
|
||||
|
||||
return deletedCostume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a sound by index. Does not dispose its sound player.
|
||||
* @param {number} index Sound index to be deleted
|
||||
* @returns {object|null} The deleted sound, if any
|
||||
*/
|
||||
deleteSoundAt (index) {
|
||||
return this.sounds.splice(index, 1)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a sound by index and dispose its sound player.
|
||||
* @param {number} index Sound index to be deleted
|
||||
* @returns {object|null} The deleted sound, if any
|
||||
*/
|
||||
deleteSound (index) {
|
||||
if (index < 0 || index >= this.sounds.length) return null;
|
||||
const deletedSound = this.deleteSoundAt(index);
|
||||
// Older versions of scratch-audio don't have removeSoundPlayer
|
||||
if (
|
||||
this.soundBank &&
|
||||
typeof deletedSound.soundId === 'string' &&
|
||||
typeof this.soundBank.removeSoundPlayer === 'function'
|
||||
) {
|
||||
this.soundBank.removeSoundPlayer(deletedSound.soundId);
|
||||
}
|
||||
return deletedSound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a clone of this sprite.
|
||||
* @param {string=} optLayerGroup Optional layer group the clone's drawable should be added to
|
||||
@@ -133,6 +192,9 @@ class Sprite {
|
||||
if (cloneIndex >= 0) {
|
||||
this.clones.splice(cloneIndex, 1);
|
||||
}
|
||||
if (this.clones.length === 0) {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
duplicate () {
|
||||
@@ -168,8 +230,20 @@ class Sprite {
|
||||
}
|
||||
|
||||
dispose () {
|
||||
if (this.runtime.renderer) {
|
||||
for (const costume of this.costumes_) {
|
||||
if (typeof costume.skinId === 'number') {
|
||||
this.runtime.renderer.destroySkin(costume.skinId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// toJSON() exposes these arrays by reference so it's better to make new ones than
|
||||
// to modify in place and probably break things.
|
||||
this.costumes_ = [];
|
||||
this.sounds = [];
|
||||
if (this.soundBank) {
|
||||
this.soundBank.dispose();
|
||||
this.soundBank = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -21,11 +21,15 @@ const isNotActuallyZero = val => {
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
const code = val.charCodeAt(i);
|
||||
// '0'.charCodeAt(0) === 48
|
||||
// '9'.charCodeAt(0) === 57
|
||||
// Any digit between those (inclusive) mean this is not purely whitespace.
|
||||
// Consider edge cases like 1e-324 before trying to optimize this to just checking for 0.
|
||||
//
|
||||
// '\t'.charCodeAt(0) === 9
|
||||
// We include tab for compatibility with scratch-www's broken trim() polyfill.
|
||||
// https://github.com/TurboWarp/scratch-vm/issues/115
|
||||
// https://scratch.mit.edu/projects/788261699/
|
||||
if (code === 48 || code === 9) {
|
||||
if ((code >= 48 && code <= 57) || code === 9) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-3
@@ -115,8 +115,12 @@ class Color {
|
||||
* @return {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}.
|
||||
*/
|
||||
static hsvToRgb (hsv) {
|
||||
let h = hsv.h % 360;
|
||||
if (h < 0) h += 360;
|
||||
let h = hsv.h;
|
||||
// Equivalent to always doing h % 360, but floating point modulo is slow.
|
||||
if (h < 0 || h >= 360) {
|
||||
h %= 360;
|
||||
if (h < 0) h += 360;
|
||||
}
|
||||
const s = Math.max(0, Math.min(hsv.s, 1));
|
||||
const v = Math.max(0, Math.min(hsv.v, 1));
|
||||
|
||||
@@ -189,7 +193,9 @@ class Color {
|
||||
if (x !== v) {
|
||||
const f = (r === x) ? g - b : ((g === x) ? b - r : r - g);
|
||||
const i = (r === x) ? 3 : ((g === x) ? 5 : 1);
|
||||
h = ((i - (f / (v - x))) * 60) % 360;
|
||||
h = (i - (f / (v - x))) * 60;
|
||||
// Pure red reports as 360 from above. We want to treat that as 0.
|
||||
if (h === 360) h = 0;
|
||||
s = (v - x) / v;
|
||||
}
|
||||
|
||||
|
||||
+88
-45
@@ -27,6 +27,7 @@ const {serializeSounds, serializeCostumes} = require('./serialization/serialize-
|
||||
require('canvas-toBlob');
|
||||
const {exportCostume} = require('./serialization/tw-costume-import-export');
|
||||
const Base64Util = require('./util/base64-util');
|
||||
const Sampler = require('./engine/tw-sampler.js');
|
||||
|
||||
const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_'];
|
||||
|
||||
@@ -222,6 +223,7 @@ class VirtualMachine extends EventEmitter {
|
||||
RenderedTarget,
|
||||
JSZip,
|
||||
Variable,
|
||||
Sampler,
|
||||
|
||||
these_broke_before_and_will_break_again: () => {
|
||||
console.warn('You are using unsupported APIs. WHEN your code breaks, do not expect help.');
|
||||
@@ -770,7 +772,7 @@ class VirtualMachine extends EventEmitter {
|
||||
return Promise.reject('Unable to verify Scratch Project version.');
|
||||
};
|
||||
return deserializePromise()
|
||||
.then(({targets, extensions}) => {
|
||||
.then(({targets, extensions, layerOrder}) => {
|
||||
if (typeof performance !== 'undefined') {
|
||||
performance.mark('scratch-vm-deserialize-end');
|
||||
try {
|
||||
@@ -784,7 +786,7 @@ class VirtualMachine extends EventEmitter {
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
return this.installTargets(targets, extensions, true);
|
||||
return this.installTargets(targets, extensions, true, layerOrder);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -794,15 +796,15 @@ class VirtualMachine extends EventEmitter {
|
||||
*/
|
||||
async _loadExtensions (extensionIDs, extensionURLs = new Map()) {
|
||||
const defaultExtensionURLs = require('./extension-support/tw-default-extension-urls');
|
||||
const extensionPromises = [];
|
||||
const builtinExtensionIDs = [];
|
||||
const customExtensionIDs = [];
|
||||
const customExtensionURLs = [];
|
||||
for (const extensionID of extensionIDs) {
|
||||
if (this.extensionManager.isExtensionLoaded(extensionID)) {
|
||||
// Already loaded
|
||||
} else if (this.extensionManager.isBuiltinExtension(extensionID)) {
|
||||
// Builtin extension
|
||||
this.extensionManager.loadExtensionIdSync(extensionID);
|
||||
builtinExtensionIDs.push(extensionID);
|
||||
} else {
|
||||
// Custom extension
|
||||
let url = extensionURLs.get(extensionID);
|
||||
if (!url && Object.prototype.hasOwnProperty.call(defaultExtensionURLs, extensionID)) {
|
||||
url = defaultExtensionURLs[extensionID];
|
||||
@@ -810,14 +812,25 @@ class VirtualMachine extends EventEmitter {
|
||||
if (!url) {
|
||||
throw new Error(`Unknown extension: ${extensionID}`);
|
||||
}
|
||||
if (await this.securityManager.canLoadExtensionFromProject(url)) {
|
||||
extensionPromises.push(this.extensionManager.loadExtensionURL(url));
|
||||
} else {
|
||||
throw new Error(`Permission to load extension denied: ${extensionID}`);
|
||||
}
|
||||
customExtensionIDs.push(extensionID);
|
||||
customExtensionURLs.push(url);
|
||||
}
|
||||
}
|
||||
return Promise.all(extensionPromises);
|
||||
|
||||
// Ask about every custom extension concurrently, before loading any of them, so that the
|
||||
// security manager can combine them into a single prompt if it wants to.
|
||||
const permissions = await Promise.all(
|
||||
customExtensionURLs.map(url => this.securityManager.canLoadExtensionFromProject(url))
|
||||
);
|
||||
const deniedExtensionIDs = customExtensionIDs.filter((_, i) => !permissions[i]);
|
||||
if (deniedExtensionIDs.length > 0) {
|
||||
throw new Error(`Permission to load extension denied: ${deniedExtensionIDs.join(', ')}`);
|
||||
}
|
||||
|
||||
for (const extensionID of builtinExtensionIDs) {
|
||||
this.extensionManager.loadExtensionIdSync(extensionID);
|
||||
}
|
||||
return Promise.all(customExtensionURLs.map(url => this.extensionManager.loadExtensionURL(url)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -825,11 +838,22 @@ class VirtualMachine extends EventEmitter {
|
||||
* @param {Array.<Target>} targets - the targets to be installed
|
||||
* @param {ImportedExtensionsInfo} extensions - metadata about extensions used by these targets
|
||||
* @param {boolean} wholeProject - set to true if installing a whole project, as opposed to a single sprite.
|
||||
* @param {number[]} [layerOrder] - layer order of each target, same order as targets.
|
||||
* @returns {Promise} resolved once targets have been installed
|
||||
*/
|
||||
async installTargets (targets, extensions, wholeProject) {
|
||||
async installTargets (targets, extensions, wholeProject, layerOrder) {
|
||||
await this.extensionManager.allAsyncExtensionsLoaded();
|
||||
|
||||
let layerOrders = null;
|
||||
if (layerOrder) {
|
||||
layerOrders = new Map();
|
||||
targets.forEach((target, index) => {
|
||||
if (target) {
|
||||
layerOrders.set(target, layerOrder[index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
targets = targets.filter(target => !!target);
|
||||
|
||||
return this._loadExtensions(extensions.extensionIDs, extensions.extensionURLs).then(() => {
|
||||
@@ -839,12 +863,10 @@ class VirtualMachine extends EventEmitter {
|
||||
// Ensure unique sprite name
|
||||
if (target.isSprite()) this.renameSprite(target.id, target.getName());
|
||||
});
|
||||
// Sort the executable targets by layerOrder.
|
||||
// Remove layerOrder property after use.
|
||||
this.runtime.executableTargets.sort((a, b) => a.layerOrder - b.layerOrder);
|
||||
targets.forEach(target => {
|
||||
delete target.layerOrder;
|
||||
});
|
||||
if (layerOrders) {
|
||||
// Sort the executable targets by layer order.
|
||||
this.runtime.executableTargets.sort((a, b) => layerOrders.get(a) - layerOrders.get(b));
|
||||
}
|
||||
|
||||
// Select the first target for editing, e.g., the first sprite.
|
||||
if (wholeProject && (targets.length > 1)) {
|
||||
@@ -935,8 +957,8 @@ class VirtualMachine extends EventEmitter {
|
||||
|
||||
const sb2 = require('./serialization/sb2');
|
||||
return sb2.deserialize(sprite, this.runtime, true, zip)
|
||||
.then(({targets, extensions}) =>
|
||||
this.installTargets(targets, extensions, false));
|
||||
.then(({targets, extensions, layerOrder}) =>
|
||||
this.installTargets(targets, extensions, false, layerOrder));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -950,7 +972,7 @@ class VirtualMachine extends EventEmitter {
|
||||
const sb3 = require('./serialization/sb3');
|
||||
return sb3
|
||||
.deserialize(sprite, this.runtime, zip, true)
|
||||
.then(({targets, extensions}) => this.installTargets(targets, extensions, false));
|
||||
.then(({targets, extensions, layerOrder}) => this.installTargets(targets, extensions, false, layerOrder));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1043,17 +1065,27 @@ class VirtualMachine extends EventEmitter {
|
||||
/**
|
||||
* Delete a costume from the current editing target.
|
||||
* @param {int} costumeIndex - the index of the costume to be removed.
|
||||
* @return {?function} A function to restore the deleted costume, or null,
|
||||
* @returns {(() => Promise<void>)|null} A function to restore the deleted costume, or null
|
||||
* if no costume was deleted.
|
||||
*/
|
||||
deleteCostume (costumeIndex) {
|
||||
const deletedCostume = this.editingTarget.deleteCostume(costumeIndex);
|
||||
const target = this.editingTarget;
|
||||
const deletedCostume = target.deleteCostume(costumeIndex);
|
||||
if (deletedCostume) {
|
||||
const target = this.editingTarget;
|
||||
this.runtime.emitProjectChanged();
|
||||
return () => {
|
||||
target.addCostume(deletedCostume);
|
||||
this.emitTargetsUpdate();
|
||||
const md5ext = `${deletedCostume.assetId}.${deletedCostume.dataFormat}`;
|
||||
return loadCostume(md5ext, deletedCostume, this.runtime).then(() => {
|
||||
if (target.sprite.clones.length === 0) {
|
||||
// Sprite was deleted while we were loading
|
||||
if (this.runtime.renderer && typeof deletedCostume.skinId === 'number') {
|
||||
this.runtime.renderer.destroySkin(deletedCostume.skinId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
target.addCostume(deletedCostume);
|
||||
this.emitTargetsUpdate();
|
||||
});
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -1146,8 +1178,8 @@ class VirtualMachine extends EventEmitter {
|
||||
/**
|
||||
* Delete a sound from the current editing target.
|
||||
* @param {int} soundIndex - the index of the sound to be removed.
|
||||
* @return {?Function} A function to restore the sound that was deleted,
|
||||
* or null, if no sound was deleted.
|
||||
* @returns {(() => Promise<void>)|null} A function to restore the deleted sound, or null
|
||||
* if no sound was deleted.
|
||||
*/
|
||||
deleteSound (soundIndex) {
|
||||
const target = this.editingTarget;
|
||||
@@ -1155,8 +1187,18 @@ class VirtualMachine extends EventEmitter {
|
||||
if (deletedSound) {
|
||||
this.runtime.emitProjectChanged();
|
||||
const restoreFun = () => {
|
||||
target.addSound(deletedSound);
|
||||
this.emitTargetsUpdate();
|
||||
const soundBank = target.sprite.soundBank;
|
||||
return loadSound(deletedSound, this.runtime, soundBank).then(() => {
|
||||
if (target.sprite.clones.length === 0) {
|
||||
// Sprite was deleted while we were loading
|
||||
if (soundBank && typeof soundBank.removeSoundPlayer === 'function') {
|
||||
soundBank.removeSoundPlayer(deletedSound.soundId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
target.addSound(deletedSound);
|
||||
this.emitTargetsUpdate();
|
||||
});
|
||||
};
|
||||
return restoreFun;
|
||||
}
|
||||
@@ -1380,7 +1422,6 @@ class VirtualMachine extends EventEmitter {
|
||||
const target = this.runtime.getTargetById(targetId);
|
||||
|
||||
if (target) {
|
||||
const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id);
|
||||
if (!target.isSprite()) {
|
||||
throw new Error('Cannot delete non-sprite targets.');
|
||||
}
|
||||
@@ -1393,19 +1434,21 @@ class VirtualMachine extends EventEmitter {
|
||||
// Remove monitors from the runtime state and remove the
|
||||
// target-specific monitored blocks (e.g. local variables)
|
||||
target.deleteMonitors();
|
||||
const currentEditingTarget = this.editingTarget;
|
||||
for (let i = 0; i < sprite.clones.length; i++) {
|
||||
const clone = sprite.clones[i];
|
||||
this.runtime.stopForTarget(sprite.clones[i]);
|
||||
this.runtime.disposeTarget(sprite.clones[i]);
|
||||
// Ensure editing target is switched if we are deleting it.
|
||||
if (clone === currentEditingTarget) {
|
||||
const nextTargetIndex = Math.min(this.runtime.targets.length - 1, targetIndexBeforeDelete);
|
||||
if (this.runtime.targets.length > 0){
|
||||
this.setEditingTarget(this.runtime.targets[nextTargetIndex].id);
|
||||
} else {
|
||||
this.editingTarget = null;
|
||||
}
|
||||
const targetIndexBeforeDelete = this.runtime.targets.indexOf(target);
|
||||
|
||||
// disposeTarget removes from sprite.clones, so iterate over a copy
|
||||
const clones = sprite.clones.slice();
|
||||
for (const clone of clones) {
|
||||
this.runtime.stopForTarget(clone);
|
||||
this.runtime.disposeTarget(clone);
|
||||
}
|
||||
|
||||
if (this.editingTarget && this.editingTarget.sprite === sprite) {
|
||||
const targets = this.runtime.targets;
|
||||
if (targets.length > 0) {
|
||||
this.setEditingTarget(targets[Math.min(targets.length - 1, targetIndexBeforeDelete)].id);
|
||||
} else {
|
||||
this.editingTarget = null;
|
||||
}
|
||||
}
|
||||
// Sprite object should be deleted by GC.
|
||||
|
||||
Vendored
+2
@@ -1,3 +1,5 @@
|
||||
Tests in this folder are run in scratch by integration/execute.js. The tests can SAY test messages that map to tap methods. Read integration/execute.js for more.
|
||||
|
||||
Tests whose names start with `tw-` are added by TurboWarp. Some of these tests are based on real projects from the scratch.mit.edu website, in which case their filename contains the original project ID and additional credits are within the project's scripts. We believe our use of these projects is fair use as the projects have been sufficiently modified and stripped down such that the project found here bears not even a slight resemblance to the original project.
|
||||
|
||||
Tests whose names start with `forkphorus-` were imported from https://github.com/forkphorus/forkphorus/tree/master/tests. There is a fair bit of test case sharing between forkphorus and TurboWarp.
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+4
@@ -95,4 +95,8 @@ FakeRenderer.prototype.isTouchingColor = function (a, b) { // eslint-disable-lin
|
||||
|
||||
FakeRenderer.prototype.setLayerGroupOrdering = function (a) {}; // eslint-disable-line no-unused-vars
|
||||
|
||||
FakeRenderer.prototype.destroySkin = function (skinId) {}; // eslint-disable-line no-unused-vars
|
||||
|
||||
FakeRenderer.prototype.destroyDrawable = function (d, layerGroup) {}; // eslint-disable-line no-unused-vars
|
||||
|
||||
module.exports = FakeRenderer;
|
||||
Vendored
+11
-10
@@ -1,14 +1,15 @@
|
||||
const testCompare = (t, lhs, op, rhs, message) => {
|
||||
const details = `Expected: ${lhs} ${op} ${rhs}`;
|
||||
const extra = {details};
|
||||
const assert = require('node:assert');
|
||||
|
||||
const testCompare = (lhs, op, rhs, message) => {
|
||||
const details = `${message}. Expected: ${lhs} ${op} ${rhs}`;
|
||||
switch (op) {
|
||||
case '<': return t.ok(lhs < rhs, message, extra);
|
||||
case '<=': return t.ok(lhs <= rhs, message, extra);
|
||||
case '===': return t.ok(lhs === rhs, message, extra);
|
||||
case '!==': return t.ok(lhs !== rhs, message, extra);
|
||||
case '>=': return t.ok(lhs >= rhs, message, extra);
|
||||
case '>': return t.ok(lhs > rhs, message, extra);
|
||||
default: return t.fail(`Unrecognized op: ${op}`);
|
||||
case '<': return assert.ok(lhs < rhs, details);
|
||||
case '<=': return assert.ok(lhs <= rhs, details);
|
||||
case '===': return assert.ok(lhs === rhs, details);
|
||||
case '!==': return assert.ok(lhs !== rhs, details);
|
||||
case '>=': return assert.ok(lhs >= rhs, details);
|
||||
case '>': return assert.ok(lhs > rhs, details);
|
||||
default: return assert.fail(`Unrecognized op: ${op}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
|
||||
@@ -11,12 +12,11 @@ const project = readFileToBuffer(projectUri);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
test('spec', t => {
|
||||
t.type(vm.addSprite, 'function');
|
||||
t.end();
|
||||
test('spec', () => {
|
||||
assert.strictEqual(typeof vm.addSprite, 'function');
|
||||
});
|
||||
|
||||
test('default cat', t => {
|
||||
test('default cat', (t, done) => {
|
||||
// Get default cat from .sprite2
|
||||
const uri = path.resolve(__dirname, '../fixtures/example_sprite.sprite2');
|
||||
const sprite = readFileToBuffer(uri);
|
||||
@@ -26,50 +26,50 @@ test('default cat', t => {
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
assert.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.loadProject(project).then(() => {
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2); // stage and default sprite
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(typeof targets, 'object');
|
||||
assert.strictEqual(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');
|
||||
assert.ok(newTarget instanceof RenderedTarget);
|
||||
assert.strictEqual(typeof newTarget.id, 'string');
|
||||
assert.strictEqual(typeof newTarget.blocks, 'object');
|
||||
assert.strictEqual(typeof newTarget.variables, 'object');
|
||||
const varIds = Object.keys(newTarget.variables);
|
||||
t.type(varIds.length, 1);
|
||||
assert.strictEqual(varIds.length, 1);
|
||||
const variable = newTarget.variables[varIds[0]];
|
||||
t.equal(variable.name, 'foo');
|
||||
t.equal(variable.value, 0);
|
||||
assert.strictEqual(variable.name, 'foo');
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(newTarget.isOriginal, true);
|
||||
assert.strictEqual(newTarget.currentCostume, 0);
|
||||
assert.strictEqual(newTarget.isOriginal, true);
|
||||
assert.strictEqual(newTarget.isStage, false);
|
||||
assert.strictEqual(newTarget.sprite.name, 'Apple');
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
setTimeout(() => {
|
||||
t.equal(variable.value, 10);
|
||||
assert.strictEqual(variable.value, 10);
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 1000);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,38 +8,38 @@ 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 => {
|
||||
test('importing sb2 project where block comment is converted to workspace comment and block is deleted', (t, done) => {
|
||||
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);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(targetComments.length, 3);
|
||||
const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null);
|
||||
t.equal(spriteWorkspaceComments.length, 2);
|
||||
assert.strictEqual(spriteWorkspaceComments.length, 2);
|
||||
|
||||
// Test the sprite block comments
|
||||
const blockComments = targetComments.filter(comment => !!comment.blockId);
|
||||
t.equal(blockComments.length, 1);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(invalidComments.length, 0);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -8,14 +9,14 @@ const projectUri = path.resolve(__dirname, '../fixtures/block-to-workspace-comme
|
||||
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 => {
|
||||
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, done) => {
|
||||
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);
|
||||
assert.strictEqual(threads.length, 0);
|
||||
|
||||
const target = vm.runtime.targets[1];
|
||||
|
||||
@@ -23,27 +24,27 @@ test('importing sb2 project where block comment is converted to workspace commen
|
||||
// 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);
|
||||
assert.strictEqual(targetComments.length, 1);
|
||||
const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null);
|
||||
t.equal(spriteWorkspaceComments.length, 1);
|
||||
assert.strictEqual(spriteWorkspaceComments.length, 1);
|
||||
|
||||
// Test the sprite block comments
|
||||
const blockComments = targetComments.filter(comment => !!comment.blockId);
|
||||
t.equal(blockComments.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(invalidComments.length, 0);
|
||||
|
||||
const targetBlocks = Object.values(target.blocks._blocks);
|
||||
t.equal(targetBlocks.length, 0);
|
||||
assert.strictEqual(targetBlocks.length, 0);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -10,16 +11,16 @@ 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 => {
|
||||
test('importing sb2 project with special chars in message names', (t, done) => {
|
||||
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);
|
||||
assert.strictEqual(threads.length, 0);
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
assert.strictEqual(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const cat = vm.runtime.targets[1];
|
||||
@@ -31,13 +32,13 @@ test('importing sb2 project with special chars in message names', t => {
|
||||
// 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);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(StringUtil.replaceUnsafeChars(abMessage.id), abMessage.id);
|
||||
assert.strictEqual(abMessage.id, abMessageId);
|
||||
assert.strictEqual(abMessage.type, Variable.BROADCAST_MESSAGE_TYPE);
|
||||
assert.strictEqual(abMessage.value, 'a&b');
|
||||
|
||||
|
||||
const ltPerfectMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === '< perfect')[0];
|
||||
@@ -45,31 +46,31 @@ test('importing sb2 project with special chars in message names', t => {
|
||||
// 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);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(StringUtil.replaceUnsafeChars(ltPerfectMessage.id), ltPerfectMessage.id);
|
||||
assert.strictEqual(ltPerfectMessage.id, ltPerfectMessageId);
|
||||
assert.strictEqual(ltPerfectMessage.type, Variable.BROADCAST_MESSAGE_TYPE);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(allBroadcastFields[ltPerfectMessageId].length, 1);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(catMessageBlocks.length, 2);
|
||||
assert.strictEqual(catMessageBlocks[0].fields.BROADCAST_OPTION.id, ltPerfectMessageId);
|
||||
assert.strictEqual(catMessageBlocks[1].fields.BROADCAST_OPTION.id, abMessageId);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -10,16 +11,16 @@ 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 => {
|
||||
test('importing sb3 project with special chars in message names', (t, done) => {
|
||||
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);
|
||||
assert.strictEqual(threads.length, 0);
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
assert.strictEqual(vm.runtime.targets.length, 2);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const cat = vm.runtime.targets[1];
|
||||
@@ -31,13 +32,13 @@ test('importing sb3 project with special chars in message names', t => {
|
||||
// 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);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(StringUtil.replaceUnsafeChars(abMessage.id), abMessage.id);
|
||||
assert.strictEqual(abMessage.id, abMessageId);
|
||||
assert.strictEqual(abMessage.type, Variable.BROADCAST_MESSAGE_TYPE);
|
||||
assert.strictEqual(abMessage.value, 'a&b');
|
||||
|
||||
|
||||
const ltPerfectMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === '< perfect')[0];
|
||||
@@ -45,31 +46,31 @@ test('importing sb3 project with special chars in message names', t => {
|
||||
// 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);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(StringUtil.replaceUnsafeChars(ltPerfectMessage.id), ltPerfectMessage.id);
|
||||
assert.strictEqual(ltPerfectMessage.id, ltPerfectMessageId);
|
||||
assert.strictEqual(ltPerfectMessage.type, Variable.BROADCAST_MESSAGE_TYPE);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(allBroadcastFields[ltPerfectMessageId].length, 1);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(catMessageBlocks.length, 2);
|
||||
assert.strictEqual(catMessageBlocks[0].fields.BROADCAST_OPTION.id, ltPerfectMessageId);
|
||||
assert.strictEqual(catMessageBlocks[1].fields.BROADCAST_OPTION.id, abMessageId);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,7 +8,7 @@ const VirtualMachine = require('../../src/index');
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('clone-cleanup', t => {
|
||||
test('clone-cleanup', (t, done) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -19,19 +20,19 @@ test('clone-cleanup', t => {
|
||||
|
||||
const verifyCounts = (expectedClones, extraThreads) => {
|
||||
// stage plus one sprite, plus clones
|
||||
t.strictEqual(vm.runtime.targets.length, 2 + expectedClones,
|
||||
assert.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,
|
||||
assert.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,
|
||||
assert.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),
|
||||
assert.strictEqual(vm.runtime.threads.length, extraThreads + (2 * expectedClones),
|
||||
`thread count at step ${testStep}`);
|
||||
};
|
||||
|
||||
@@ -66,13 +67,13 @@ test('clone-cleanup', t => {
|
||||
verifyCounts(0, 0);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -14,7 +15,7 @@ const cloudVarLimit = readFileToBuffer(cloudVarLimitUri);
|
||||
const cloudVarExceededLimit = readFileToBuffer(cloudVarExceededLimitUri);
|
||||
const cloudVarLocal = readFileToBuffer(cloudVarLocalUri);
|
||||
|
||||
test('importing an sb2 project with cloud variables', t => {
|
||||
test('importing an sb2 project with cloud variables', async () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -23,26 +24,24 @@ test('importing an sb2 project with cloud variables', t => {
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarSimple).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
await vm.loadProject(cloudVarSimple);
|
||||
assert.strictEqual(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
t.equal(stageVars.length, 1);
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
assert.strictEqual(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);
|
||||
const variable = stageVars[0];
|
||||
assert.strictEqual(variable.name, '☁ firstCloud');
|
||||
assert.strictEqual(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
|
||||
assert.strictEqual(variable.isCloud, true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
|
||||
test('importing an sb2 project with cloud variables at the limit for a project', t => {
|
||||
test('importing an sb2 project with cloud variables at the limit for a project', async () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -51,22 +50,20 @@ test('importing an sb2 project with cloud variables at the limit for a project',
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarLimit).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
await vm.loadProject(cloudVarLimit);
|
||||
assert.strictEqual(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
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);
|
||||
assert.strictEqual(stageVars.length, 10);
|
||||
// All of the 8 stage variables should be cloud variables
|
||||
assert.strictEqual(stageVars.filter(v => v.isCloud).length, 10);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
|
||||
test('importing an sb2 project with cloud variables exceeding the limit for a project', t => {
|
||||
test('importing an sb2 project with cloud variables exceeding the limit for a project', async () => {
|
||||
// This tests a hacked project where additional cloud variables exceeding
|
||||
// the project limit have been added.
|
||||
const vm = new VirtualMachine();
|
||||
@@ -77,22 +74,20 @@ test('importing an sb2 project with cloud variables exceeding the limit for a pr
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarExceededLimit).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
await vm.loadProject(cloudVarExceededLimit);
|
||||
assert.strictEqual(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
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);
|
||||
assert.strictEqual(stageVars.length, 15);
|
||||
// Only 8 of the variables should have the isCloud flag set to true
|
||||
assert.strictEqual(stageVars.filter(v => v.isCloud).length, 10);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
|
||||
test('importing one project after the other resets cloud variable limit', t => {
|
||||
test('importing one project after the other resets cloud variable limit', async () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -101,30 +96,27 @@ test('importing one project after the other resets cloud variable limit', t => {
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarExceededLimit).then(() => {
|
||||
t.equal(vm.runtime.canAddCloudVariable(), false);
|
||||
await vm.loadProject(cloudVarExceededLimit);
|
||||
assert.strictEqual(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);
|
||||
await vm.loadProject(cloudVarSimple);
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
assert.strictEqual(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);
|
||||
const variable = stageVars[0];
|
||||
assert.strictEqual(variable.name, '☁ firstCloud');
|
||||
assert.strictEqual(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
|
||||
assert.strictEqual(variable.isCloud, true);
|
||||
|
||||
t.equal(vm.runtime.canAddCloudVariable(), true);
|
||||
assert.strictEqual(vm.runtime.canAddCloudVariable(), true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
|
||||
test('local cloud variables get imported as regular variables', t => {
|
||||
test('local cloud variables get imported as regular variables', async () => {
|
||||
// This tests a hacked project where a sprite-local variable is
|
||||
// has the cloud variable flag set.
|
||||
const vm = new VirtualMachine();
|
||||
@@ -135,21 +127,19 @@ test('local cloud variables get imported as regular variables', t => {
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarLocal).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), false);
|
||||
await vm.loadProject(cloudVarLocal);
|
||||
assert.strictEqual(vm.runtime.hasCloudData(), false);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
|
||||
t.equal(stageVars.length, 0);
|
||||
assert.strictEqual(stageVars.length, 0);
|
||||
|
||||
const sprite = vm.runtime.targets[1];
|
||||
const spriteVars = Object.values(sprite.variables);
|
||||
const sprite = vm.runtime.targets[1];
|
||||
const spriteVars = Object.values(sprite.variables);
|
||||
|
||||
t.equal(spriteVars.length, 1);
|
||||
t.equal(spriteVars[0].isCloud, false);
|
||||
assert.strictEqual(spriteVars.length, 1);
|
||||
assert.strictEqual(spriteVars[0].isCloud, false);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -14,7 +15,7 @@ const cloudVarLimit = readFileToBuffer(cloudVarLimitUri);
|
||||
const cloudVarExceededLimit = readFileToBuffer(cloudVarExceededLimitUri);
|
||||
const cloudVarLocal = readFileToBuffer(cloudVarLocalUri);
|
||||
|
||||
test('importing an sb3 project with cloud variables', t => {
|
||||
test('importing an sb3 project with cloud variables', async () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -23,24 +24,22 @@ test('importing an sb3 project with cloud variables', t => {
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarSimple).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
await vm.loadProject(cloudVarSimple);
|
||||
assert.strictEqual(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
t.equal(stageVars.length, 1);
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
assert.strictEqual(stageVars.length, 1);
|
||||
|
||||
const variable = stageVars[0];
|
||||
t.equal(variable.name, '☁ firstCloud');
|
||||
t.equal(Number(variable.value), 100);
|
||||
t.equal(variable.isCloud, true);
|
||||
const variable = stageVars[0];
|
||||
assert.strictEqual(variable.name, '☁ firstCloud');
|
||||
assert.strictEqual(Number(variable.value), 100);
|
||||
assert.strictEqual(variable.isCloud, true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
|
||||
test('importing an sb3 project with cloud variables at the limit for a project', t => {
|
||||
test('importing an sb3 project with cloud variables at the limit for a project', async () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -49,22 +48,20 @@ test('importing an sb3 project with cloud variables at the limit for a project',
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarLimit).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
await vm.loadProject(cloudVarLimit);
|
||||
assert.strictEqual(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
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);
|
||||
assert.strictEqual(stageVars.length, 10);
|
||||
// All of the 10 stage variables should be cloud variables
|
||||
assert.strictEqual(stageVars.filter(v => v.isCloud).length, 10);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
|
||||
test('importing an sb3 project with cloud variables exceeding the limit for a project', t => {
|
||||
test('importing an sb3 project with cloud variables exceeding the limit for a project', async () => {
|
||||
// This tests a hacked project where additional cloud variables exceeding
|
||||
// the project limit have been added.
|
||||
const vm = new VirtualMachine();
|
||||
@@ -75,22 +72,20 @@ test('importing an sb3 project with cloud variables exceeding the limit for a pr
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarExceededLimit).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), true);
|
||||
await vm.loadProject(cloudVarExceededLimit);
|
||||
assert.strictEqual(vm.runtime.hasCloudData(), true);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
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);
|
||||
assert.strictEqual(stageVars.length, 15);
|
||||
// Only 8 of the variables should have the isCloud flag set to true
|
||||
assert.strictEqual(stageVars.filter(v => v.isCloud).length, 10);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
|
||||
test('importing one project after the other resets cloud variable limit', t => {
|
||||
test('importing one project after the other resets cloud variable limit', async () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -99,28 +94,25 @@ test('importing one project after the other resets cloud variable limit', t => {
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarExceededLimit).then(() => {
|
||||
t.equal(vm.runtime.canAddCloudVariable(), false);
|
||||
await vm.loadProject(cloudVarExceededLimit);
|
||||
assert.strictEqual(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);
|
||||
await vm.loadProject(cloudVarSimple);
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
assert.strictEqual(stageVars.length, 1);
|
||||
|
||||
const variable = stageVars[0];
|
||||
t.equal(variable.name, '☁ firstCloud');
|
||||
t.equal(Number(variable.value), 100);
|
||||
t.equal(variable.isCloud, true);
|
||||
const variable = stageVars[0];
|
||||
assert.strictEqual(variable.name, '☁ firstCloud');
|
||||
assert.strictEqual(Number(variable.value), 100);
|
||||
assert.strictEqual(variable.isCloud, true);
|
||||
|
||||
t.equal(vm.runtime.canAddCloudVariable(), true);
|
||||
assert.strictEqual(vm.runtime.canAddCloudVariable(), true);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
|
||||
test('local cloud variables get imported as regular variables', t => {
|
||||
test('local cloud variables get imported as regular variables', async () => {
|
||||
// This tests a hacked project where a sprite-local variable is
|
||||
// has the cloud variable flag set.
|
||||
const vm = new VirtualMachine();
|
||||
@@ -131,21 +123,19 @@ test('local cloud variables get imported as regular variables', t => {
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(cloudVarLocal).then(() => {
|
||||
t.equal(vm.runtime.hasCloudData(), false);
|
||||
await vm.loadProject(cloudVarLocal);
|
||||
assert.strictEqual(vm.runtime.hasCloudData(), false);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
const stage = vm.runtime.targets[0];
|
||||
const stageVars = Object.values(stage.variables);
|
||||
|
||||
t.equal(stageVars.length, 0);
|
||||
assert.strictEqual(stageVars.length, 0);
|
||||
|
||||
const sprite = vm.runtime.targets[1];
|
||||
const spriteVars = Object.values(sprite.variables);
|
||||
const sprite = vm.runtime.targets[1];
|
||||
const spriteVars = Object.values(sprite.variables);
|
||||
|
||||
t.equal(spriteVars.length, 1);
|
||||
t.equal(spriteVars[0].isCloud, false);
|
||||
assert.strictEqual(spriteVars.length, 1);
|
||||
assert.strictEqual(spriteVars[0].isCloud, false);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
});
|
||||
vm.quit();
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,14 +8,14 @@ const VirtualMachine = require('../../src/index');
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/comments.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
test('importing sb2 project with comments', t => {
|
||||
test('importing sb2 project with comments', (t, done) => {
|
||||
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);
|
||||
assert.strictEqual(threads.length, 0);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const target = vm.runtime.targets[1];
|
||||
@@ -22,60 +23,60 @@ test('importing sb2 project with comments', t => {
|
||||
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.');
|
||||
assert.strictEqual(stageComments.length, 1);
|
||||
assert.strictEqual(stageComments[0].minimized, true);
|
||||
assert.strictEqual(stageComments[0].text, 'A minimized stage comment.');
|
||||
// The stage comment is a workspace comment
|
||||
t.equal(stageComments[0].blockId, null);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(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.');
|
||||
assert.strictEqual(spriteWorkspaceComments.length, 1);
|
||||
assert.strictEqual(spriteWorkspaceComments[0].minimized, false);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(blockComments.length, 5);
|
||||
|
||||
t.equal(blockComments[0].minimized, true);
|
||||
t.equal(blockComments[0].text, '1. Green Flag Comment.');
|
||||
assert.strictEqual(blockComments[0].minimized, true);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(greenFlagBlock.comment, blockComments[0].id);
|
||||
assert.strictEqual(greenFlagBlock.opcode, 'event_whenflagclicked');
|
||||
|
||||
t.equal(blockComments[1].minimized, true);
|
||||
t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.');
|
||||
assert.strictEqual(blockComments[1].minimized, true);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(turnRightBlock.comment, blockComments[1].id);
|
||||
assert.strictEqual(turnRightBlock.opcode, 'motion_turnright');
|
||||
|
||||
t.equal(blockComments[2].minimized, false);
|
||||
t.equal(blockComments[2].text, '3. Comment for a loop.');
|
||||
assert.strictEqual(blockComments[2].minimized, false);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(repeatBlock.comment, blockComments[2].id);
|
||||
assert.strictEqual(repeatBlock.opcode, 'control_repeat');
|
||||
|
||||
t.equal(blockComments[3].minimized, false);
|
||||
t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.');
|
||||
assert.strictEqual(blockComments[3].minimized, false);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(changeColorBlock.comment, blockComments[3].id);
|
||||
assert.strictEqual(changeColorBlock.opcode, 'looks_changeeffectby');
|
||||
|
||||
t.equal(blockComments[4].minimized, false);
|
||||
t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.');
|
||||
assert.strictEqual(blockComments[4].minimized, false);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(stopAllBlock.comment, blockComments[4].id);
|
||||
assert.strictEqual(stopAllBlock.opcode, 'control_stop');
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,14 +8,14 @@ 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 => {
|
||||
test('load an sb3 project with comments', (t, done) => {
|
||||
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);
|
||||
assert.strictEqual(threads.length, 0);
|
||||
|
||||
const stage = vm.runtime.targets[0];
|
||||
const target = vm.runtime.targets[1];
|
||||
@@ -22,60 +23,60 @@ test('load an sb3 project with comments', t => {
|
||||
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.');
|
||||
assert.strictEqual(stageComments.length, 1);
|
||||
assert.strictEqual(stageComments[0].minimized, true);
|
||||
assert.strictEqual(stageComments[0].text, 'A minimized stage comment.');
|
||||
// The stage comment is a workspace comment
|
||||
t.equal(stageComments[0].blockId, null);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(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.');
|
||||
assert.strictEqual(spriteWorkspaceComments.length, 1);
|
||||
assert.strictEqual(spriteWorkspaceComments[0].minimized, false);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(blockComments.length, 5);
|
||||
|
||||
t.equal(blockComments[0].minimized, true);
|
||||
t.equal(blockComments[0].text, '1. Green Flag Comment.');
|
||||
assert.strictEqual(blockComments[0].minimized, true);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(greenFlagBlock.comment, blockComments[0].id);
|
||||
assert.strictEqual(greenFlagBlock.opcode, 'event_whenflagclicked');
|
||||
|
||||
t.equal(blockComments[1].minimized, true);
|
||||
t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.');
|
||||
assert.strictEqual(blockComments[1].minimized, true);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(turnRightBlock.comment, blockComments[1].id);
|
||||
assert.strictEqual(turnRightBlock.opcode, 'motion_turnright');
|
||||
|
||||
t.equal(blockComments[2].minimized, false);
|
||||
t.equal(blockComments[2].text, '3. Comment for a loop.');
|
||||
assert.strictEqual(blockComments[2].minimized, false);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(repeatBlock.comment, blockComments[2].id);
|
||||
assert.strictEqual(repeatBlock.opcode, 'control_repeat');
|
||||
|
||||
t.equal(blockComments[3].minimized, false);
|
||||
t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.');
|
||||
assert.strictEqual(blockComments[3].minimized, false);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(changeColorBlock.comment, blockComments[3].id);
|
||||
assert.strictEqual(changeColorBlock.opcode, 'looks_changeeffectby');
|
||||
|
||||
t.equal(blockComments[4].minimized, false);
|
||||
t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.');
|
||||
assert.strictEqual(blockComments[4].minimized, false);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(stopAllBlock.comment, blockComments[4].id);
|
||||
assert.strictEqual(stopAllBlock.opcode, 'control_stop');
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -11,16 +12,16 @@ const project = readFileToBuffer(projectUri);
|
||||
const spriteUri = path.resolve(__dirname, '../fixtures/sprite.json');
|
||||
const sprite = fs.readFileSync(spriteUri, 'utf8');
|
||||
|
||||
test('complex', t => {
|
||||
test('complex', (t, done) => {
|
||||
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);
|
||||
assert.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Manipulate each target
|
||||
@@ -55,7 +56,7 @@ test('complex', t => {
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,20 +8,20 @@ const VirtualMachine = require('../../src/index');
|
||||
const uri = path.resolve(__dirname, '../fixtures/control.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('control', t => {
|
||||
test('control', (t, done) => {
|
||||
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);
|
||||
assert.ok(threads.length > 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,7 +8,7 @@ const VirtualMachine = require('../../src/index');
|
||||
const uri = path.resolve(__dirname, '../fixtures/data.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('data', t => {
|
||||
test('data', (t, done) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -15,11 +16,11 @@ test('data', t => {
|
||||
vm.on('playgroundData', () => {
|
||||
// @todo Additional tests
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
|
||||
@@ -11,12 +12,11 @@ const project = readFileToBuffer(projectUri);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
test('spec', t => {
|
||||
t.type(vm.deleteSprite, 'function');
|
||||
t.end();
|
||||
test('spec', () => {
|
||||
assert.strictEqual(typeof vm.deleteSprite, 'function');
|
||||
});
|
||||
|
||||
test('default cat', t => {
|
||||
test('default cat', (t, done) => {
|
||||
// Get default cat from .sprite2
|
||||
// const uri = path.resolve(__dirname, '../fixtures/example_sprite.sprite2');
|
||||
// const sprite = readFileToBuffer(uri);
|
||||
@@ -26,32 +26,32 @@ test('default cat', t => {
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
assert.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.loadProject(project).then(() => {
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2); // stage and default sprite
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(vm.runtime.targets.length, 1);
|
||||
|
||||
t.type(addSpriteBack, 'function');
|
||||
assert.strictEqual(typeof addSpriteBack, 'function');
|
||||
|
||||
addSpriteBack().then(() => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
t.equal(vm.runtime.targets[1].getName(), defaultSprite.getName());
|
||||
assert.strictEqual(vm.runtime.targets.length, 2);
|
||||
assert.strictEqual(vm.runtime.targets[1].getName(), defaultSprite.getName());
|
||||
|
||||
vm.greenFlag();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,20 +8,20 @@ const VirtualMachine = require('../../src/index');
|
||||
const uri = path.resolve(__dirname, '../fixtures/event.sb2');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('event', t => {
|
||||
test('event', (t, done) => {
|
||||
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);
|
||||
assert.ok(threads.length > 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
+35
-33
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
@@ -14,15 +14,15 @@ const VirtualMachine = require('../../src/index');
|
||||
* 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).
|
||||
* - "pass MESSAGE" will pass an assertion with MESSAGE.
|
||||
* - "fail MESSAGE" will fail the test with MESSAGE once the project finishes.
|
||||
* - "plan NUMBER_OF_TESTS" will t.plan(Number(NUMBER_OF_TESTS)).
|
||||
* - "end" will t.end().
|
||||
* - "end" marks the test as having reached its 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
|
||||
* When its done you must SAY "end" so the test knows that the end has
|
||||
* been reached.
|
||||
*/
|
||||
|
||||
@@ -43,16 +43,15 @@ const whenThreadsComplete = (t, vm, uri, timeLimit = 5000) =>
|
||||
}, 50);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
t.fail(`Timeout waiting for threads to complete: ${uri}`);
|
||||
reject(new Error('time limit reached'));
|
||||
|
||||
// Attempt to stop the lingering VM from interfering with other tests.
|
||||
vm.quit();
|
||||
|
||||
reject(new Error(`Timeout waiting for threads to complete: ${uri}`));
|
||||
}, timeLimit);
|
||||
|
||||
// Clear the interval to allow the process to exit
|
||||
// naturally.
|
||||
t.tearDown(() => {
|
||||
t.after(() => {
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
@@ -66,22 +65,25 @@ const fileFilter = /\.sb[23]?$/i;
|
||||
fs.readdirSync(executeDir)
|
||||
.filter(uri => fileFilter.test(uri))
|
||||
.forEach(uri => {
|
||||
const run = (t, enableCompiler) => {
|
||||
const run = async (t, enableCompiler) => {
|
||||
const vm = new VirtualMachine();
|
||||
// Stop a VM left running by a failed test from leaking into later tests.
|
||||
t.after(() => vm.quit());
|
||||
|
||||
// Map string messages to tap reporting methods. This will be used
|
||||
// Map string messages to test reporting methods. This will be used
|
||||
// with events from scratch's runtime emitted on block instructions.
|
||||
let didPlan;
|
||||
let didEnd;
|
||||
const failures = [];
|
||||
const reporters = {
|
||||
comment (message) {
|
||||
t.comment(message);
|
||||
t.diagnostic(message);
|
||||
},
|
||||
pass (reason) {
|
||||
t.pass(reason);
|
||||
t.assert.ok(true, reason);
|
||||
},
|
||||
fail (reason) {
|
||||
t.fail(reason);
|
||||
failures.push(reason);
|
||||
},
|
||||
plan (count) {
|
||||
didPlan = true;
|
||||
@@ -90,7 +92,6 @@ fs.readdirSync(executeDir)
|
||||
end () {
|
||||
didEnd = true;
|
||||
vm.quit();
|
||||
t.end();
|
||||
}
|
||||
};
|
||||
const reportVmResult = text => {
|
||||
@@ -128,25 +129,26 @@ fs.readdirSync(executeDir)
|
||||
|
||||
// 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"');
|
||||
}
|
||||
await vm.loadProject(project);
|
||||
await vm.greenFlag();
|
||||
await whenThreadsComplete(t, vm, uri);
|
||||
// Setting a plan is not required but is a good idea.
|
||||
if (!didPlan) {
|
||||
t.diagnostic('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();
|
||||
}
|
||||
});
|
||||
// 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) {
|
||||
vm.quit();
|
||||
failures.push('did not say "end"');
|
||||
}
|
||||
|
||||
// Not an assertion, as it would count towards the project's plan.
|
||||
if (failures.length) {
|
||||
throw new Error(failures.join('\n'));
|
||||
}
|
||||
};
|
||||
test(`${uri} (interpreted)`, t => run(t, false));
|
||||
test(`${uri} (compiled)`, t => run(t, true));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -8,19 +9,15 @@ 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
|
||||
});
|
||||
});
|
||||
test(`${name} - interpreted`, (t, done) => callback(done, {
|
||||
enabled: false
|
||||
}));
|
||||
test(`${name} - compiled`, (t, done) => callback(done, {
|
||||
enabled: true
|
||||
}));
|
||||
};
|
||||
|
||||
compilerAndInterpreter('complex', (t, co) => {
|
||||
compilerAndInterpreter('complex', (done, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
@@ -28,18 +25,18 @@ compilerAndInterpreter('complex', (t, co) => {
|
||||
// Evaluate playground data and exit
|
||||
vm.on('playgroundData', e => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
assert.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']);
|
||||
assert.deepEqual(results, ['3', '2', '1', 'stage']);
|
||||
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -8,32 +9,28 @@ 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
|
||||
});
|
||||
});
|
||||
test(`${name} - interpreted`, (t, done) => callback(done, {
|
||||
enabled: false
|
||||
}));
|
||||
test(`${name} - compiled`, (t, done) => callback(done, {
|
||||
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 checkIsHatThread = (vm, hatThread) => {
|
||||
assert.strictEqual(hatThread.stackClick, false);
|
||||
assert.strictEqual(hatThread.updateMonitor, false);
|
||||
const blockContainer = hatThread.target.blocks;
|
||||
const opcode = blockContainer.getOpcode(blockContainer.getBlock(hatThread.topBlock));
|
||||
t.assert(vm.runtime.getIsEdgeActivatedHat(opcode));
|
||||
assert.ok(vm.runtime.getIsEdgeActivatedHat(opcode));
|
||||
};
|
||||
|
||||
const checkIsStackClickThread = (t, vm, stackClickThread) => {
|
||||
t.equal(stackClickThread.stackClick, true);
|
||||
t.equal(stackClickThread.updateMonitor, false);
|
||||
const checkIsStackClickThread = (vm, stackClickThread) => {
|
||||
assert.strictEqual(stackClickThread.stackClick, true);
|
||||
assert.strictEqual(stackClickThread.updateMonitor, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,13 +40,13 @@ const checkIsStackClickThread = (t, vm, stackClickThread) => {
|
||||
* 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) => {
|
||||
compilerAndInterpreter('edge activated hat thread runs once every frame', (done, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
vm.clear();
|
||||
@@ -57,23 +54,23 @@ compilerAndInterpreter('edge activated hat thread runs once every frame', (t, co
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(threads.length, 1);
|
||||
checkIsHatThread(vm, threads[0]);
|
||||
assert.ok(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();
|
||||
assert.strictEqual(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(threads.length, 1);
|
||||
checkIsHatThread(vm, threads[0]);
|
||||
assert.ok(threads[0].status === Thread.STATUS_DONE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -83,13 +80,13 @@ compilerAndInterpreter('edge activated hat thread runs once every frame', (t, co
|
||||
* 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) => {
|
||||
compilerAndInterpreter('edge activated hat thread runs after being added to previously executed target', (done, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
vm.clear();
|
||||
@@ -97,14 +94,14 @@ compilerAndInterpreter('edge activated hat thread runs after being added to prev
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(threads.length, 1);
|
||||
checkIsHatThread(vm, threads[0]);
|
||||
assert.ok(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);
|
||||
@@ -115,13 +112,13 @@ compilerAndInterpreter('edge activated hat thread runs after being added to prev
|
||||
// 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();
|
||||
assert.strictEqual(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(threads.length, 2);
|
||||
checkIsHatThread(vm, threads[0]);
|
||||
checkIsHatThread(vm, threads[1]);
|
||||
assert.ok(threads[0].status === Thread.STATUS_DONE);
|
||||
assert.ok(threads[1].status === Thread.STATUS_DONE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -130,13 +127,13 @@ compilerAndInterpreter('edge activated hat thread runs after being added to prev
|
||||
* 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) => {
|
||||
compilerAndInterpreter('edge activated hat thread not added twice', (done, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
vm.clear();
|
||||
@@ -144,25 +141,25 @@ compilerAndInterpreter('edge activated hat thread not added twice', (t, co) => {
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 1);
|
||||
assert.strictEqual(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);
|
||||
checkIsHatThread(vm, vm.runtime.threads[0]);
|
||||
assert.ok(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();
|
||||
assert.strictEqual(vm.runtime.threads.length, 1);
|
||||
assert.strictEqual(doneThreads.length, 0);
|
||||
checkIsHatThread(vm, vm.runtime.threads[0]);
|
||||
assert.ok(vm.runtime.threads[0] === prevThread);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -172,7 +169,7 @@ compilerAndInterpreter('edge activated hat thread not added twice', (t, co) => {
|
||||
* 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) => {
|
||||
compilerAndInterpreter('edge activated hat should trigger for both sprites when sprite is duplicated', (done, co) => {
|
||||
|
||||
// Project that is similar to timer-greater-than-hat.sb2, but has code on the sprite so that
|
||||
// the sprite can be duplicated
|
||||
@@ -184,7 +181,7 @@ compilerAndInterpreter('edge activated hat should trigger for both sprites when
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
vm.clear();
|
||||
@@ -192,15 +189,15 @@ compilerAndInterpreter('edge activated hat should trigger for both sprites when
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(projectWithSprite).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 1);
|
||||
checkIsHatThread(vm, vm.runtime.threads[0]);
|
||||
assert.ok(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);
|
||||
assert.strictEqual(numTargetEdgeHats, 1);
|
||||
|
||||
vm.duplicateSprite(vm.runtime.targets[1].id).then(() => {
|
||||
vm.runtime._step();
|
||||
@@ -208,8 +205,8 @@ compilerAndInterpreter('edge activated hat should trigger for both sprites when
|
||||
// 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();
|
||||
assert.strictEqual(numTargetEdgeHats, 2);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -220,7 +217,7 @@ compilerAndInterpreter('edge activated hat should trigger for both sprites when
|
||||
* 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) => {
|
||||
compilerAndInterpreter('edge activated hat should trigger for both sprites when sprite is cloned', (done, co) => {
|
||||
|
||||
// Project that is similar to loudness-hat-block.sb2, but has code on the sprite so that
|
||||
// the sprite can be duplicated
|
||||
@@ -232,7 +229,7 @@ compilerAndInterpreter('edge activated hat should trigger for both sprites when
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
vm.clear();
|
||||
@@ -240,18 +237,18 @@ compilerAndInterpreter('edge activated hat should trigger for both sprites when
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(projectWithSprite).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 1);
|
||||
checkIsHatThread(vm, vm.runtime.threads[0]);
|
||||
assert.ok(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);
|
||||
assert.strictEqual(numTargetEdgeHats, 1);
|
||||
|
||||
const cloneTarget = vm.runtime.targets[1].makeClone();
|
||||
vm.runtime.addTarget(cloneTarget);
|
||||
@@ -262,8 +259,8 @@ compilerAndInterpreter('edge activated hat should trigger for both sprites when
|
||||
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();
|
||||
assert.strictEqual(numTargetEdgeHats, 2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -272,13 +269,13 @@ compilerAndInterpreter('edge activated hat should trigger for both sprites when
|
||||
* 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) => {
|
||||
compilerAndInterpreter('edge activated hat thread does not interrupt stack click thread', (done, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
vm.clear();
|
||||
@@ -286,14 +283,14 @@ compilerAndInterpreter('edge activated hat thread does not interrupt stack click
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(doneThreads.length, 1);
|
||||
checkIsHatThread(vm, doneThreads[0]);
|
||||
assert.ok(doneThreads[0].status === Thread.STATUS_DONE);
|
||||
|
||||
// Add stack click thread on this hat
|
||||
vm.runtime.toggleScript(doneThreads[0].topBlock, {stackClick: true});
|
||||
@@ -301,8 +298,8 @@ compilerAndInterpreter('edge activated hat thread does not interrupt stack click
|
||||
// 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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(doneThreads.length, 2);
|
||||
let hatThread;
|
||||
let stackClickThread;
|
||||
if (doneThreads[0].stackClick) {
|
||||
@@ -312,11 +309,11 @@ compilerAndInterpreter('edge activated hat thread does not interrupt stack click
|
||||
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();
|
||||
checkIsHatThread(vm, hatThread);
|
||||
checkIsStackClickThread(vm, stackClickThread);
|
||||
assert.ok(doneThreads[0].status === Thread.STATUS_DONE);
|
||||
assert.ok(doneThreads[1].status === Thread.STATUS_DONE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -325,13 +322,13 @@ compilerAndInterpreter('edge activated hat thread does not interrupt stack click
|
||||
* 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) => {
|
||||
compilerAndInterpreter('edge activated hat thread does not interrupt stack click thread', (done, co) => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.setCompilerOptions(co);
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
// Note: don't run vm.start(), we handle calling _step() manually in this test
|
||||
vm.runtime.currentStepTime = 0;
|
||||
vm.clear();
|
||||
@@ -339,14 +336,14 @@ compilerAndInterpreter('edge activated hat thread does not interrupt stack click
|
||||
vm.setTurboMode(false);
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 1);
|
||||
assert.strictEqual(doneThreads.length, 0);
|
||||
checkIsHatThread(vm, vm.runtime.threads[0]);
|
||||
assert.ok(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
|
||||
|
||||
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
|
||||
|
||||
@@ -356,8 +353,8 @@ compilerAndInterpreter('edge activated hat thread does not interrupt stack click
|
||||
// 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);
|
||||
assert.strictEqual(vm.runtime.threads.length, 0);
|
||||
assert.strictEqual(doneThreads.length, 2);
|
||||
let hatThread;
|
||||
let stackClickThread;
|
||||
if (doneThreads[0].stackClick) {
|
||||
@@ -367,11 +364,11 @@ compilerAndInterpreter('edge activated hat thread does not interrupt stack click
|
||||
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();
|
||||
checkIsHatThread(vm, hatThread);
|
||||
checkIsStackClickThread(vm, stackClickThread);
|
||||
assert.ok(doneThreads[0].status === Thread.STATUS_DONE);
|
||||
assert.ok(doneThreads[1].status === Thread.STATUS_DONE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,20 +8,20 @@ const VirtualMachine = require('../../src/index');
|
||||
const uri = path.resolve(__dirname, '../fixtures/single_sound.sb');
|
||||
const project = readFileToBuffer(uri);
|
||||
|
||||
test('default', t => {
|
||||
test('default', (t, done) => {
|
||||
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);
|
||||
assert.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
@@ -33,7 +34,7 @@ test('default', t => {
|
||||
|
||||
// Check that the sound has the correct md5
|
||||
// This md5 was obtained from the asset server
|
||||
t.equal(firstSound.md5, 'edb9713dedbe9a2e05c09e0540182ef1.wav');
|
||||
assert.strictEqual(firstSound.md5, 'edb9713dedbe9a2e05c09e0540182ef1.wav');
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
@@ -7,20 +8,20 @@ const VirtualMachine = require('../../src/index');
|
||||
const uri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const project = extractProjectJson(uri);
|
||||
|
||||
test('default', t => {
|
||||
test('default', (t, done) => {
|
||||
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);
|
||||
assert.ok(threads.length === 0);
|
||||
vm.quit();
|
||||
t.end();
|
||||
done();
|
||||
});
|
||||
|
||||
// Start VM, load project, and run
|
||||
t.doesNotThrow(() => {
|
||||
assert.doesNotThrow(() => {
|
||||
vm.start();
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson;
|
||||
|
||||
@@ -7,12 +8,11 @@ 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('spec', () => {
|
||||
assert.strictEqual(typeof sb2.deserialize, 'function');
|
||||
});
|
||||
|
||||
test('nested default/*', t => {
|
||||
test('nested default/*', async () => {
|
||||
// Get SB2 JSON (string)
|
||||
const uri = path.resolve(__dirname, '../fixtures/default_nested.sb2');
|
||||
const json = extractProjectJson(uri, 'default');
|
||||
@@ -20,33 +20,31 @@ test('nested default/*', t => {
|
||||
// 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');
|
||||
const {targets} = await sb2.deserialize(json, rt);
|
||||
// Test
|
||||
assert.strictEqual(typeof json, 'object');
|
||||
assert.strictEqual(typeof rt, 'object');
|
||||
assert.strictEqual(typeof 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');
|
||||
assert.ok(targets[0] instanceof renderedTarget);
|
||||
assert.strictEqual(typeof targets[0].id, 'string');
|
||||
assert.strictEqual(typeof targets[0].blocks, 'object');
|
||||
assert.strictEqual(typeof targets[0].variables, 'object');
|
||||
assert.strictEqual(typeof 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);
|
||||
assert.strictEqual(targets[0].isOriginal, true);
|
||||
assert.strictEqual(targets[0].currentCostume, 0);
|
||||
assert.strictEqual(targets[0].isOriginal, true);
|
||||
assert.strictEqual(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');
|
||||
assert.ok(targets[1] instanceof renderedTarget);
|
||||
assert.strictEqual(typeof targets[1].id, 'string');
|
||||
assert.strictEqual(typeof targets[1].blocks, 'object');
|
||||
assert.strictEqual(typeof targets[1].variables, 'object');
|
||||
assert.strictEqual(typeof 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();
|
||||
});
|
||||
assert.strictEqual(targets[1].isOriginal, true);
|
||||
assert.strictEqual(targets[1].currentCostume, 0);
|
||||
assert.strictEqual(targets[1].isOriginal, true);
|
||||
assert.strictEqual(targets[1].isStage, false);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson;
|
||||
|
||||
@@ -7,12 +8,11 @@ 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('spec', () => {
|
||||
assert.strictEqual(typeof sb2.deserialize, 'function');
|
||||
});
|
||||
|
||||
test('default', t => {
|
||||
test('default', async () => {
|
||||
// Get SB2 JSON (string)
|
||||
const uri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const json = extractProjectJson(uri);
|
||||
@@ -20,33 +20,31 @@ test('default', t => {
|
||||
// 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');
|
||||
const {targets} = await sb2.deserialize(json, rt);
|
||||
// Test
|
||||
assert.strictEqual(typeof json, 'object');
|
||||
assert.strictEqual(typeof rt, 'object');
|
||||
assert.strictEqual(typeof 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');
|
||||
assert.ok(targets[0] instanceof renderedTarget);
|
||||
assert.strictEqual(typeof targets[0].id, 'string');
|
||||
assert.strictEqual(typeof targets[0].blocks, 'object');
|
||||
assert.strictEqual(typeof targets[0].variables, 'object');
|
||||
assert.strictEqual(typeof 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);
|
||||
assert.strictEqual(targets[0].isOriginal, true);
|
||||
assert.strictEqual(targets[0].currentCostume, 0);
|
||||
assert.strictEqual(targets[0].isOriginal, true);
|
||||
assert.strictEqual(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');
|
||||
assert.ok(targets[1] instanceof renderedTarget);
|
||||
assert.strictEqual(typeof targets[1].id, 'string');
|
||||
assert.strictEqual(typeof targets[1].blocks, 'object');
|
||||
assert.strictEqual(typeof targets[1].variables, 'object');
|
||||
assert.strictEqual(typeof 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();
|
||||
});
|
||||
assert.strictEqual(targets[1].isOriginal, true);
|
||||
assert.strictEqual(targets[1].currentCostume, 0);
|
||||
assert.strictEqual(targets[1].isOriginal, true);
|
||||
assert.strictEqual(targets[1].isStage, false);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
const test = require('tap').test;
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const Worker = require('tiny-worker');
|
||||
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
@@ -51,22 +52,22 @@ class TestInternalExtension {
|
||||
}
|
||||
}
|
||||
|
||||
test('internal extension', t => {
|
||||
test('internal extension', () => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
const extension = new TestInternalExtension();
|
||||
t.ok(extension.status.constructorCalled);
|
||||
assert.ok(extension.status.constructorCalled);
|
||||
|
||||
t.notOk(extension.status.getInfoCalled);
|
||||
assert.ok(!extension.status.getInfoCalled);
|
||||
vm.extensionManager._registerInternalExtension(extension);
|
||||
t.ok(extension.status.getInfoCalled);
|
||||
assert.ok(extension.status.getInfoCalled);
|
||||
|
||||
const func = vm.runtime.getOpcodeFunction('testInternalExtension_go');
|
||||
t.type(func, 'function');
|
||||
assert.strictEqual(typeof func, 'function');
|
||||
|
||||
t.notOk(extension.status.goCalled);
|
||||
assert.ok(!extension.status.goCalled);
|
||||
const goBlockInfo = func();
|
||||
t.ok(extension.status.goCalled);
|
||||
assert.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`.
|
||||
@@ -79,41 +80,38 @@ test('internal extension', t => {
|
||||
terminal: false,
|
||||
text: 'go'
|
||||
};
|
||||
t.deepEqual(goBlockInfo, expectedBlockInfo);
|
||||
assert.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);
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].menus.length, 2);
|
||||
// First menu has 3 items.
|
||||
t.equal(
|
||||
assert.strictEqual(
|
||||
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();
|
||||
assert.strictEqual(typeof vm.runtime._blockInfo[0].menus[1].json.args0[0].options, 'function');
|
||||
});
|
||||
|
||||
test('load sync', t => {
|
||||
test('load sync', () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager.loadExtensionIdSync('coreExample');
|
||||
t.ok(vm.extensionManager.isExtensionLoaded('coreExample'));
|
||||
assert.ok(vm.extensionManager.isExtensionLoaded('coreExample'));
|
||||
|
||||
t.equal(vm.runtime._blockInfo.length, 1);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].blocks.length, 3);
|
||||
assert.strictEqual(typeof vm.runtime._blockInfo[0].blocks[0].info, 'object');
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].blocks[0].info.func, 'MAKE_A_VARIABLE');
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].blocks[0].info.blockType, 'button');
|
||||
assert.strictEqual(typeof vm.runtime._blockInfo[0].blocks[1].info, 'object');
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].blocks[1].info.opcode, 'exampleOpcode');
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].blocks[1].info.blockType, 'reporter');
|
||||
assert.strictEqual(typeof vm.runtime._blockInfo[0].blocks[2].info, 'object');
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].blocks[2].info.opcode, 'exampleWithInlineImage');
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].blocks[1].info.func(), 'no stage yet');
|
||||
|
||||
const sprite = new Sprite(null, vm.runtime);
|
||||
sprite.name = 'Stage';
|
||||
@@ -121,7 +119,5 @@ test('load sync', t => {
|
||||
stage.isStage = true;
|
||||
vm.runtime.targets = [stage];
|
||||
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.func(), 'Stage');
|
||||
|
||||
t.end();
|
||||
assert.strictEqual(vm.runtime._blockInfo[0].blocks[1].info.func(), 'Stage');
|
||||
});
|
||||
Loaded 100 of 486 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user