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/README.md b/README.md index 2888fc6c0..512bc1645 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ npm start ``` ## Playground -To run the Playground, make sure the dev server's running and go to [http://localhost:8073/](http://localhost:8073/) - you will be directed to the playground, which demonstrates various tools and internal state. +To run the Playground, make sure the dev server's running and go to [http://localhost:8073/playground/](http://localhost:8073/playground/) - you will be directed to the playground, which demonstrates various tools and internal state. ![VM Playground Screenshot](https://i.imgur.com/nOCNqEc.gif) @@ -50,7 +50,7 @@ npm run build ``` ## How to include in a Node.js App -For an extended setup example, check out the /playground directory, which includes a fully running VM instance. +For an extended setup example, check out the /src/playground directory, which includes a fully running VM instance. ```js var VirtualMachine = require('scratch-vm'); var vm = new VirtualMachine(); diff --git a/package.json b/package.json index 6ab89c9f3..4418452f5 100644 --- a/package.json +++ b/package.json @@ -25,15 +25,16 @@ }, "devDependencies": { "adm-zip": "0.4.7", - "babel-eslint": "7.1.1", + "babel-eslint": "^7.1.1", "copy-webpack-plugin": "4.0.1", - "eslint": "3.15.0", + "eslint": "^3.16.0", "eslint-config-scratch": "^3.1.0", - "expose-loader": "0.7.1", - "gh-pages": "0.12.0", - "highlightjs": "9.8.0", + "expose-loader": "0.7.3", + "gh-pages": "^0.12.0", + "highlightjs": "^9.8.0", "htmlparser2": "3.9.2", - "json": "9.0.4", + "jsdom": "^9.11.0", + "json": "^9.0.4", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", "promise": "7.1.1", @@ -41,10 +42,10 @@ "scratch-blocks": "latest", "scratch-render": "latest", "script-loader": "0.7.0", - "stats.js": "0.17.0", - "tap": "10.0.2", - "travis-after-all": "1.4.4", + "stats.js": "^0.17.0", + "tap": "^10.2.0", + "travis-after-all": "^1.4.4", "webpack": "2.2.1", - "webpack-dev-server": "1.16.3" + "webpack-dev-server": "^2.4.1" } } diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index 28737b361..285b241ab 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -110,7 +110,7 @@ Scratch3ControlBlocks.prototype.stop = function (args, util) { option === 'other scripts in stage') { util.stopOtherTargetThreads(); } else if (option === 'this script') { - util.stopThread(); + util.stopThisScript(); } }; diff --git a/src/blocks/scratch3_pen.js b/src/blocks/scratch3_pen.js index c8f98341e..9b9f64919 100644 --- a/src/blocks/scratch3_pen.js +++ b/src/blocks/scratch3_pen.js @@ -147,6 +147,7 @@ Scratch3PenBlocks.prototype._updatePenColor = function (penState) { penState.penAttributes.color4f[0] = rgb.r / 255.0; penState.penAttributes.color4f[1] = rgb.g / 255.0; penState.penAttributes.color4f[2] = rgb.b / 255.0; + penState.penAttributes.color4f[3] = 1; }; /** @@ -260,6 +261,8 @@ Scratch3PenBlocks.prototype.setPenColorToColor = function (args, util) { penState.penAttributes.color4f[2] = rgb.b / 255.0; if (rgb.hasOwnProperty('a')) { // Will there always be an 'a'? penState.penAttributes.color4f[3] = rgb.a / 255.0; + } else { + penState.penAttributes.color4f[3] = 1; } }; diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 20d137580..76c03f6fd 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) { @@ -192,6 +193,7 @@ Scratch3SoundBlocks.prototype.changeEffect = function (args, util) { Scratch3SoundBlocks.prototype.clearEffects = function (args, util) { var soundState = this._getSoundState(util.target); for (var effect in soundState.effects) { + if (!soundState.effects.hasOwnProperty(effect)) continue; soundState.effects[effect] = 0; } if (util.target.audioPlayer === null) return; diff --git a/src/engine/adapter.js b/src/engine/adapter.js index 56275e1cb..88c8a18c5 100644 --- a/src/engine/adapter.js +++ b/src/engine/adapter.js @@ -24,6 +24,7 @@ var domToBlocks = function (blocksDOM) { // Flatten blocks object into a list. var blocksList = []; for (var b in blocks) { + if (!blocks.hasOwnProperty(b)) continue; blocksList.push(blocks[b]); } return blocksList; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index ed03be324..d9172228c 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -149,6 +149,7 @@ Blocks.prototype.getTopLevelScript = function (id) { */ Blocks.prototype.getProcedureDefinition = function (name) { for (var id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; var block = this._blocks[id]; if ((block.opcode === 'procedures_defnoreturn' || block.opcode === 'procedures_defreturn') && @@ -166,6 +167,7 @@ Blocks.prototype.getProcedureDefinition = function (name) { */ Blocks.prototype.getProcedureParamNames = function (name) { for (var id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; var block = this._blocks[id]; if ((block.opcode === 'procedures_defnoreturn' || block.opcode === 'procedures_defreturn') && @@ -415,6 +417,7 @@ Blocks.prototype.blockToXML = function (blockId) { } // Add any inputs on this block. for (var input in block.inputs) { + if (!block.inputs.hasOwnProperty(input)) continue; var blockInput = block.inputs[input]; // Only encode a value tag if the value input is occupied. if (blockInput.block || blockInput.shadow) { @@ -431,6 +434,7 @@ Blocks.prototype.blockToXML = function (blockId) { } // Add any fields on this block. for (var field in block.fields) { + if (!block.fields.hasOwnProperty(field)) continue; var blockField = block.fields[field]; var value = blockField.value; if (typeof value === 'string') { diff --git a/src/engine/execute.js b/src/engine/execute.js index 44951aba3..9d6342f3f 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -110,6 +110,7 @@ var execute = function (sequencer, thread) { Object.keys(inputs).length === 0) { // One field and no inputs - treat as arg. for (var fieldKey in fields) { // One iteration. + if (!fields.hasOwnProperty(fieldKey)) continue; handleReport(fields[fieldKey].value); } } else { @@ -126,11 +127,13 @@ var execute = function (sequencer, thread) { // Add all fields on this block to the argValues. for (var fieldName in fields) { + if (!fields.hasOwnProperty(fieldName)) continue; argValues[fieldName] = fields[fieldName].value; } // Recursively evaluate input blocks. for (var inputName in inputs) { + if (!inputs.hasOwnProperty(inputName)) continue; var input = inputs[inputName]; var inputBlockId = input.block; // Is there no value for this input waiting in the stack frame? @@ -183,8 +186,8 @@ var execute = function (sequencer, thread) { stopOtherTargetThreads: function () { runtime.stopForTarget(target, thread); }, - stopThread: function () { - sequencer.retireThread(thread); + stopThisScript: function () { + thread.stopThisScript(); }, startProcedure: function (procedureCode) { sequencer.stepToProcedure(thread, procedureCode); @@ -233,8 +236,23 @@ var execute = function (sequencer, thread) { primitiveReportedValue.then(function (resolvedValue) { handleReport(resolvedValue); if (typeof resolvedValue === 'undefined') { - var popped = thread.popStack(); - var nextBlockId = thread.target.blocks.getNextBlock(popped); + do { + // In the case that the promise is the last block in the current thread stack + // We need to pop out repeatedly until we find the next block. + var popped = thread.popStack(); + if (popped === null) { + return; + } + var nextBlockId = thread.target.blocks.getNextBlock(popped); + if (nextBlockId !== null) { + // A next block exists so break out this loop + break; + } + // Investigate the next block and if not in a loop, + // then repeat and pop the next item off the stack frame + var stackFrame = thread.peekStackFrame(); + } while (stackFrame !== null && !stackFrame.isLoop); + thread.pushStack(nextBlockId); } else { thread.popStack(); diff --git a/src/engine/runtime.js b/src/engine/runtime.js index f196a6a69..130c4ae95 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -414,7 +414,7 @@ Runtime.prototype.allScriptsDo = function (f, optTarget) { if (optTarget) { targets = [optTarget]; } - for (var t = 0; t < targets.length; t++) { + for (var t = targets.length - 1; t >= 0; t--) { var target = targets[t]; var scripts = target.blocks.getScripts(); for (var j = 0; j < scripts.length; j++) { @@ -439,6 +439,12 @@ Runtime.prototype.startHats = function (requestedHatOpcode, } var instance = this; var newThreads = []; + + for (var opts in optMatchFields) { + if (!optMatchFields.hasOwnProperty(opts)) continue; + optMatchFields[opts] = optMatchFields[opts].toUpperCase(); + } + // Consider all scripts, looking for hats with opcode `requestedHatOpcode`. this.allScriptsDo(function (topBlockId, target) { var potentialHatOpcode = target.blocks.getBlock(topBlockId).opcode; @@ -458,6 +464,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, if (Object.keys(hatFields).length === 0) { var hatInputs = target.blocks.getInputs(topBlockId); for (var input in hatInputs) { + if (!hatInputs.hasOwnProperty(input)) continue; var id = hatInputs[input].block; var fields = target.blocks.getFields(id); hatFields = Object.assign(fields, hatFields); @@ -466,7 +473,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, if (optMatchFields) { for (var matchField in optMatchFields) { - if (hatFields[matchField].value !== + if (hatFields[matchField].value.toUpperCase() !== optMatchFields[matchField]) { // Field mismatch. return; @@ -587,6 +594,7 @@ Runtime.prototype.stopAll = function () { Runtime.prototype._step = function () { // Find all edge-activated hats, and add them to threads to be evaluated. for (var hatType in this._hats) { + if (!this._hats.hasOwnProperty(hatType)) continue; var hat = this._hats[hatType]; if (hat.edgeActivated) { this.startHats(hatType); @@ -785,6 +793,18 @@ Runtime.prototype.getSpriteTargetByName = function (spriteName) { } }; +/** + * Get a target by its drawable id. + * @param {number} drawableID drawable id of target to find + * @return {?Target} The target, if found + */ +Runtime.prototype.getTargetByDrawableId = function (drawableID) { + for (var i = 0; i < this.targets.length; i++) { + var target = this.targets[i]; + if (target.drawableID === drawableID) return target; + } +}; + /** * Update the clone counter to track how many clones are created. * @param {number} changeAmount How many clones have been created/destroyed. diff --git a/src/engine/thread.js b/src/engine/thread.js index 9693b26f1..f093f886e 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -140,6 +140,27 @@ Thread.prototype.popStack = function () { return this.stack.pop(); }; +/** + * Pop back down the stack frame until we hit a procedure call or the stack frame is emptied + */ +Thread.prototype.stopThisScript = function () { + var blockID = this.peekStack(); + while (blockID !== null) { + var block = this.target.blocks.getBlock(blockID); + if (typeof block !== 'undefined' && block.opcode === 'procedures_callnoreturn') { + break; + } + this.popStack(); + blockID = this.peekStack(); + } + + if (this.stack.length === 0) { + // Clean up! + this.requestScriptGlowInFrame = false; + this.status = Thread.STATUS_DONE; + } +}; + /** * Get top stack item. * @return {?string} Block ID on top of stack. 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 ''; }; /** diff --git a/src/io/mouse.js b/src/io/mouse.js index bcd19087e..4a7361fa4 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -46,7 +46,7 @@ Mouse.prototype.postData = function (data) { } if (typeof data.isDown !== 'undefined') { this._isDown = data.isDown; - if (this._isDown) { + if (!this._isDown) { this._activateClickHats(data.x, data.y); } } 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 90% rename from playground/playground.js rename to src/playground/playground.js index cb5a81a3e..f32e17ef9 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(); @@ -60,6 +59,11 @@ window.onload = function() { }); window.workspace = workspace; + // Filter available blocks + var toolbox = vm.filterToolbox(workspace.options.languageTree); + // var toolbox = workspace.options.languageTree; + workspace.updateToolbox(toolbox); + // Attach scratch-blocks events to VM. workspace.addChangeListener(vm.blockListener); var flyoutWorkspace = workspace.getFlyout().getWorkspace(); @@ -74,7 +78,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 +87,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 +105,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 +129,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 +143,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 +217,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 +226,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 +234,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 +243,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/src/serialization/sb2.js b/src/serialization/sb2.js index a07bb7232..3ef4f24f7 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -104,6 +104,9 @@ var parseScratchObject = function (object, runtime, topLevel) { if (object.hasOwnProperty('direction')) { target.direction = object.direction; } + if (object.hasOwnProperty('isDraggable')) { + target.draggable = object.isDraggable; + } if (object.hasOwnProperty('scale')) { // SB2 stores as 1.0 = 100%; we use % in the VM. target.size = object.scale * 100; diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 18440e679..f96ca9240 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -33,6 +33,13 @@ var RenderedTarget = function (sprite, runtime) { */ this.drawableID = null; + /** + * Drag state of this rendered target. If true, x/y position can't be + * changed by blocks. + * @type {boolean} + */ + this.dragging = false; + /** * Map of current graphic effect values. * @type {!Object.} @@ -106,6 +113,12 @@ RenderedTarget.prototype.y = 0; */ RenderedTarget.prototype.direction = 90; +/** + * Whether the rendered target is draggable on the stage + * @type {boolean} + */ +RenderedTarget.prototype.draggable = false; + /** * Whether the rendered target is currently visible. * @type {boolean} @@ -160,22 +173,27 @@ RenderedTarget.prototype.rotationStyle = ( * Set the X and Y coordinates. * @param {!number} x New X coordinate, in Scratch coordinates. * @param {!number} y New Y coordinate, in Scratch coordinates. + * @param {?boolean} force Force setting X/Y, in case of dragging */ -RenderedTarget.prototype.setXY = function (x, y) { - if (this.isStage) { - return; - } +RenderedTarget.prototype.setXY = function (x, y, force) { + if (this.isStage) return; + if (this.dragging && !force) return; var oldX = this.x; var oldY = this.y; - this.x = x; - this.y = y; if (this.renderer) { + var position = this.renderer.getFencedPositionOfDrawable(this.drawableID, [x, y]); + this.x = position[0]; + this.y = position[1]; + this.renderer.updateDrawableProperties(this.drawableID, { - position: [this.x, this.y] + position: position }); if (this.visible) { this.runtime.requestRedraw(); } + } else { + this.x = x; + this.y = y; } this.emit(RenderedTarget.EVENT_TARGET_MOVED, this, oldX, oldY); this.runtime.spriteInfoReport(this); @@ -224,6 +242,16 @@ RenderedTarget.prototype.setDirection = function (direction) { this.runtime.spriteInfoReport(this); }; +/** + * Set draggability; i.e., whether it's able to be dragged in the player + * @param {!boolean} draggable True if should be draggable. + */ +RenderedTarget.prototype.setDraggable = function (draggable) { + if (this.isStage) return; + this.draggable = !!draggable; + this.runtime.spriteInfoReport(this); +}; + /** * Set a say bubble. * @param {?string} type Type of say bubble: "say", "think", or null. @@ -315,6 +343,7 @@ RenderedTarget.prototype.setEffect = function (effectName, value) { */ RenderedTarget.prototype.clearEffects = function () { for (var effectName in this.effects) { + if (!this.effects.hasOwnProperty(effectName)) continue; this.effects[effectName] = 0; } if (this.renderer) { @@ -422,20 +451,23 @@ RenderedTarget.prototype.updateAllDrawableProperties = function () { if (this.renderer) { var renderedDirectionScale = this._getRenderedDirectionAndScale(); var costume = this.sprite.costumes[this.currentCostume]; + var bitmapResolution = costume.bitmapResolution || 1; var props = { position: [this.x, this.y], direction: renderedDirectionScale.direction, + draggable: this.draggable, scale: renderedDirectionScale.scale, visible: this.visible, skin: costume.skin, - costumeResolution: costume.bitmapResolution, + costumeResolution: bitmapResolution, rotationCenter: [ - costume.rotationCenterX / costume.bitmapResolution, - costume.rotationCenterY / costume.bitmapResolution + costume.rotationCenterX / bitmapResolution, + costume.rotationCenterY / bitmapResolution ] }; - for (var effectID in this.effects) { - props[effectID] = this.effects[effectID]; + for (var effectName in this.effects) { + if (!this.effects.hasOwnProperty(effectName)) continue; + props[effectName] = this.effects[effectName]; } this.renderer.updateDrawableProperties(this.drawableID, props); if (this.visible) { @@ -582,7 +614,7 @@ RenderedTarget.prototype.goBackLayers = function (nLayers) { /** * Move behind some other rendered target. - * @param {!Clone} other Other rendered target to move behind. + * @param {!RenderedTarget} other Other rendered target to move behind. */ RenderedTarget.prototype.goBehindOther = function (other) { if (this.renderer) { @@ -637,11 +669,11 @@ RenderedTarget.prototype.keepInFence = function (newX, newY, optFence) { /** * Make a clone, copying any run-time properties. * If we've hit the global clone limit, returns null. - * @return {!RenderedTarget} New clone. + * @return {RenderedTarget} New clone. */ RenderedTarget.prototype.makeClone = function () { if (!this.runtime.clonesAvailable() || this.isStage) { - return; // Hit max clone limit, or this is the stage. + return null; // Hit max clone limit, or this is the stage. } this.runtime.changeCloneCounter(1); var newClone = this.sprite.createClone(); @@ -649,6 +681,7 @@ RenderedTarget.prototype.makeClone = function () { newClone.x = this.x; newClone.y = this.y; newClone.direction = this.direction; + newClone.draggable = this.draggable; newClone.visible = this.visible; newClone.size = this.size; newClone.currentCostume = this.currentCostume; @@ -687,15 +720,19 @@ RenderedTarget.prototype.onStopAll = function () { * @param {object} data An object with sprite info data to set. */ RenderedTarget.prototype.postSpriteInfo = function (data) { + var force = data.hasOwnProperty('force') ? data.force : null; if (data.hasOwnProperty('x')) { - this.setXY(data.x, this.y); + this.setXY(data.x, this.y, force); } if (data.hasOwnProperty('y')) { - this.setXY(this.x, data.y); + this.setXY(this.x, data.y, force); } if (data.hasOwnProperty('direction')) { this.setDirection(data.direction); } + if (data.hasOwnProperty('draggable')) { + this.setDraggable(data.draggable); + } if (data.hasOwnProperty('rotationStyle')) { this.setRotationStyle(data.rotationStyle); } @@ -704,6 +741,20 @@ RenderedTarget.prototype.postSpriteInfo = function (data) { } }; +/** + * Put the sprite into the drag state. While in effect, setXY must be forced + */ +RenderedTarget.prototype.startDrag = function () { + this.dragging = true; +}; + +/** + * Remove the sprite from the drag state. + */ +RenderedTarget.prototype.stopDrag = function () { + this.dragging = false; +}; + /** * Serialize sprite info, used when emitting events about the sprite * @returns {object} Sprite data as a simple object @@ -717,6 +768,7 @@ RenderedTarget.prototype.toJSON = function () { y: this.y, size: this.size, direction: this.direction, + draggable: this.draggable, costume: this.getCurrentCostume(), costumeCount: this.getCostumes().length, currentCostume: this.currentCostume, diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js index bccc5d909..81626f3a7 100644 --- a/src/sprites/sprite.js +++ b/src/sprites/sprite.js @@ -46,7 +46,7 @@ var Sprite = function (blocks, runtime) { /** * Create a clone of this sprite. - * @returns {!Clone} Newly created clone. + * @returns {!RenderedTarget} Newly created clone. */ Sprite.prototype.createClone = function () { var newClone = new RenderedTarget(this, this.runtime); diff --git a/src/util/cast.js b/src/util/cast.js index a8a1535b8..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,6 +109,11 @@ Cast.toRgbColorObject = function (value) { Cast.compare = function (v1, v2) { var n1 = Number(v1); var n2 = Number(v2); + if (n1 === 0 && Cast.isWhiteSpace(v1)) { + n1 = NaN; + } else if (n2 === 0 && Cast.isWhiteSpace(v2)) { + n2 = NaN; + } if (isNaN(n1) || isNaN(n2)) { // At least one argument can't be converted to a number. // Scratch compares strings as case insensitive. diff --git a/src/util/filter-toolbox.js b/src/util/filter-toolbox.js new file mode 100644 index 000000000..54d59ced8 --- /dev/null +++ b/src/util/filter-toolbox.js @@ -0,0 +1,53 @@ +/** + * Filter Blockly toolbox XML node containing blocks to only those with + * valid opcodes. Return a copy of the node with valid blocks. + * @param {HTMLElement} node Blockly toolbox XML node + * @param {Array.} opcodes Valid opcodes. Blocks producing other opcodes + * will be filtered. + * @returns {HTMLElement} filtered toolbox XML node + */ +var filterToolboxNode = function (node, opcodes) { + var filteredCategory = node.cloneNode(); + for (var block = node.firstElementChild; block; block = block.nextElementSibling) { + if (block.nodeName.toLowerCase() !== 'block') continue; + var opcode = block.getAttribute('type').toLowerCase(); + if (opcodes.indexOf(opcode) !== -1) { + filteredCategory.appendChild(block.cloneNode(true)); + } + } + return filteredCategory; +}; + +/** + * Filter Blockly toolbox XML and return a copy which only contains blocks with + * existent opcodes. Categories with no valid children will be removed. + * @param {HTMLElement} toolbox Blockly toolbox XML node + * @param {Array.} opcodes Valid opcodes. Blocks producing other opcodes + * will be filtered. + * @returns {HTMLElement} filtered toolbox XML node + */ +var filterToolbox = function (toolbox, opcodes) { + if (!toolbox.hasChildNodes()) return toolbox; + var filteredToolbox; + if (toolbox.firstElementChild.nodeName.toLowerCase() === 'category') { + filteredToolbox = toolbox.cloneNode(); + for ( + var category = toolbox.firstElementChild; + category; + category = category.nextElementSibling + ) { + if (category.nodeName.toLowerCase() !== 'category') continue; + var filteredCategory = filterToolboxNode(category, opcodes); + if (filteredCategory.hasChildNodes() || + filteredCategory.hasAttribute('custom') + ) { + filteredToolbox.appendChild(filteredCategory); + } + } + } else { + filteredToolbox = filterToolboxNode(toolbox, opcodes); + } + return filteredToolbox; +}; + +module.exports = filterToolbox; diff --git a/src/util/timer.js b/src/util/timer.js index 17c66a567..9a60e5688 100644 --- a/src/util/timer.js +++ b/src/util/timer.js @@ -23,16 +23,35 @@ var Timer = function () {}; */ Timer.prototype.startTime = 0; +/** + * Disable use of self.performance for now as it results in lower performance + * However, instancing it like below (caching the self.performance to a local variable) negates most of the issues. + * @type {boolean} + */ +var USE_PERFORMANCE = false; + +/** + * Legacy object to allow for us to call now to get the old style date time (for backwards compatibility) + * @deprecated This is only called via the nowObj.now() if no other means is possible... + */ +var legacyDateCode = { + now: function () { + return new Date().getTime(); + } +}; + +/** + * Use this object to route all time functions through single access points. + */ +var nowObj = (USE_PERFORMANCE && typeof self !== 'undefined' && self.performance && 'now' in self.performance) ? + self.performance : Date.now ? Date : legacyDateCode; + /** * Return the currently known absolute time, in ms precision. * @returns {number} ms elapsed since 1 January 1970 00:00:00 UTC. */ Timer.prototype.time = function () { - if (Date.now) { - return Date.now(); - } else { - return new Date().getTime(); - } + return nowObj.now(); }; /** @@ -43,12 +62,7 @@ Timer.prototype.time = function () { * @returns {number} ms-scale accurate time relative to other relative times. */ Timer.prototype.relativeTime = function () { - if (typeof self !== 'undefined' && - self.performance && 'now' in self.performance) { - return self.performance.now(); - } else { - return this.time(); - } + return nowObj.now(); }; /** @@ -56,15 +70,11 @@ Timer.prototype.relativeTime = function () { * at the most accurate precision possible. */ Timer.prototype.start = function () { - this.startTime = this.relativeTime(); + this.startTime = nowObj.now(); }; -/** - * Check time elapsed since `timer.start` was called. - * @returns {number} Time elapsed, in ms (possibly sub-ms precision). - */ Timer.prototype.timeElapsed = function () { - return this.relativeTime() - this.startTime; + return nowObj.now() - this.startTime; }; module.exports = Timer; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 7aa5d6654..d70725cd3 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1,6 +1,7 @@ var EventEmitter = require('events'); var util = require('util'); +var filterToolbox = require('./util/filter-toolbox'); var Runtime = require('./engine/runtime'); var sb2 = require('./serialization/sb2'); @@ -379,6 +380,41 @@ VirtualMachine.prototype.emitWorkspaceUpdate = function () { }); }; +/** + * Get a target id for a drawable id. Useful for interacting with the renderer + * @param {int} drawableId The drawable id to request the target id for + * @returns {?string} The target id, if found. Will also be null if the target found is the stage. + */ +VirtualMachine.prototype.getTargetIdForDrawableId = function (drawableId) { + var target = this.runtime.getTargetByDrawableId(drawableId); + if (target.hasOwnProperty('id') && target.hasOwnProperty('isStage') && !target.isStage) { + return target.id; + } + return null; +}; + +/** + * Put a target into a "drag" state, during which its X/Y positions will be unaffected + * by blocks. + * @param {string} targetId The id for the target to put into a drag state + */ +VirtualMachine.prototype.startDrag = function (targetId) { + var target = this.runtime.getTargetById(targetId); + if (target) { + target.startDrag(); + this.setEditingTarget(target.id); + } +}; + +/** + * Remove a target from a drag state, so blocks may begin affecting X/Y position again + * @param {string} targetId The id for the target to remove from the drag state + */ +VirtualMachine.prototype.stopDrag = function (targetId) { + var target = this.runtime.getTargetById(targetId); + if (target) target.stopDrag(); +}; + /** * Post/edit sprite info for the current editing target. * @param {object} data An object with sprite info data to set. @@ -387,4 +423,17 @@ VirtualMachine.prototype.postSpriteInfo = function (data) { this.editingTarget.postSpriteInfo(data); }; + +/** + * Filter Blockly toolbox XML and return a copy which only contains blocks with + * existent opcodes. Categories with no valid children will be removed. + * @param {HTMLElement} toolbox Blockly toolbox XML node + * @returns {HTMLElement} filtered toolbox XML node + */ +VirtualMachine.prototype.filterToolbox = function (toolbox) { + var opcodes = Object.keys(this.runtime._primitives) + .concat(Object.keys(this.runtime._hats)); + return filterToolbox(toolbox, opcodes); +}; + module.exports = VirtualMachine; diff --git a/test/fixtures/.eslintrc.js b/test/fixtures/.eslintrc.js new file mode 100644 index 000000000..8df47e667 --- /dev/null +++ b/test/fixtures/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'max-len': [0] + } +}; diff --git a/test/fixtures/hat-execution-order.sb2 b/test/fixtures/hat-execution-order.sb2 new file mode 100644 index 000000000..f9a7efdcc Binary files /dev/null and b/test/fixtures/hat-execution-order.sb2 differ diff --git a/test/fixtures/toolboxes.js b/test/fixtures/toolboxes.js new file mode 100644 index 000000000..5f74c89df --- /dev/null +++ b/test/fixtures/toolboxes.js @@ -0,0 +1,840 @@ +var jsdom = require('jsdom').jsdom; +var categories = ''; +var simple = ''; +var empty = ''; +module.exports = { + categories: jsdom(categories).body.firstElementChild, + simple: jsdom(simple).body.firstElementChild, + empty: jsdom(empty).body.firstElementChild +}; diff --git a/test/integration/complex.js b/test/integration/complex.js index 1ff1d384f..dea8d40bc 100644 --- a/test/integration/complex.js +++ b/test/integration/complex.js @@ -34,6 +34,7 @@ test('complex', function (t) { x: 0, y: 10, direction: 90, + draggable: true, rotationStyle: 'all around', visible: true }); diff --git a/test/integration/hat-execution-order.js b/test/integration/hat-execution-order.js new file mode 100644 index 000000000..0a302624b --- /dev/null +++ b/test/integration/hat-execution-order.js @@ -0,0 +1,39 @@ +var path = require('path'); +var test = require('tap').test; +var extract = require('../fixtures/extract'); +var VirtualMachine = require('../../src/index'); + +var projectUri = path.resolve(__dirname, '../fixtures/hat-execution-order.sb2'); +var project = extract(projectUri); + +test('complex', function (t) { + var vm = new VirtualMachine(); + + // Evaluate playground data and exit + vm.on('playgroundData', function (e) { + var threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + + var results = vm.runtime.targets[0].lists.results.contents; + t.deepEqual(results, ['3', '2', '1', 'stage']); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(function () { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project); + vm.greenFlag(); + }); + + // After two seconds, get playground data and stop + setTimeout(function () { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); +}); diff --git a/test/unit/blocks_control.js b/test/unit/blocks_control.js index 2dc6049df..7867524ce 100644 --- a/test/unit/blocks_control.js +++ b/test/unit/blocks_control.js @@ -104,7 +104,7 @@ test('stop', function (t) { var state = { stopAll: 0, stopOtherTargetThreads: 0, - stopThread: 0 + stopThisScript: 0 }; var util = { stopAll: function () { @@ -113,8 +113,8 @@ test('stop', function (t) { stopOtherTargetThreads: function () { state.stopOtherTargetThreads++; }, - stopThread: function () { - state.stopThread++; + stopThisScript: function () { + state.stopThisScript++; } }; @@ -125,6 +125,6 @@ test('stop', function (t) { c.stop({STOP_OPTION: 'this script'}, util); t.strictEqual(state.stopAll, 1); t.strictEqual(state.stopOtherTargetThreads, 2); - t.strictEqual(state.stopThread, 1); + t.strictEqual(state.stopThisScript, 1); t.end(); }); diff --git a/test/unit/engine_sequencer.js b/test/unit/engine_sequencer.js index 23ec4c13d..1e000ad36 100644 --- a/test/unit/engine_sequencer.js +++ b/test/unit/engine_sequencer.js @@ -1,8 +1,178 @@ var test = require('tap').test; var Sequencer = require('../../src/engine/sequencer'); +var Runtime = require('../../src/engine/runtime'); +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(Sequencer, 'function'); - // @todo + + var r = new Runtime(); + var s = new Sequencer(r); + + t.type(s, 'object'); + t.ok(s instanceof Sequencer); + + t.type(s.stepThreads, 'function'); + t.type(s.stepThread, 'function'); + t.type(s.stepToBranch, 'function'); + t.type(s.stepToProcedure, 'function'); + t.type(s.retireThread, 'function'); + + t.end(); +}); + +var randomString = function () { + var top = Math.random().toString(36); + return top.substring(7); +}; + +var generateBlock = function (id) { + var block = {fields: Object, + id: id, + inputs: {}, + STEPS: Object, + block: 'fakeBlock', + name: 'fakeName', + next: null, + opcode: 'procedures_defnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + return block; +}; + +var generateBlockInput = function (id, next, inp) { + var block = {fields: Object, + id: id, + inputs: {SUBSTACK: {block: inp, name: 'SUBSTACK'}}, + STEPS: Object, + block: 'fakeBlock', + name: 'fakeName', + next: next, + opcode: 'procedures_defnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + return block; +}; + +var generateThread = function (runtime) { + var s = new Sprite(); + var rt = new RenderedTarget(s, runtime); + var th = new Thread(randomString()); + + var next = randomString(); + var inp = randomString(); + var name = th.topBlock; + + rt.blocks.createBlock(generateBlockInput(name, next, inp)); + th.pushStack(name); + rt.blocks.createBlock(generateBlock(inp)); + + for (var i = 0; i < 10; i++) { + name = next; + next = randomString(); + inp = randomString(); + + rt.blocks.createBlock(generateBlockInput(name, next, inp)); + th.pushStack(name); + rt.blocks.createBlock(generateBlock(inp)); + } + rt.blocks.createBlock(generateBlock(next)); + th.pushStack(next); + th.target = rt; + + runtime.threads.push(th); + + return th; +}; + +test('stepThread', function (t) { + var r = new Runtime(); + var s = new Sequencer(r); + var th = generateThread(r); + t.notEquals(th.status, Thread.STATUS_DONE); + s.stepThread(th); + t.strictEquals(th.status, Thread.STATUS_DONE); + th = generateThread(r); + th.status = Thread.STATUS_YIELD; + s.stepThread(th); + t.notEquals(th.status, Thread.STATUS_DONE); + th.status = Thread.STATUS_PROMISE_WAIT; + s.stepThread(th); + t.notEquals(th.status, Thread.STATUS_DONE); + + t.end(); +}); + +test('stepToBranch', function (t) { + var r = new Runtime(); + var s = new Sequencer(r); + var th = generateThread(r); + s.stepToBranch(th, 2, false); + t.strictEquals(th.peekStack(), null); + th.popStack(); + s.stepToBranch(th, 1, false); + t.strictEquals(th.peekStack(), null); + th.popStack(); + th.popStack(); + s.stepToBranch(th, 1, false); + t.notEquals(th.peekStack(), null); + + t.end(); +}); + +test('retireThread', function (t) { + var r = new Runtime(); + var s = new Sequencer(r); + var th = generateThread(r); + t.strictEquals(th.stack.length, 12); + s.retireThread(th); + t.strictEquals(th.stack.length, 0); + t.strictEquals(th.status, Thread.STATUS_DONE); + + t.end(); +}); + +test('stepToProcedure', function (t) { + var r = new Runtime(); + var s = new Sequencer(r); + var th = generateThread(r); + var expectedBlock = th.peekStack(); + s.stepToProcedure(th, ''); + t.strictEquals(th.peekStack(), expectedBlock); + s.stepToProcedure(th, 'faceCode'); + t.strictEquals(th.peekStack(), expectedBlock); + s.stepToProcedure(th, 'faceCode'); + th.target.blocks.getBlock(th.stack[th.stack.length - 4]).mutation.proccode = 'othercode'; + expectedBlock = th.stack[th.stack.length - 4]; + s.stepToProcedure(th, 'othercode'); + t.strictEquals(th.peekStack(), expectedBlock); + + + t.end(); +}); + +test('stepThreads', function (t) { + var r = new Runtime(); + r.currentStepTime = Infinity; + var s = new Sequencer(r); + t.strictEquals(s.stepThreads().length, 0); + generateThread(r); + t.strictEquals(r.threads.length, 1); + t.strictEquals(s.stepThreads().length, 0); + r.threads[0].status = Thread.STATUS_RUNNING; + t.strictEquals(s.stepThreads().length, 1); + t.end(); }); diff --git a/test/unit/engine_thread.js b/test/unit/engine_thread.js index 3bda4fc69..815de5ca3 100644 --- a/test/unit/engine_thread.js +++ b/test/unit/engine_thread.js @@ -1,8 +1,277 @@ 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'); - // @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(); +}); + +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(); }); diff --git a/test/unit/util_filter-toolbox.js b/test/unit/util_filter-toolbox.js new file mode 100644 index 000000000..9c3ea073c --- /dev/null +++ b/test/unit/util_filter-toolbox.js @@ -0,0 +1,22 @@ +var toolboxes = require('../fixtures/toolboxes'); +var test = require('tap').test; +var filterToolbox = require('../../src/util/filter-toolbox'); + +test('categories', function (t) { + var filteredToolbox = filterToolbox(toolboxes.categories, ['motion_movesteps']); + t.strictEqual(filteredToolbox.children.length, 3); + t.strictEqual(filteredToolbox.firstElementChild.children.length, 1); + t.end(); +}); + +test('simple', function (t) { + var filteredToolbox = filterToolbox(toolboxes.simple, ['motion_movesteps']); + t.strictEqual(filteredToolbox.children.length, 1); + t.end(); +}); + +test('empty', function (t) { + var filteredToolbox = filterToolbox(toolboxes.empty, ['motion_movesteps']); + t.strictEqual(filteredToolbox.children.length, 0); + t.end(); +}); 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' }]) ]) })