diff --git a/.travis.yml b/.travis.yml index 29c45a80..955aa5ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: node_js node_js: - 6 - node +env: + - NPM_SCRIPT="tap:unit -- --jobs=4" + - NPM_SCRIPT="tap:integration -- --jobs=4" sudo: false cache: directories: @@ -9,27 +12,29 @@ cache: install: - npm install - npm update -after_script: -- | - # RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel - declare exitCode - $(npm bin)/travis-after-all - exitCode=$? - if [[ - # Execute after all jobs finish successfully - $exitCode = 0 && - # Only release on release branches - $RELEASE_BRANCHES =~ $TRAVIS_BRANCH && - # Don't release on PR builds - $TRAVIS_PULL_REQUEST = "false" - ]]; then - # Authenticate NPM - echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc - # Set version to timestamp - npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s) - npm publish - # Publish to gh-pages as most recent committer - git config --global user.email $(git log --pretty=format:"%ae" -n1) - git config --global user.name $(git log --pretty=format:"%an" -n1) - npm run --silent deploy -- -x -r $GH_PAGES_REPO - fi +script: npm run $NPM_SCRIPT +jobs: + include: + - env: NPM_SCRIPT=lint + node_js: 6 + - stage: release + node_js: 6 + env: NPM_SCRIPT=build + before_deploy: + - npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s) + - git config --global user.email $(git log --pretty=format:"%ae" -n1) + - git config --global user.name $(git log --pretty=format:"%an" -n1) + deploy: + - provider: script + "on": + all_branches: true + condition: $RELEASE_BRANCHES =~ $TRAVIS_BRANCH + skip_cleanup: true + script: npm run --silent deploy -- -x -r $GH_PAGES_REPO + - provider: npm + "on": + all_branches: true + condition: $RELEASE_BRANCHES =~ $TRAVIS_BRANCH + skip_cleanup: true + email: $NPM_EMAIL + api_key: $NPM_TOKEN diff --git a/package.json b/package.json index d51b27e2..d20b8672 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,11 @@ "coverage": "./node_modules/.bin/tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov", "deploy": "touch playground/.nojekyll && ./node_modules/.bin/gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"", "lint": "./node_modules/.bin/eslint .", - "prepublish": "npm run build", - "prepublish-watch": "npm run watch", + "prepublish": "in-publish && npm run build || not-in-publish", "start": "./node_modules/.bin/webpack-dev-server", "tap": "./node_modules/.bin/tap ./test/{unit,integration}/*.js", + "tap:unit": "./node_modules/.bin/tap ./test/unit/*.js", + "tap:integration": "./node_modules/.bin/tap ./test/integration/*.js", "test": "npm run lint && npm run tap", "watch": "./node_modules/.bin/webpack --progress --colors --watch", "version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"" @@ -38,6 +39,7 @@ "highlightjs": "^9.8.0", "htmlparser2": "3.9.2", "immutable": "3.8.1", + "in-publish": "^2.0.0", "json": "^9.0.4", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", @@ -50,7 +52,6 @@ "socket.io-client": "1.7.3", "stats.js": "^0.17.0", "tap": "^10.2.0", - "travis-after-all": "^1.4.4", "webpack": "^2.4.1", "webpack-dev-server": "^2.4.1" } diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index a9361f23..6bd19ede 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -113,18 +113,18 @@ class Scratch3SoundBlocks { playSound (args, util) { const index = this._getSoundIndex(args.SOUND_MENU, util); if (index >= 0) { - const md5 = util.target.sprite.sounds[index].md5; + const soundId = util.target.sprite.sounds[index].soundId; if (util.target.audioPlayer === null) return; - util.target.audioPlayer.playSound(md5); + util.target.audioPlayer.playSound(soundId); } } playSoundAndWait (args, util) { const index = this._getSoundIndex(args.SOUND_MENU, util); if (index >= 0) { - const md5 = util.target.sprite.sounds[index].md5; + const soundId = util.target.sprite.sounds[index].soundId; if (util.target.audioPlayer === null) return; - return util.target.audioPlayer.playSound(md5); + return util.target.audioPlayer.playSound(soundId); } } @@ -135,18 +135,20 @@ class Scratch3SoundBlocks { return -1; } - let index; - - // try to convert to a number and use that as an index - const num = parseInt(soundName, 10); - if (!isNaN(num)) { - index = MathUtil.wrapClamp(num, 0, len - 1); + // look up by name first + const index = this.getSoundIndexByName(soundName, util); + if (index !== -1) { return index; } - // return the index for the sound of that name - index = this.getSoundIndexByName(soundName, util); - return index; + // then try using the sound name as a 1-indexed index + const oneIndexedIndex = parseInt(soundName, 10); + if (!isNaN(oneIndexedIndex)) { + return MathUtil.wrapClamp(oneIndexedIndex - 1, 0, len - 1); + } + + // could not be found as a name or converted to index, return -1 + return -1; } getSoundIndexByName (soundName, util) { @@ -181,7 +183,7 @@ class Scratch3SoundBlocks { let drum = Cast.toNumber(args.DRUM); drum -= 1; // drums are one-indexed if (typeof this.runtime.audioEngine === 'undefined') return; - drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums); + drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1); let beats = Cast.toNumber(args.BEATS); beats = this._clampBeats(beats); if (util.target.audioPlayer === null) return; @@ -204,7 +206,7 @@ class Scratch3SoundBlocks { let instNum = Cast.toNumber(args.INSTRUMENT); instNum -= 1; // instruments are one-indexed if (typeof this.runtime.audioEngine === 'undefined') return; - instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments); + instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments - 1); soundState.currentInstrument = instNum; return this.runtime.audioEngine.instrumentPlayer.loadInstrument(soundState.currentInstrument); } diff --git a/src/engine/target.js b/src/engine/target.js index da00074b..2a6be81a 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -4,6 +4,7 @@ const Blocks = require('./blocks'); const Variable = require('../engine/variable'); const List = require('../engine/list'); const uid = require('../util/uid'); +const {Map} = require('immutable'); /** * @fileoverview @@ -161,6 +162,21 @@ class Target extends EventEmitter { const variable = this.variables[id]; if (variable.id === id) { variable.name = newName; + + if (this.runtime) { + const blocks = this.runtime.monitorBlocks; + blocks.changeBlock({ + id: id, + element: 'field', + name: 'VARIABLE', + value: id + }, this.runtime); + this.runtime.requestUpdateMonitor(Map({ + id: id, + params: blocks._getBlockParams(blocks.getBlock(variable.id)) + })); + } + } } } @@ -172,6 +188,9 @@ class Target extends EventEmitter { deleteVariable (id) { if (this.variables.hasOwnProperty(id)) { delete this.variables[id]; + if (this.runtime) { + this.runtime.requestRemoveMonitor(id); + } } } diff --git a/src/import/load-sound.js b/src/import/load-sound.js index 295b87eb..8f4f9edf 100644 --- a/src/import/load-sound.js +++ b/src/import/load-sound.js @@ -31,7 +31,10 @@ const loadSound = function (sound, runtime) { {data: soundAsset.data} )); }) - .then(() => sound); + .then(soundId => { + sound.soundId = soundId; + return sound; + }); }; module.exports = loadSound; diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 2946756d..48e89f44 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -77,15 +77,16 @@ const flatten = function (blocks) { * a list of blocks in a branch (e.g., in forever), * or a list of blocks in an argument (e.g., move [pick random...]). * @param {Array.} blockList SB2 JSON-format block list. + * @param {Function} getVariableId function to retreive a variable's ID based on name * @return {Array.} Scratch VM-format block list. */ -const parseBlockList = function (blockList) { +const parseBlockList = function (blockList, getVariableId) { const resultingList = []; let previousBlock = null; // For setting next. for (let i = 0; i < blockList.length; i++) { const block = blockList[i]; // eslint-disable-next-line no-use-before-define - const parsedBlock = parseBlock(block); + const parsedBlock = parseBlock(block, getVariableId); if (typeof parsedBlock === 'undefined') continue; if (previousBlock) { parsedBlock.parent = previousBlock.id; @@ -102,14 +103,15 @@ const parseBlockList = function (blockList) { * This should only handle top-level scripts that include X, Y coordinates. * @param {!object} scripts Scripts object from SB2 JSON. * @param {!Blocks} blocks Blocks object to load parsed blocks into. + * @param {Function} getVariableId function to retreive a variable's ID based on name */ -const parseScripts = function (scripts, blocks) { +const parseScripts = function (scripts, blocks, getVariableId) { for (let i = 0; i < scripts.length; i++) { const script = scripts[i]; const scriptX = script[0]; const scriptY = script[1]; const blockList = script[2]; - const parsedBlockList = parseBlockList(blockList); + const parsedBlockList = parseBlockList(blockList, getVariableId); if (parsedBlockList[0]) { // Adjust script coordinates to account for // larger block size in scratch-blocks. @@ -127,6 +129,30 @@ const parseScripts = function (scripts, blocks) { } }; +/** + * Create a callback for assigning fixed IDs to imported variables + * Generator stores the global variable mapping in a closure + * @param {!string} targetId the id of the target to scope the variable to + * @return {string} variable ID + */ +const generateVariableIdGetter = (function () { + let globalVariableNameMap = {}; + const namer = (targetId, name) => `${targetId}-${name}`; + return function (targetId, topLevel) { + // Reset the global variable map if topLevel + if (topLevel) globalVariableNameMap = {}; + return function (name) { + if (topLevel) { // Store the name/id pair in the globalVariableNameMap + globalVariableNameMap[name] = namer(targetId, name); + return globalVariableNameMap[name]; + } + // Not top-level, so first check the global name map + if (globalVariableNameMap[name]) return globalVariableNameMap[name]; + return namer(targetId, name); + }; + }; +}()); + /** * Parse a single "Scratch object" and create all its in-memory VM objects. * @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher. @@ -180,19 +206,18 @@ const parseScratchObject = function (object, runtime, topLevel) { soundPromises.push(loadSound(sound, runtime)); } } - // If included, parse any and all scripts/blocks on the object. - if (object.hasOwnProperty('scripts')) { - parseScripts(object.scripts, blocks); - } + // Create the first clone, and load its run-state from JSON. const target = sprite.createClone(); + const getVariableId = generateVariableIdGetter(target.id, topLevel); + // Load target properties from JSON. if (object.hasOwnProperty('variables')) { for (let j = 0; j < object.variables.length; j++) { const variable = object.variables[j]; const newVariable = new Variable( - null, + getVariableId(variable.name), variable.name, variable.value, variable.isPersistent @@ -200,6 +225,12 @@ const parseScratchObject = function (object, runtime, topLevel) { target.variables[newVariable.id] = newVariable; } } + + // If included, parse any and all scripts/blocks on the object. + if (object.hasOwnProperty('scripts')) { + parseScripts(object.scripts, blocks, getVariableId); + } + if (object.hasOwnProperty('lists')) { for (let k = 0; k < object.lists.length; k++) { const list = object.lists[k]; @@ -294,9 +325,10 @@ const sb2import = function (json, runtime, optForceSprite) { /** * Parse a single SB2 JSON-formatted block and its children. * @param {!object} sb2block SB2 JSON-formatted block. + * @param {Function} getVariableId function to retreive a variable's ID based on name * @return {object} Scratch VM format block. */ -const parseBlock = function (sb2block) { +const parseBlock = function (sb2block, getVariableId) { // First item in block object is the old opcode (e.g., 'forward:'). const oldOpcode = sb2block[0]; // Convert the block using the specMap. See sb2specmap.js. @@ -341,10 +373,10 @@ const parseBlock = function (sb2block) { let innerBlocks; if (typeof providedArg[0] === 'object' && providedArg[0]) { // Block list occupies the input. - innerBlocks = parseBlockList(providedArg); + innerBlocks = parseBlockList(providedArg, getVariableId); } else { // Single block occupies the input. - innerBlocks = [parseBlock(providedArg)]; + innerBlocks = [parseBlock(providedArg, getVariableId)]; } let previousBlock = null; for (let j = 0; j < innerBlocks.length; j++) { @@ -426,6 +458,11 @@ const parseBlock = function (sb2block) { name: expectedArg.fieldName, value: providedArg }; + + if (expectedArg.fieldName === 'VARIABLE') { + // Add `id` property to variable fields + activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg); + } } } // Special cases to generate mutations. diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 7f7fbe0d..bbf85231 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -1,5 +1,6 @@ const log = require('../util/log'); const MathUtil = require('../util/math-util'); +const StringUtil = require('../util/string-util'); const Target = require('../engine/target'); /** @@ -230,6 +231,9 @@ class RenderedTarget extends Target { if (this.isStage) { return; } + if (!isFinite(direction)) { + return; + } // Keep direction between -179 and +180. this.direction = MathUtil.wrapClamp(direction, -179, 180); if (this.renderer) { @@ -391,6 +395,28 @@ class RenderedTarget extends Target { this.runtime.requestTargetsUpdate(this); } + /** + * Add a costume, taking care to avoid duplicate names. + * @param {!object} costumeObject Object representing the costume. + */ + addCostume (costumeObject) { + const usedNames = this.sprite.costumes.map(costume => costume.name); + costumeObject.name = StringUtil.unusedName(costumeObject.name, usedNames); + this.sprite.costumes.push(costumeObject); + } + + /** + * Rename a costume, taking care to avoid duplicate names. + * @param {int} costumeIndex - the index of the costume to be renamed. + * @param {string} newName - the desired new name of the costume (will be modified if already in use). + */ + renameCostume (costumeIndex, newName) { + const usedNames = this.sprite.costumes + .filter((costume, index) => costumeIndex !== index) + .map(costume => costume.name); + this.sprite.costumes[costumeIndex].name = StringUtil.unusedName(newName, usedNames); + } + /** * Delete a costume by index. * @param {number} index Costume index to be deleted @@ -414,6 +440,28 @@ class RenderedTarget extends Target { this.runtime.requestTargetsUpdate(this); } + /** + * Add a sound, taking care to avoid duplicate names. + * @param {!object} soundObject Object representing the sound. + */ + addSound (soundObject) { + const usedNames = this.sprite.sounds.map(sound => sound.name); + soundObject.name = StringUtil.unusedName(soundObject.name, usedNames); + this.sprite.sounds.push(soundObject); + } + + /** + * Rename a sound, taking care to avoid duplicate names. + * @param {int} soundIndex - the index of the sound to be renamed. + * @param {string} newName - the desired new name of the sound (will be modified if already in use). + */ + renameSound (soundIndex, newName) { + const usedNames = this.sprite.sounds + .filter((sound, index) => soundIndex !== index) + .map(sound => sound.name); + this.sprite.sounds[soundIndex].name = StringUtil.unusedName(newName, usedNames); + } + /** * Delete a sound by index. * @param {number} index Sound index to be deleted diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 2ca4e21d..762ef8c0 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -280,13 +280,23 @@ class VirtualMachine extends EventEmitter { */ addCostume (md5ext, costumeObject) { loadCostume(md5ext, costumeObject, this.runtime).then(() => { - this.editingTarget.sprite.costumes.push(costumeObject); + this.editingTarget.addCostume(costumeObject); this.editingTarget.setCostume( this.editingTarget.sprite.costumes.length - 1 ); }); } + /** + * Rename a costume on the current editing target. + * @param {int} costumeIndex - the index of the costume to be renamed. + * @param {string} newName - the desired new name of the costume (will be modified if already in use). + */ + renameCostume (costumeIndex, newName) { + this.editingTarget.renameCostume(costumeIndex, newName); + this.emitTargetsUpdate(); + } + /** * Delete a costume from the current editing target. * @param {int} costumeIndex - the index of the costume to be removed. @@ -302,11 +312,47 @@ class VirtualMachine extends EventEmitter { */ addSound (soundObject) { return loadSound(soundObject, this.runtime).then(() => { - this.editingTarget.sprite.sounds.push(soundObject); + this.editingTarget.addSound(soundObject); this.emitTargetsUpdate(); }); } + /** + * Rename a sound on the current editing target. + * @param {int} soundIndex - the index of the sound to be renamed. + * @param {string} newName - the desired new name of the sound (will be modified if already in use). + */ + renameSound (soundIndex, newName) { + this.editingTarget.renameSound(soundIndex, newName); + this.emitTargetsUpdate(); + } + + /** + * Get a sound buffer from the audio engine. + * @param {int} soundIndex - the index of the sound to be got. + * @return {AudioBuffer} the sound's audio buffer. + */ + getSoundBuffer (soundIndex) { + const id = this.editingTarget.sprite.sounds[soundIndex].soundId; + if (id && this.runtime && this.runtime.audioEngine) { + return this.runtime.audioEngine.getSoundBuffer(id); + } + return null; + } + + /** + * Update a sound buffer. + * @param {int} soundIndex - the index of the sound to be updated. + * @param {AudioBuffer} newBuffer - new audio buffer for the audio engine. + */ + updateSoundBuffer (soundIndex, newBuffer) { + const id = this.editingTarget.sprite.sounds[soundIndex].soundId; + if (id && this.runtime && this.runtime.audioEngine) { + this.runtime.audioEngine.updateSoundBuffer(id, newBuffer); + } + this.emitTargetsUpdate(); + } + /** * Delete a sound from the current editing target. * @param {int} soundIndex - the index of the sound to be removed. @@ -519,8 +565,11 @@ class VirtualMachine extends EventEmitter { * of the current editing target's blocks. */ emitWorkspaceUpdate () { - // @todo Include variables scoped to editing target also. - const variableMap = this.runtime.getTargetForStage().variables; + const variableMap = Object.assign({}, + this.runtime.getTargetForStage().variables, + this.editingTarget.variables + ); + const variables = Object.keys(variableMap).map(k => variableMap[k]); const xmlString = ` diff --git a/test/fixtures/data.sb2 b/test/fixtures/data.sb2 index a1a815ef..e4b8c0b1 100644 Binary files a/test/fixtures/data.sb2 and b/test/fixtures/data.sb2 differ diff --git a/test/integration/complex.js b/test/integration/complex.js index 11392848..331f2a98 100644 --- a/test/integration/complex.js +++ b/test/integration/complex.js @@ -43,7 +43,7 @@ test('complex', t => { vm.addCostume( 'f9a1c175dbe2e5dee472858dd30d16bb.svg', { - costumeName: 'costume1', + name: 'costume1', baseLayerID: 0, baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg', bitmapResolution: 1, @@ -79,7 +79,7 @@ test('complex', t => { vm.addBackdrop( '6b3d87ba2a7f89be703163b6c1d4c964.png', { - costumeName: 'baseball-field', + name: 'baseball-field', baseLayerID: 26, baseLayerMD5: '6b3d87ba2a7f89be703163b6c1d4c964.png', bitmapResolution: 2, diff --git a/test/unit/blocks_sounds.js b/test/unit/blocks_sounds.js new file mode 100644 index 00000000..753f46f0 --- /dev/null +++ b/test/unit/blocks_sounds.js @@ -0,0 +1,111 @@ +const test = require('tap').test; +const Sound = require('../../src/blocks/scratch3_sound'); +let playedSound; +let playedDrum; +let playedInstrument; +const runtime = { + audioEngine: { + numDrums: 3, + numInstruments: 3, + instrumentPlayer: { + loadInstrument: instrument => (playedInstrument = instrument) + } + } +}; +const blocks = new Sound(runtime); +const util = { + target: { + sprite: { + sounds: [ + {name: 'first name', soundId: 'first soundId'}, + {name: 'second name', soundId: 'second soundId'}, + {name: 'third name', soundId: 'third soundId'}, + {name: '6', soundId: 'fourth soundId'} + ] + }, + audioPlayer: { + playSound: soundId => (playedSound = soundId), + playDrumForBeats: drum => (playedDrum = drum) + } + } +}; + +test('playSound with a name string works', t => { + const args = {SOUND_MENU: 'second name'}; + blocks.playSound(args, util); + t.strictEqual(playedSound, 'second soundId'); + t.end(); +}); + +test('playSound with a number string works 1-indexed', t => { + let args = {SOUND_MENU: '5'}; + blocks.playSound(args, util); + t.strictEqual(playedSound, 'first soundId'); + + args = {SOUND_MENU: '1'}; + blocks.playSound(args, util); + t.strictEqual(playedSound, 'first soundId'); + + args = {SOUND_MENU: '0'}; + blocks.playSound(args, util); + t.strictEqual(playedSound, 'fourth soundId'); + t.end(); +}); + +test('playSound with a number works 1-indexed', t => { + let args = {SOUND_MENU: 5}; + blocks.playSound(args, util); + t.strictEqual(playedSound, 'first soundId'); + + args = {SOUND_MENU: 1}; + blocks.playSound(args, util); + t.strictEqual(playedSound, 'first soundId'); + + args = {SOUND_MENU: 0}; + blocks.playSound(args, util); + t.strictEqual(playedSound, 'fourth soundId'); + t.end(); +}); + +test('playSound prioritizes sound index if given a number', t => { + const args = {SOUND_MENU: 6}; + blocks.playSound(args, util); + // Ignore the sound named '6', wrapClamp to the second instead + t.strictEqual(playedSound, 'second soundId'); + t.end(); +}); + +test('playSound prioritizes sound name if given a string', t => { + const args = {SOUND_MENU: '6'}; + blocks.playSound(args, util); + // Use the sound named '6', which is the fourth + t.strictEqual(playedSound, 'fourth soundId'); + t.end(); +}); + +test('playDrum uses 1-indexing and wrap clamps', t => { + let args = {DRUM: 1}; + blocks.playDrumForBeats(args, util); + t.strictEqual(playedDrum, 0); + + args = {DRUM: runtime.audioEngine.numDrums + 1}; + blocks.playDrumForBeats(args, util); + t.strictEqual(playedDrum, 0); + + t.end(); +}); + +test('setInstrument uses 1-indexing and wrap clamps', t => { + // Stub getSoundState + blocks._getSoundState = () => ({}); + + let args = {INSTRUMENT: 1}; + blocks.setInstrument(args, util); + t.strictEqual(playedInstrument, 0); + + args = {INSTRUMENT: runtime.audioEngine.numInstruments + 1}; + blocks.setInstrument(args, util); + t.strictEqual(playedInstrument, 0); + + t.end(); +}); diff --git a/test/unit/serialization_sb2.js b/test/unit/serialization_sb2.js index cd3294f0..68535b40 100644 --- a/test/unit/serialization_sb2.js +++ b/test/unit/serialization_sb2.js @@ -51,3 +51,20 @@ test('default', t => { t.end(); }); }); + +test('data scoping', t => { + // Get SB2 JSON (string) + const uri = path.resolve(__dirname, '../fixtures/data.sb2'); + const file = extract(uri); + const json = JSON.parse(file); + + // Create runtime instance & load SB2 into it + const rt = new Runtime(); + sb2.deserialize(json, rt).then(targets => { + const globalVariableIds = Object.keys(targets[0].variables); + const localVariableIds = Object.keys(targets[1].variables); + t.equal(targets[0].variables[globalVariableIds[0]].name, 'foo'); + t.equal(targets[1].variables[localVariableIds[0]].name, 'local'); + t.end(); + }); +}); diff --git a/test/unit/sprites_rendered-target.js b/test/unit/sprites_rendered-target.js index ecb4a089..470afbfd 100644 --- a/test/unit/sprites_rendered-target.js +++ b/test/unit/sprites_rendered-target.js @@ -355,3 +355,43 @@ test('#toJSON returns the sounds and costumes', t => { t.same(a.toJSON().costumes, costumes); t.end(); }); + +test('#addSound does not duplicate names', t => { + const spr = new Sprite(); + const a = new RenderedTarget(spr, null); + a.sprite.sounds = [{name: 'first'}]; + a.addSound({name: 'first'}); + t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'first2'}]); + t.end(); +}); + +test('#addCostume does not duplicate names', t => { + const spr = new Sprite(); + const a = new RenderedTarget(spr, null); + a.sprite.costumes = [{name: 'first'}]; + a.addCostume({name: 'first'}); + t.deepEqual(a.sprite.costumes, [{name: 'first'}, {name: 'first2'}]); + t.end(); +}); + +test('#renameSound does not duplicate names', t => { + const spr = new Sprite(); + const a = new RenderedTarget(spr, null); + a.sprite.sounds = [{name: 'first'}, {name: 'second'}]; + a.renameSound(0, 'first'); // Shouldn't increment the name, noop + t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'second'}]); + a.renameSound(1, 'first'); + t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'first2'}]); + t.end(); +}); + +test('#renameCostume does not duplicate names', t => { + const spr = new Sprite(); + const a = new RenderedTarget(spr, null); + a.sprite.costumes = [{name: 'first'}, {name: 'second'}]; + a.renameCostume(0, 'first'); // Shouldn't increment the name, noop + t.deepEqual(a.sprite.costumes, [{name: 'first'}, {name: 'second'}]); + a.renameCostume(1, 'first'); + t.deepEqual(a.sprite.costumes, [{name: 'first'}, {name: 'first2'}]); + t.end(); +}); diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js index 37a62ff6..624dddcd 100644 --- a/test/unit/virtual-machine.js +++ b/test/unit/virtual-machine.js @@ -109,3 +109,42 @@ test('renameSprite does not increment when renaming to the same name', t => { t.equal(vm.runtime.targets[0].sprite.name, 'this name'); t.end(); }); + +test('emitWorkspaceUpdate', t => { + const vm = new VirtualMachine(); + vm.runtime.targets = [ + { + isStage: true, + variables: { + global: { + toXML: () => 'global' + } + } + }, { + variables: { + unused: { + toXML: () => 'unused' + } + } + }, { + variables: { + local: { + toXML: () => 'local' + } + }, + blocks: { + toXML: () => 'blocks' + } + } + ]; + vm.editingTarget = vm.runtime.targets[2]; + + let xml = null; + vm.emit = (event, data) => (xml = data.xml); + vm.emitWorkspaceUpdate(); + t.notEqual(xml.indexOf('global'), -1); + t.notEqual(xml.indexOf('local'), -1); + t.equal(xml.indexOf('unused'), -1); + t.notEqual(xml.indexOf('blocks'), -1); + t.end(); +});