From cf215bf0bcac1906393029b4e6f861ad012b4e2e Mon Sep 17 00:00:00 2001 From: griffpatch Date: Wed, 8 Feb 2017 08:38:50 +0000 Subject: [PATCH 1/7] Fix for the "Empty or white space strings equal 0" bug See: Empty or white space strings equal 0 #435 --- src/util/cast.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/util/cast.js b/src/util/cast.js index a8a1535b8..c2e1cfe0f 100644 --- a/src/util/cast.js +++ b/src/util/cast.js @@ -100,6 +100,12 @@ Cast.toRgbColorObject = function (value) { Cast.compare = function (v1, v2) { var n1 = Number(v1); var n2 = Number(v2); + if (n1 === 0 && (v1 == null || typeof v1 === 'string' && v1.trim().length === 0)) { + n1 = NaN; + } + if (n2 === 0 && (v2 == null || typeof v2 === 'string' && v2.trim().length === 0)) { + n2 = NaN; + } if (isNaN(n1) || isNaN(n2)) { // At least one argument can't be converted to a number. // Scratch compares strings as case insensitive. From 8354774fcf3f4b6d61b545aebe09f00ea63724a0 Mon Sep 17 00:00:00 2001 From: SillyInventor Date: Thu, 9 Feb 2017 14:09:01 -0500 Subject: [PATCH 2/7] Switch from return null to return empty string in keyCodeToScratchKey --- src/io/keyboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/keyboard.js b/src/io/keyboard.js index da0e41625..c7a210803 100644 --- a/src/io/keyboard.js +++ b/src/io/keyboard.js @@ -56,7 +56,7 @@ Keyboard.prototype._keyCodeToScratchKey = function (keyCode) { case 39: return 'right arrow'; case 40: return 'down arrow'; } - return null; + return ''; }; /** From cd750b5dda4ee7a845fd90349cd6075e3d0f0bd0 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 9 Feb 2017 14:52:15 -0500 Subject: [PATCH 3/7] pass volume to audioengine playnote --- src/blocks/scratch3_sound.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 20d137580..e9f9df4d7 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -135,8 +135,9 @@ Scratch3SoundBlocks.prototype.playNoteForBeats = function (args, util) { var beats = Cast.toNumber(args.BEATS); var soundState = this._getSoundState(util.target); var inst = soundState.currentInstrument; + var vol = soundState.volume; if (typeof this.runtime.audioEngine === 'undefined') return; - return this.runtime.audioEngine.playNoteForBeatsWithInst(note, beats, inst); + return this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, vol); }; Scratch3SoundBlocks.prototype.playDrumForBeats = function (args, util) { From 3324f51193079329b2d7a469d2e0cc850031d46f Mon Sep 17 00:00:00 2001 From: SillyInventor Date: Thu, 9 Feb 2017 18:18:53 -0500 Subject: [PATCH 4/7] Added some unit tests for some thread functions --- test/unit/engine_thread.js | 107 ++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/test/unit/engine_thread.js b/test/unit/engine_thread.js index 3bda4fc69..a06a316a7 100644 --- a/test/unit/engine_thread.js +++ b/test/unit/engine_thread.js @@ -3,6 +3,111 @@ var Thread = require('../../src/engine/thread'); test('spec', function (t) { t.type(Thread, 'function'); - // @todo + + var th = new Thread('arbitraryString'); + t.type(th, 'object'); + t.ok(th instanceof Thread); + t.type(th.pushStack, 'function'); + t.type(th.reuseStackForNextBlock, 'function'); + t.type(th.popStack, 'function'); + t.type(th.stopThisScript, 'function'); + t.type(th.peekStack, 'function'); + t.type(th.peekStackFrame, 'function'); + t.type(th.peekParentStackFrame, 'function'); + t.type(th.pushReportedValue, 'function'); + t.type(th.pushParam, 'function'); + t.type(th.peekStack, 'function'); + t.type(th.getParam, 'function'); + t.type(th.atStackTop, 'function'); + t.type(th.goToNextBlock, 'function'); + t.type(th.isRecursiveCall, 'function'); + + t.end(); +}); + +test('pushStack', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + + t.end(); +}); + +test('popStack', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + t.strictEquals(th.popStack(), 'arbitraryString'); + t.strictEquals(th.popStack(), undefined); + + t.end(); +}); + +test('atStackTop', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.pushStack('secondString'); + t.strictEquals(th.atStackTop(), false); + th.popStack(); + t.strictEquals(th.atStackTop(), true); + + t.end(); +}); + +test('reuseStackForNextBlock', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.reuseStackForNextBlock('secondString'); + t.strictEquals(th.popStack(), 'secondString'); + + t.end(); +}); + +test('peekStackFrame', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + t.strictEquals(th.peekStackFrame().warpMode, false); + th.popStack(); + t.strictEquals(th.peekStackFrame(), null); + + t.end(); +}); + +test('peekParentStackFrame', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.peekStackFrame().warpMode = true; + t.strictEquals(th.peekParentStackFrame(), null); + th.pushStack('secondString'); + t.strictEquals(th.peekParentStackFrame().warpMode, true); + + t.end(); +}); + +test('pushReportedValue', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.pushStack('secondString'); + th.pushReportedValue('value'); + t.strictEquals(th.peekParentStackFrame().reported.null, 'value'); + + t.end(); +}); + +test('peekStack', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + t.strictEquals(th.peekStack(), 'arbitraryString'); + th.popStack(); + t.strictEquals(th.peekStack(), null); + + t.end(); +}); + +test('PushGetParam', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.pushParam('testParam', 'testValue'); + t.strictEquals(th.peekStackFrame().params.testParam, 'testValue'); + t.strictEquals(th.getParam('testParam'), 'testValue'); + t.end(); }); From ad5bc1afbda3e9e5fa0618a0d9de84a1a76d8b21 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Fri, 10 Feb 2017 08:02:11 +0000 Subject: [PATCH 5/7] Pull white space checks into function Clean up code by pulling white space checks into a seperate helper function --- src/util/cast.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/util/cast.js b/src/util/cast.js index c2e1cfe0f..79e76b134 100644 --- a/src/util/cast.js +++ b/src/util/cast.js @@ -90,6 +90,15 @@ Cast.toRgbColorObject = function (value) { return color; }; +/** + * Determine if a Scratch argument is a white space string (or null / empty). + * @param {*} val value to check. + * @return {boolean} True if the argument is all white spaces or null / empty. + */ +Cast.isWhiteSpace = function (val) { + return val === null || typeof val === 'string' && val.trim().length === 0; +}; + /** * Compare two values, using Scratch cast, case-insensitive string compare, etc. * In Scratch 2.0, this is captured by `interp.compare.` @@ -100,10 +109,9 @@ Cast.toRgbColorObject = function (value) { Cast.compare = function (v1, v2) { var n1 = Number(v1); var n2 = Number(v2); - if (n1 === 0 && (v1 == null || typeof v1 === 'string' && v1.trim().length === 0)) { + if (n1 === 0 && Cast.isWhiteSpace(v1)) { n1 = NaN; - } - if (n2 === 0 && (v2 == null || typeof v2 === 'string' && v2.trim().length === 0)) { + } else if (n2 === 0 && Cast.isWhiteSpace(v2)) { n2 = NaN; } if (isNaN(n1) || isNaN(n2)) { From e8949dcdf5b8843f7bd1f2a4c189d7e8eb38f479 Mon Sep 17 00:00:00 2001 From: SillyInventor Date: Fri, 10 Feb 2017 13:06:39 -0500 Subject: [PATCH 6/7] Added remaining tests --- test/unit/engine_thread.js | 164 +++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/test/unit/engine_thread.js b/test/unit/engine_thread.js index a06a316a7..815de5ca3 100644 --- a/test/unit/engine_thread.js +++ b/test/unit/engine_thread.js @@ -1,5 +1,7 @@ var test = require('tap').test; var Thread = require('../../src/engine/thread'); +var RenderedTarget = require('../../src/sprites/rendered-target'); +var Sprite = require('../../src/sprites/sprite'); test('spec', function (t) { t.type(Thread, 'function'); @@ -111,3 +113,165 @@ test('PushGetParam', function (t) { t.end(); }); + +test('goToNextBlock', function (t) { + var th = new Thread('arbitraryString'); + var s = new Sprite(); + var rt = new RenderedTarget(s, null); + var block1 = {fields: Object, + id: 'arbitraryString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: 'secondString', + opcode: 'motion_movesteps', + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + var block2 = {fields: Object, + id: 'secondString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'procedures_callnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + + rt.blocks.createBlock(block1); + rt.blocks.createBlock(block2); + rt.blocks.createBlock(block2); + th.target = rt; + + t.strictEquals(th.peekStack(), null); + th.pushStack('secondString'); + t.strictEquals(th.peekStack(), 'secondString'); + th.goToNextBlock(); + t.strictEquals(th.peekStack(), null); + th.pushStack('secondString'); + th.pushStack('arbitraryString'); + t.strictEquals(th.peekStack(), 'arbitraryString'); + th.goToNextBlock(); + t.strictEquals(th.peekStack(), 'secondString'); + th.goToNextBlock(); + t.strictEquals(th.peekStack(), null); + + t.end(); +}); + +test('stopThisScript', function (t) { + var th = new Thread('arbitraryString'); + var s = new Sprite(); + var rt = new RenderedTarget(s, null); + var block1 = {fields: Object, + id: 'arbitraryString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'motion_movesteps', + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + var block2 = {fields: Object, + id: 'secondString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'procedures_callnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + + rt.blocks.createBlock(block1); + rt.blocks.createBlock(block2); + th.target = rt; + + th.stopThisScript(); + t.strictEquals(th.peekStack(), null); + th.pushStack('arbitraryString'); + t.strictEquals(th.peekStack(), 'arbitraryString'); + th.stopThisScript(); + t.strictEquals(th.peekStack(), null); + th.pushStack('arbitraryString'); + th.pushStack('secondString'); + th.stopThisScript(); + t.strictEquals(th.peekStack(), 'secondString'); + + t.end(); +}); + +test('isRecursiveCall', function (t) { + var th = new Thread('arbitraryString'); + var s = new Sprite(); + var rt = new RenderedTarget(s, null); + var block1 = {fields: Object, + id: 'arbitraryString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'motion_movesteps', + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + var block2 = {fields: Object, + id: 'secondString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'procedures_callnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + + rt.blocks.createBlock(block1); + rt.blocks.createBlock(block2); + th.target = rt; + + t.strictEquals(th.isRecursiveCall('fakeCode'), false); + th.pushStack('secondString'); + t.strictEquals(th.isRecursiveCall('fakeCode'), false); + th.pushStack('arbitraryString'); + t.strictEquals(th.isRecursiveCall('fakeCode'), true); + th.pushStack('arbitraryString'); + t.strictEquals(th.isRecursiveCall('fakeCode'), true); + th.popStack(); + t.strictEquals(th.isRecursiveCall('fakeCode'), true); + th.popStack(); + t.strictEquals(th.isRecursiveCall('fakeCode'), false); + th.popStack(); + t.strictEquals(th.isRecursiveCall('fakeCode'), false); + + t.end(); +}); From 243d68f88bef66d085870177b179341cad63d8ec Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 10 Feb 2017 14:21:37 -0800 Subject: [PATCH 7/7] Use Webpack to generate the whole playground The previous configuration mixed Webpack output with static content in order to create the playground. This change moves that static content from `/playground/` into `/src/playground/` and adds a Webpack rule to copy it into the playground as part of the build. On the surface this might seem unnecessary, but it provides at least two benefits: - It's no longer possible to accidentally load stale build output through `webpack-dev-server` in the event of a misconfiguration. This was very easy in the previous configuration, and might in fact be the only way that `webpack-dev-server` ever worked for this repository. - It's simpler to ensure that various rules apply to the hand-authored content and not build outputs. This includes lint rules, `.gitignore`, IDE symbol search paths, etc. --- .gitignore | 8 +--- {playground => src/playground}/index.html | 0 {playground => src/playground}/playground.css | 0 {playground => src/playground}/playground.js | 41 +++++++++---------- webpack.config.js | 4 +- 5 files changed, 24 insertions(+), 29 deletions(-) rename {playground => src/playground}/index.html (100%) rename {playground => src/playground}/playground.css (100%) rename {playground => src/playground}/playground.js (92%) diff --git a/.gitignore b/.gitignore index fdf767042..2f3825877 100644 --- a/.gitignore +++ b/.gitignore @@ -14,10 +14,4 @@ npm-* # Build /dist -/playground/assets -/playground/media -/playground/scratch-vm.js -/playground/scratch-vm.js.map -/playground/vendor.js -/playground/vendor.js.map -/playground/zenburn.css +/playground diff --git a/playground/index.html b/src/playground/index.html similarity index 100% rename from playground/index.html rename to src/playground/index.html diff --git a/playground/playground.css b/src/playground/playground.css similarity index 100% rename from playground/playground.css rename to src/playground/playground.css diff --git a/playground/playground.js b/src/playground/playground.js similarity index 92% rename from playground/playground.js rename to src/playground/playground.js index cb5a81a3e..9ee924122 100644 --- a/playground/playground.js +++ b/src/playground/playground.js @@ -1,4 +1,3 @@ - var loadProject = function () { var id = location.hash.substring(1); if (id.length < 1 || !isFinite(id)) { @@ -7,7 +6,7 @@ var loadProject = function () { var url = 'https://projects.scratch.mit.edu/internalapi/project/' + id + '/get/'; var r = new XMLHttpRequest(); - r.onreadystatechange = function() { + r.onreadystatechange = function () { if (this.readyState === 4) { if (r.status === 200) { window.vm.loadProject(this.responseText); @@ -18,7 +17,7 @@ var loadProject = function () { r.send(); }; -window.onload = function() { +window.onload = function () { // Lots of global variables to make debugging easier // Instantiate the VM. var vm = new window.VirtualMachine(); @@ -74,7 +73,7 @@ window.onload = function() { // Playground data tabs. // Block representation tab. var blockexplorer = document.getElementById('blockexplorer'); - var updateBlockExplorer = function(blocks) { + var updateBlockExplorer = function (blocks) { blockexplorer.innerHTML = JSON.stringify(blocks, null, 2); window.hljs.highlightBlock(blockexplorer); }; @@ -83,7 +82,7 @@ window.onload = function() { var threadexplorer = document.getElementById('threadexplorer'); var cachedThreadJSON = ''; var updateThreadExplorer = function (newJSON) { - if (newJSON != cachedThreadJSON) { + if (newJSON !== cachedThreadJSON) { cachedThreadJSON = newJSON; threadexplorer.innerHTML = cachedThreadJSON; window.hljs.highlightBlock(threadexplorer); @@ -101,7 +100,7 @@ window.onload = function() { // VM handlers. // Receipt of new playground data (thread, block representations). - vm.on('playgroundData', function(data) { + vm.on('playgroundData', function (data) { updateThreadExplorer(data.threads); updateBlockExplorer(data.blocks); }); @@ -125,7 +124,7 @@ window.onload = function() { var targetOption = document.createElement('option'); targetOption.setAttribute('value', data.targetList[i].id); // If target id matches editingTarget id, select it. - if (data.targetList[i].id == data.editingTarget) { + if (data.targetList[i].id === data.editingTarget) { targetOption.setAttribute('selected', 'selected'); } targetOption.appendChild( @@ -139,23 +138,23 @@ window.onload = function() { }; // Feedback for stacks and blocks running. - vm.on('SCRIPT_GLOW_ON', function(data) { + vm.on('SCRIPT_GLOW_ON', function (data) { workspace.glowStack(data.id, true); }); - vm.on('SCRIPT_GLOW_OFF', function(data) { + vm.on('SCRIPT_GLOW_OFF', function (data) { workspace.glowStack(data.id, false); }); - vm.on('BLOCK_GLOW_ON', function(data) { + vm.on('BLOCK_GLOW_ON', function (data) { workspace.glowBlock(data.id, true); }); - vm.on('BLOCK_GLOW_OFF', function(data) { + vm.on('BLOCK_GLOW_OFF', function (data) { workspace.glowBlock(data.id, false); }); - vm.on('VISUAL_REPORT', function(data) { + vm.on('VISUAL_REPORT', function (data) { workspace.reportValue(data.id, data.value); }); - vm.on('SPRITE_INFO_REPORT', function(data) { + vm.on('SPRITE_INFO_REPORT', function (data) { if (data.id !== selectedTarget.value) return; // Not the editingTarget document.getElementById('sinfo-x').value = data.x; document.getElementById('sinfo-y').value = data.y; @@ -213,7 +212,7 @@ window.onload = function() { // Feed keyboard events as VM I/O events. document.addEventListener('keydown', function (e) { // Don't capture keys intended for Blockly inputs. - if (e.target != document && e.target != document.body) { + if (e.target !== document && e.target !== document.body) { return; } window.vm.postIOData('keyboard', { @@ -222,7 +221,7 @@ window.onload = function() { }); e.preventDefault(); }); - document.addEventListener('keyup', function(e) { + document.addEventListener('keyup', function (e) { // Always capture up events, // even those that have switched to other targets. window.vm.postIOData('keyboard', { @@ -230,7 +229,7 @@ window.onload = function() { isDown: false }); // E.g., prevent scroll. - if (e.target != document && e.target != document.body) { + if (e.target !== document && e.target !== document.body) { e.preventDefault(); } }); @@ -239,25 +238,25 @@ window.onload = function() { vm.start(); // Inform VM of animation frames. - var animate = function() { + var animate = function () { stats.update(); requestAnimationFrame(animate); }; requestAnimationFrame(animate); // Handlers for green flag and stop all. - document.getElementById('greenflag').addEventListener('click', function() { + document.getElementById('greenflag').addEventListener('click', function () { vm.greenFlag(); }); - document.getElementById('stopall').addEventListener('click', function() { + document.getElementById('stopall').addEventListener('click', function () { vm.stopAll(); }); - document.getElementById('turbomode').addEventListener('change', function() { + document.getElementById('turbomode').addEventListener('change', function () { var turboOn = document.getElementById('turbomode').checked; vm.setTurboMode(turboOn); }); document.getElementById('compatmode').addEventListener('change', - function() { + function () { var compatibilityMode = document.getElementById('compatmode').checked; vm.setCompatibilityMode(compatibilityMode); }); diff --git a/webpack.config.js b/webpack.config.js index f58b7a1bf..29edbfcd1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,7 @@ var webpack = require('webpack'); var base = { devServer: { - contentBase: path.resolve(__dirname, 'playground'), + contentBase: false, host: '0.0.0.0', port: process.env.PORT || 8073 }, @@ -108,6 +108,8 @@ module.exports = [ to: 'media' }, { from: 'node_modules/highlightjs/styles/zenburn.css' + }, { + from: 'src/playground' }]) ]) })