diff --git a/package.json b/package.json index 459a971a0..3ba71fb0d 100644 --- a/package.json +++ b/package.json @@ -25,15 +25,15 @@ }, "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", + "json": "^9.0.4", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", "promise": "7.1.1", @@ -41,10 +41,10 @@ "scratch-blocks": "latest", "scratch-render": "latest", "script-loader": "0.7.0", - "stats.js": "0.17.0", - "tap": "10.1.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": "2.3.0" + "webpack-dev-server": "^2.4.1" } } diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index e9f9df4d7..c60e41fb0 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -33,6 +33,36 @@ Scratch3SoundBlocks.DEFAULT_SOUND_STATE = { } }; +/** + * The minimum and maximum MIDI note numbers, for clamping the input to play note. + * @type {{min: number, max: number}} + */ +Scratch3SoundBlocks.MIDI_NOTE_RANGE = {min: 36, max: 96}; // C2 to C7 + +/** + * The minimum and maximum beat values, for clamping the duration of play note, play drum and rest. + * 100 beats at the default tempo of 60bpm is 100 seconds. + * @type {{min: number, max: number}} + */ +Scratch3SoundBlocks.BEAT_RANGE = {min: 0, max: 100}; + + /** The minimum and maximum tempo values, in bpm. + * @type {{min: number, max: number}} + */ +Scratch3SoundBlocks.TEMPO_RANGE = {min: 20, max: 500}; + + /** The minimum and maximum values for each sound effect. + * @type {{effect:{min: number, max: number}}} + */ +Scratch3SoundBlocks.EFFECT_RANGE = { + pitch: {min: -600, max: 600}, // -5 to 5 octaves + pan: {min: -100, max: 100}, // 100% left to 100% right + echo: {min: 0, max: 100}, // 0 to max (75%) feedback + reverb: {min: 0, max: 100}, // wet/dry: 0 to 100% wet + fuzz: {min: 0, max: 100}, // wed/dry: 0 to 100% wet + robot: {min: 0, max: 600} // 0 to 5 octaves +}; + /** * @param {Target} target - collect sound state for this target. * @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary. @@ -132,7 +162,9 @@ Scratch3SoundBlocks.prototype.stopAllSounds = function (args, util) { Scratch3SoundBlocks.prototype.playNoteForBeats = function (args, util) { var note = Cast.toNumber(args.NOTE); + note = MathUtil.clamp(note, Scratch3SoundBlocks.MIDI_NOTE_RANGE.min, Scratch3SoundBlocks.MIDI_NOTE_RANGE.max); var beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); var soundState = this._getSoundState(util.target); var inst = soundState.currentInstrument; var vol = soundState.volume; @@ -146,16 +178,22 @@ Scratch3SoundBlocks.prototype.playDrumForBeats = function (args, util) { if (typeof this.runtime.audioEngine === 'undefined') return; drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums); var beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); if (util.target.audioPlayer === null) return; return util.target.audioPlayer.playDrumForBeats(drum, beats); }; Scratch3SoundBlocks.prototype.restForBeats = function (args) { var beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); if (typeof this.runtime.audioEngine === 'undefined') return; return this.runtime.audioEngine.waitForBeats(beats); }; +Scratch3SoundBlocks.prototype._clampBeats = function (beats) { + return MathUtil.clamp(beats, Scratch3SoundBlocks.BEAT_RANGE.min, Scratch3SoundBlocks.BEAT_RANGE.max); +}; + Scratch3SoundBlocks.prototype.setInstrument = function (args, util) { var soundState = this._getSoundState(util.target); var instNum = Cast.toNumber(args.INSTRUMENT); @@ -167,25 +205,29 @@ Scratch3SoundBlocks.prototype.setInstrument = function (args, util) { }; Scratch3SoundBlocks.prototype.setEffect = function (args, util) { - var effect = Cast.toString(args.EFFECT).toLowerCase(); - var value = Cast.toNumber(args.VALUE); - - var soundState = this._getSoundState(util.target); - if (!soundState.effects.hasOwnProperty(effect)) return; - - soundState.effects[effect] = value; - if (util.target.audioPlayer === null) return; - util.target.audioPlayer.setEffect(effect, soundState.effects[effect]); + this._updateEffect(args, util, false); }; Scratch3SoundBlocks.prototype.changeEffect = function (args, util) { + this._updateEffect(args, util, true); +}; + +Scratch3SoundBlocks.prototype._updateEffect = function (args, util, change) { var effect = Cast.toString(args.EFFECT).toLowerCase(); var value = Cast.toNumber(args.VALUE); var soundState = this._getSoundState(util.target); if (!soundState.effects.hasOwnProperty(effect)) return; - soundState.effects[effect] += value; + if (change) { + soundState.effects[effect] += value; + } else { + soundState.effects[effect] = value; + } + + var effectRange = Scratch3SoundBlocks.EFFECT_RANGE[effect]; + soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], effectRange.min, effectRange.max); + if (util.target.audioPlayer === null) return; util.target.audioPlayer.setEffect(effect, soundState.effects[effect]); }; @@ -193,6 +235,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; @@ -224,15 +267,21 @@ Scratch3SoundBlocks.prototype.getVolume = function (args, util) { }; Scratch3SoundBlocks.prototype.setTempo = function (args) { - var value = Cast.toNumber(args.TEMPO); - if (typeof this.runtime.audioEngine === 'undefined') return; - this.runtime.audioEngine.setTempo(value); + var tempo = Cast.toNumber(args.TEMPO); + this._updateTempo(tempo); }; Scratch3SoundBlocks.prototype.changeTempo = function (args) { - var value = Cast.toNumber(args.TEMPO); + var change = Cast.toNumber(args.TEMPO); if (typeof this.runtime.audioEngine === 'undefined') return; - this.runtime.audioEngine.changeTempo(value); + var tempo = change + this.runtime.audioEngine.currentTempo; + this._updateTempo(tempo); +}; + +Scratch3SoundBlocks.prototype._updateTempo = function (tempo) { + tempo = MathUtil.clamp(tempo, Scratch3SoundBlocks.TEMPO_RANGE.min, Scratch3SoundBlocks.TEMPO_RANGE.max); + if (typeof this.runtime.audioEngine === 'undefined') return; + this.runtime.audioEngine.setTempo(tempo); }; Scratch3SoundBlocks.prototype.getTempo = function () { 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 f8dacf93a..e18521196 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -145,6 +145,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') && @@ -162,6 +163,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') && @@ -411,6 +413,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) { @@ -427,6 +430,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 c91361aae..fa10e3a01 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -121,11 +121,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? @@ -227,8 +229,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 1632aa28d..1ebab55b0 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -441,6 +441,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, var newThreads = []; for (var opts in optMatchFields) { + if (!optMatchFields.hasOwnProperty(opts)) continue; optMatchFields[opts] = optMatchFields[opts].toUpperCase(); } @@ -465,6 +466,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, if (Object.keys(hatFields).length === 0) { var hatInputs = blocks.getInputs(block); for (var input in hatInputs) { + if (!hatInputs.hasOwnProperty(input)) continue; var id = hatInputs[input].block; var inpBlock = blocks.getBlock(id); var fields = blocks.getFields(inpBlock); @@ -595,6 +597,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); @@ -793,6 +796,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/import/sb2import.js b/src/import/sb2import.js index 20669466e..39a1b1f15 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.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/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/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 0a0a115f9..e062df53f 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) { @@ -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; @@ -673,9 +706,10 @@ RenderedTarget.prototype.onGreenFlag = function () { /** * Called when the project receives a "stop all" - * Stop all sounds + * Stop all sounds and clear graphic effects. */ RenderedTarget.prototype.onStopAll = function () { + this.clearEffects(); if (this.audioPlayer) { this.audioPlayer.stopAllSounds(); this.audioPlayer.clearEffects(); @@ -687,15 +721,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 +742,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 @@ -716,6 +768,7 @@ RenderedTarget.prototype.toJSON = function () { x: this.x, y: this.y, direction: this.direction, + draggable: this.draggable, costume: this.getCurrentCostume(), costumeCount: this.getCostumes().length, visible: this.visible, diff --git a/src/util/string-util.js b/src/util/string-util.js new file mode 100644 index 000000000..6270cd885 --- /dev/null +++ b/src/util/string-util.js @@ -0,0 +1,17 @@ +var StringUtil = function () {}; + +StringUtil.withoutTrailingDigits = function (s) { + var i = s.length - 1; + while ((i >= 0) && ('0123456789'.indexOf(s.charAt(i)) > -1)) i--; + return s.slice(0, i + 1); +}; + +StringUtil.unusedName = function (name, existingNames) { + if (existingNames.indexOf(name) < 0) return name; + name = StringUtil.withoutTrailingDigits(name); + var i = 2; + while (existingNames.indexOf(name + i) >= 0) i++; + return name + i; +}; + +module.exports = StringUtil; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 003c80e9f..576e3a78a 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -3,6 +3,9 @@ var util = require('util'); var Runtime = require('./engine/runtime'); var sb2import = require('./import/sb2import'); +var StringUtil = require('./util/string-util'); + +var RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; /** * Handles connections between blocks, stage, and extensions. @@ -204,7 +207,15 @@ VirtualMachine.prototype.renameSprite = function (targetId, newName) { if (!sprite) { throw new Error('No sprite associated with this target.'); } - sprite.name = newName; + if (newName && RESERVED_NAMES.indexOf(newName) === -1) { + var names = this.runtime.targets.filter(function (runtimeTarget) { + return runtimeTarget.isSprite(); + }).map(function (runtimeTarget) { + return runtimeTarget.sprite.name; + }); + + sprite.name = StringUtil.unusedName(newName, names); + } this.emitTargetsUpdate(); } else { throw new Error('No target with the provided id.'); @@ -328,6 +339,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 && 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. diff --git a/test/integration/complex.js b/test/integration/complex.js index 1ff1d384f..f8dbd2cc2 100644 --- a/test/integration/complex.js +++ b/test/integration/complex.js @@ -26,7 +26,7 @@ test('complex', function (t) { var targets = data.targetList; for (var i in targets) { if (targets[i].isStage === true) continue; - if (targets[i].name === 'test') continue; + if (targets[i].name.match(/test/)) continue; vm.setEditingTarget(targets[i].id); vm.renameSprite(targets[i].id, 'test'); @@ -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/unit/sprites_rendered-target.js b/test/unit/sprites_rendered-target.js index 58c61bdf7..3ff40e2cd 100644 --- a/test/unit/sprites_rendered-target.js +++ b/test/unit/sprites_rendered-target.js @@ -11,3 +11,13 @@ test('clone effects', function (t) { t.ok(a.effects !== b.effects); t.end(); }); + +test('#stopAll clears graphics effects', function (t) { + var spr = new Sprite(); + var a = new RenderedTarget(spr, null); + var effectName = 'brightness'; + a.setEffect(effectName, 100); + a.onStopAll(); + t.equals(a.effects[effectName], 0); + t.end(); +}); diff --git a/test/unit/util_string.js b/test/unit/util_string.js new file mode 100644 index 000000000..fdcd439d3 --- /dev/null +++ b/test/unit/util_string.js @@ -0,0 +1,57 @@ +var test = require('tap').test; +var StringUtil = require('../../src/util/string-util'); + +test('withoutTrailingDigits', function (t) { + t.strictEqual(StringUtil.withoutTrailingDigits('boeing747'), 'boeing'); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing747 '), 'boeing747 '); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing𝟨'), 'boeing𝟨'); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing 747'), 'boeing '); + t.strictEqual(StringUtil.withoutTrailingDigits('747'), ''); + t.end(); +}); + +test('unusedName', function (t) { + t.strictEqual( + StringUtil.unusedName( + 'name', + ['not the same name'] + ), + 'name' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name'] + ), + 'name2' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name30'] + ), + 'name' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name', 'name2'] + ), + 'name3' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name', 'name3'] + ), + 'name2' + ); + t.strictEqual( + StringUtil.unusedName( + 'boeing747', + ['boeing747'] + ), + 'boeing2' // Yup, this matches scratch-flash... + ); + t.end(); +}); diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js new file mode 100644 index 000000000..eed537dd3 --- /dev/null +++ b/test/unit/virtual-machine.js @@ -0,0 +1,96 @@ +var test = require('tap').test; +var VirtualMachine = require('../../src/virtual-machine.js'); + +test('renameSprite throws when there is no sprite with that id', function (t) { + var vm = new VirtualMachine(); + vm.runtime.getTargetById = () => null; + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('No target with the provided id.') + ); + t.end(); +}); + +test('renameSprite throws when used on a non-sprite target', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + isSprite: () => false + }; + vm.runtime.getTargetById = () => (fakeTarget); + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('Cannot rename non-sprite targets.') + ); + t.end(); +}); + +test('renameSprite throws when there is no sprite for given target', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + sprite: null, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('No sprite associated with this target.') + ); + t.end(); +}); + +test('renameSprite sets the sprite name', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', 'not-original'); + t.equal(fakeTarget.sprite.name, 'not-original'); + t.end(); +}); + +test('renameSprite does not set sprite names to an empty string', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', ''); + t.equal(fakeTarget.sprite.name, 'original'); + t.end(); +}); + +test('renameSprite does not set sprite names to reserved names', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', '_mouse_'); + t.equal(fakeTarget.sprite.name, 'original'); + t.end(); +}); + +test('renameSprite increments from existing sprite names', function (t) { + var vm = new VirtualMachine(); + vm.emitTargetsUpdate = () => {}; + vm.runtime.targets = [{ + id: 'id1', + isSprite: () => true, + sprite: { + name: 'this name' + } + }, { + id: 'id2', + isSprite: () => true, + sprite: { + name: 'that name' + } + }]; + vm.renameSprite('id1', 'that name'); + t.equal(vm.runtime.targets[0].sprite.name, 'that name2'); + t.end(); +});