diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 13122c1f1..0a6750224 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -6,6 +6,7 @@ const Clone = require('../util/clone'); const {Map} = require('immutable'); const BlocksExecuteCache = require('./blocks-execute-cache'); const log = require('../util/log'); +const Variable = require('./variable'); /** * @fileoverview @@ -670,6 +671,44 @@ class Blocks { this.resetCache(); } + /** + * Returns a map of all references to variables or lists from blocks + * in this block container. + * @return {object} A map of variable ID to a list of all variable references + * for that ID. A variable reference contains the field referencing that variable + * and also the type of the variable being referenced. + */ + getAllVariableAndListReferences () { + const blocks = this._blocks; + const allReferences = Object.create(null); + for (const blockId in blocks) { + let varOrListField = null; + let varType = null; + if (blocks[blockId].fields.VARIABLE) { + varOrListField = blocks[blockId].fields.VARIABLE; + varType = Variable.SCALAR_TYPE; + } else if (blocks[blockId].fields.LIST) { + varOrListField = blocks[blockId].fields.LIST; + varType = Variable.LIST_TYPE; + } + if (varOrListField) { + const currVarId = varOrListField.id; + if (allReferences[currVarId]) { + allReferences[currVarId].push({ + referencingField: varOrListField, + type: varType + }); + } else { + allReferences[currVarId] = [{ + referencingField: varOrListField, + type: varType + }]; + } + } + } + return allReferences; + } + /** * Keep blocks up to date after a variable gets renamed. * @param {string} varId The id of the variable that was renamed diff --git a/src/engine/target.js b/src/engine/target.js index a8c6828ee..69620c91b 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -6,6 +6,7 @@ const Comment = require('../engine/comment'); const uid = require('../util/uid'); const {Map} = require('immutable'); const log = require('../util/log'); +const StringUtil = require('../util/string-util'); /** * @fileoverview @@ -80,14 +81,40 @@ class Target extends EventEmitter { } /** - * Look up a variable object, and create it if one doesn't exist. + * Get the names of all the variables of the given type that are in scope for this target. + * For targets that are not the stage, this includes any target-specific + * variables as well as any stage variables. + * For the stage, this is all stage variables. + * @param {string} type The variable type to search for; defaults to Variable.SCALAR_TYPE + * @return {Array} A list of variable names + */ + getAllVariableNamesInScopeByType (type) { + if (typeof type !== 'string') type = Variable.SCALAR_TYPE; + const targetVariables = Object.values(this.variables) + .filter(v => v.type === type) + .map(variable => variable.name); + if (this.isStage || !this.runtime) { + return targetVariables; + } + const stage = this.runtime.getTargetForStage(); + const stageVariables = stage.getAllVariableNamesInScopeByType(type); + return targetVariables.concat(stageVariables); + } + + /** + * Look up a variable object, first by id, and then by name if the id is not found. + * Create a new variable if both lookups fail. * @param {string} id Id of the variable. * @param {string} name Name of the variable. * @return {!Variable} Variable object. */ lookupOrCreateVariable (id, name) { - const variable = this.lookupVariableById(id); + let variable = this.lookupVariableById(id); if (variable) return variable; + + variable = this.lookupVariableByNameAndType(name, Variable.SCALAR_TYPE); + if (variable) return variable; + // No variable with this name exists - create it locally. const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false); this.variables[id] = newVariable; @@ -161,6 +188,40 @@ class Target extends EventEmitter { } } + /** + * Look up a variable object by its name and variable type. + * Search begins with local variables; then global variables if a local one + * was not found. + * @param {string} name Name of the variable. + * @param {string} type Type of the variable. Defaults to Variable.SCALAR_TYPE. + * @return {?Variable} Variable object if found, or null if not. + */ + lookupVariableByNameAndType (name, type) { + if (typeof name !== 'string') return; + if (typeof type !== 'string') type = Variable.SCALAR_TYPE; + + for (const varId in this.variables) { + const currVar = this.variables[varId]; + if (currVar.name === name && currVar.type === type) { + return currVar; + } + } + + if (this.runtime && !this.isStage) { + const stage = this.runtime.getTargetForStage(); + if (stage) { + for (const varId in stage.variables) { + const currVar = stage.variables[varId]; + if (currVar.name === name && currVar.type === type) { + return currVar; + } + } + } + } + + return null; + } + /** * Look up a list object for this target, and create it if one doesn't exist. * Search begins for local lists; then look for globals. @@ -169,8 +230,12 @@ class Target extends EventEmitter { * @return {!Varible} Variable object representing the found/created list. */ lookupOrCreateList (id, name) { - const list = this.lookupVariableById(id); + let list = this.lookupVariableById(id); if (list) return list; + + list = this.lookupVariableByNameAndType(name, Variable.LIST_TYPE); + if (list) return list; + // No variable with this name exists - create it locally. const newList = new Variable(id, name, Variable.LIST_TYPE, false); this.variables[id] = newList; @@ -240,10 +305,13 @@ class Target extends EventEmitter { name: 'VARIABLE', value: id }, this.runtime); - this.runtime.requestUpdateMonitor(Map({ - id: id, - params: blocks._getBlockParams(blocks.getBlock(variable.id)) - })); + const monitorBlock = blocks.getBlock(variable.id); + if (monitorBlock) { + this.runtime.requestUpdateMonitor(Map({ + id: id, + params: blocks._getBlockParams(monitorBlock) + })); + } } } @@ -264,6 +332,101 @@ class Target extends EventEmitter { } } + /** + * Fixes up variable references in this target avoiding conflicts with + * pre-existing variables in the same scope. + * This is used when uploading this target as a new sprite into an existing + * project, where the new sprite may contain references + * to variable names that already exist as global variables in the project + * (and thus are in scope for variable references in the given sprite). + * + * If the given target has a block that references an existing global variable and that + * variable *does not* exist in the target itself (e.g. it was a global variable in the + * project the sprite was originally exported from), fix the variable references in this sprite + * to reference the id of the pre-existing global variable. + * If the given target has a block that references an existing global variable and that + * variable does exist in the target itself (e.g. it's a local variable in the sprite being uploaded), + * then the variable is renamed to distinguish itself from the pre-existing variable. + * All blocks that reference the local variable will be updated to use the new name. + */ + fixUpVariableReferences () { + if (!this.runtime) return; // There's no runtime context to conflict with + if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded) + const stage = this.runtime.getTargetForStage(); + if (!stage || !stage.variables) return; + + const renameConflictingLocalVar = (id, name, type) => { + const conflict = stage.lookupVariableByNameAndType(name, type); + if (conflict) { + const newName = StringUtil.unusedName( + `${this.getName()}: ${name}`, + this.getAllVariableNamesInScopeByType(type)); + this.renameVariable(id, newName); + return newName; + } + return null; + }; + + const allReferences = this.blocks.getAllVariableAndListReferences(); + const unreferencedLocalVarIds = []; + if (Object.keys(this.variables).length > 0) { + for (const localVarId in this.variables) { + if (!this.variables.hasOwnProperty(localVarId)) continue; + if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId); + } + } + const conflictIdsToReplace = Object.create(null); + for (const varId in allReferences) { + // We don't care about which var ref we get, they should all have the same var info + const varRef = allReferences[varId][0]; + const varName = varRef.referencingField.value; + const varType = varRef.type; + if (this.lookupVariableById(varId)) { + // Found a variable with the id in either the target or the stage, + // figure out which one. + if (this.variables.hasOwnProperty(varId)) { + // If the target has the variable, then check whether the stage + // has one with the same name and type. If it does, then rename + // this target specific variable so that there is a distinction. + const newVarName = renameConflictingLocalVar(varId, varName, varType); + + if (newVarName) { + // We are not calling this.blocks.updateBlocksAfterVarRename + // here because it will search through all the blocks. We already + // have access to all the references for this var id. + allReferences[varId].map(ref => { + ref.referencingField.value = newVarName; + return ref; + }); + } + } + } else { + const existingVar = this.lookupVariableByNameAndType(varName, varType); + if (existingVar && !conflictIdsToReplace[varId]) { + conflictIdsToReplace[varId] = existingVar.id; + } + } + } + // Rename any local variables that were missed above because they aren't + // referenced by any blocks + for (const id in unreferencedLocalVarIds) { + const varId = unreferencedLocalVarIds[id]; + const name = this.variables[varId].name; + const type = this.variables[varId].type; + renameConflictingLocalVar(varId, name, type); + } + // Finally, handle global var conflicts (e.g. a sprite is uploaded, and has + // blocks referencing some variable that the sprite does not own, and this + // variable conflicts with a global var) + for (const conflictId in conflictIdsToReplace) { + const existingId = conflictIdsToReplace[conflictId]; + allReferences[conflictId].map(varRef => { + varRef.referencingField.id = existingId; + return varRef; + }); + } + } + /** * Post/edit sprite info. * @param {object} data An object with sprite info data to set. diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 0ab76a15d..96ad5dd5b 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -446,15 +446,25 @@ const serializeTarget = function (target) { /** * Serializes the specified VM runtime. - * @param {!Runtime} runtime VM runtime instance to be serialized. + * @param {!Runtime} runtime VM runtime instance to be serialized. + * @param {string=} targetId Optional target id if serializing only a single target * @return {object} Serialized runtime instance. */ -const serialize = function (runtime) { +const serialize = function (runtime, targetId) { // Fetch targets const obj = Object.create(null); - const flattenedOriginalTargets = JSON.parse(JSON.stringify( + const flattenedOriginalTargets = JSON.parse(JSON.stringify(targetId ? + [runtime.getTargetById(targetId)] : runtime.targets.filter(target => target.isOriginal))); - obj.targets = flattenedOriginalTargets.map(t => serializeTarget(t, runtime)); + + const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, runtime)); + + if (targetId) { + return serializedTargets[0]; + } + + obj.targets = serializedTargets; + // TODO Serialize monitors @@ -926,10 +936,11 @@ const deserialize = function (json, runtime, zip, isSingleSprite) { return Promise.all( ((isSingleSprite ? [json] : json.targets) || []).map(target => parseScratchObject(target, runtime, extensions, zip)) - ).then(targets => ({ - targets, - extensions - })); + ) + .then(targets => ({ + targets, + extensions + })); }; module.exports = { diff --git a/src/serialization/serialize-assets.js b/src/serialization/serialize-assets.js index 9dc7bf0d9..ac444f11a 100644 --- a/src/serialization/serialize-assets.js +++ b/src/serialization/serialize-assets.js @@ -5,10 +5,11 @@ * to be written and the contents of the file, the serialized asset. * @param {Runtime} runtime The runtime with the assets to be serialized * @param {string} assetType The type of assets to be serialized: 'sounds' | 'costumes' + * @param {string=} optTargetId Optional target id to serialize assets for * @returns {Array} An array of file descriptors for each asset */ -const serializeAssets = function (runtime, assetType) { - const targets = runtime.targets; +const serializeAssets = function (runtime, assetType, optTargetId) { + const targets = optTargetId ? [runtime.getTargetById(optTargetId)] : runtime.targets; const assetDescs = []; for (let i = 0; i < targets.length; i++) { const currTarget = targets[i]; @@ -27,14 +28,16 @@ const serializeAssets = function (runtime, assetType) { }; /** - * Serialize all the sounds in the provided runtime into an array of file - * descriptors. A file descriptor is an object containing the name of the file + * Serialize all the sounds in the provided runtime or, if a target id is provided, + * in the specified target into an array of file descriptors. + * A file descriptor is an object containing the name of the file * to be written and the contents of the file, the serialized sound. * @param {Runtime} runtime The runtime with the sounds to be serialized + * @param {string=} optTargetId Optional targetid for serializing sounds of a single target * @returns {Array} An array of file descriptors for each sound */ -const serializeSounds = function (runtime) { - return serializeAssets(runtime, 'sounds'); +const serializeSounds = function (runtime, optTargetId) { + return serializeAssets(runtime, 'sounds', optTargetId); }; /** @@ -42,10 +45,11 @@ const serializeSounds = function (runtime) { * descriptors. A file descriptor is an object containing the name of the file * to be written and the contents of the file, the serialized costume. * @param {Runtime} runtime The runtime with the costumes to be serialized + * @param {string} optTargetId Optional targetid for serializing costumes of a single target * @returns {Array} An array of file descriptors for each costume */ -const serializeCostumes = function (runtime) { - return serializeAssets(runtime, 'costumes'); +const serializeCostumes = function (runtime, optTargetId) { + return serializeAssets(runtime, 'costumes', optTargetId); }; module.exports = { diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 57ae97e76..940991e49 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -263,14 +263,7 @@ class VirtualMachine extends EventEmitter { // Put everything in a zip file zip.file('project.json', projectJson); - for (let i = 0; i < soundDescs.length; i++) { - const currSound = soundDescs[i]; - zip.file(currSound.fileName, currSound.fileContent); - } - for (let i = 0; i < costumeDescs.length; i++) { - const currCostume = costumeDescs[i]; - zip.file(currCostume.fileName, currCostume.fileContent); - } + this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip); return zip.generateAsync({ type: 'blob', @@ -281,6 +274,43 @@ class VirtualMachine extends EventEmitter { }); } + _addFileDescsToZip (fileDescs, zip) { + for (let i = 0; i < fileDescs.length; i++) { + const currFileDesc = fileDescs[i]; + zip.file(currFileDesc.fileName, currFileDesc.fileContent); + } + } + + /** + * Exports a sprite in the sprite3 format. + * @param {string} targetId ID of the target to export + * @param {string=} optZipType Optional type that the resulting + * zip should be outputted in. Options are: base64, binarystring, + * array, uint8array, arraybuffer, blob, or nodebuffer. Defaults to + * blob if argument not provided. + * See https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html#type-option + * for more information about these options. + * @return {object} A generated zip of the sprite and its assets in the format + * specified by optZipType or blob by default. + */ + exportSprite (targetId, optZipType) { + const soundDescs = serializeSounds(this.runtime, targetId); + const costumeDescs = serializeCostumes(this.runtime, targetId); + const spriteJson = JSON.stringify(sb3.serialize(this.runtime, targetId)); + + const zip = new JSZip(); + zip.file('sprite.json', spriteJson); + this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip); + + return zip.generateAsync({ + type: typeof optZipType === 'string' ? optZipType : 'blob', + compression: 'DEFLATE', + compressionOptions: { + level: 6 + } + }); + } + /** * Export project as a Scratch 3.0 JSON representation. * @return {string} Serialized state of the runtime. @@ -368,6 +398,10 @@ class VirtualMachine extends EventEmitter { this.editingTarget = targets[0]; } + if (!wholeProject) { + this.editingTarget.fixUpVariableReferences(); + } + // Update the VM user's knowledge of targets and blocks on the workspace. this.emitTargetsUpdate(); this.emitWorkspaceUpdate(); diff --git a/test/fixtures/events.json b/test/fixtures/events.json index a5a126f07..dbddd2777 100644 --- a/test/fixtures/events.json +++ b/test/fixtures/events.json @@ -90,5 +90,17 @@ "type": "comment_create", "commentId": "a comment", "xy": {"x": 10, "y": 20} + }, + "mockVariableBlock": { + "name": "block", + "xml": { + "outerHTML": "a mock variable" + } + }, + "mockListBlock": { + "name": "block", + "xml": { + "outerHTML": "a mock list" + } } } diff --git a/test/unit/engine_blocks.js b/test/unit/engine_blocks.js index 08b0b414e..5659b6878 100644 --- a/test/unit/engine_blocks.js +++ b/test/unit/engine_blocks.js @@ -1,5 +1,8 @@ const test = require('tap').test; const Blocks = require('../../src/engine/blocks'); +const Variable = require('../../src/engine/variable'); +const adapter = require('../../src/engine/adapter'); +const events = require('../fixtures/events.json'); test('spec', t => { const b = new Blocks(); @@ -776,3 +779,32 @@ test('updateTargetSpecificBlocks changes sprite clicked hat to stage clicked for t.end(); }); + +test('getAllVariableAndListReferences returns an empty map references when variable blocks do not exist', t => { + const b = new Blocks(); + t.equal(Object.keys(b.getAllVariableAndListReferences()).length, 0); + t.end(); +}); + +test('getAllVariableAndListReferences returns references when variable blocks exist', t => { + const b = new Blocks(); + + let varListRefs = b.getAllVariableAndListReferences(); + t.equal(Object.keys(varListRefs).length, 0); + + b.createBlock(adapter(events.mockVariableBlock)[0]); + b.createBlock(adapter(events.mockListBlock)[0]); + + varListRefs = b.getAllVariableAndListReferences(); + t.equal(Object.keys(varListRefs).length, 2); + t.equal(Array.isArray(varListRefs['mock var id']), true); + t.equal(varListRefs['mock var id'].length, 1); + t.equal(varListRefs['mock var id'][0].type, Variable.SCALAR_TYPE); + t.equal(varListRefs['mock var id'][0].referencingField.value, 'a mock variable'); + t.equal(Array.isArray(varListRefs['mock list id']), true); + t.equal(varListRefs['mock list id'].length, 1); + t.equal(varListRefs['mock list id'][0].type, Variable.LIST_TYPE); + t.equal(varListRefs['mock list id'][0].referencingField.value, 'a mock list'); + + t.end(); +}); diff --git a/test/unit/engine_target.js b/test/unit/engine_target.js index a92dd5e61..098908e1f 100644 --- a/test/unit/engine_target.js +++ b/test/unit/engine_target.js @@ -1,6 +1,9 @@ const test = require('tap').test; const Target = require('../../src/engine/target'); const Variable = require('../../src/engine/variable'); +const adapter = require('../../src/engine/adapter'); +const Runtime = require('../../src/engine/runtime'); +const events = require('../fixtures/events.json'); test('spec', t => { const target = new Target(); @@ -145,7 +148,7 @@ test('deleteVariable2', t => { t.end(); }); -test('lookupOrCreateList creates a list if var with given id does not exist', t => { +test('lookupOrCreateList creates a list if var with given id or var with given name does not exist', t => { const target = new Target(); const variables = target.variables; @@ -174,6 +177,22 @@ test('lookupOrCreateList returns list if one with given id exists', t => { t.end(); }); +test('lookupOrCreateList succeeds in finding list if id is incorrect but name matches', t => { + const target = new Target(); + const variables = target.variables; + + t.equal(Object.keys(variables).length, 0); + target.createVariable('foo', 'bar', Variable.LIST_TYPE); + t.equal(Object.keys(variables).length, 1); + + const listVar = target.lookupOrCreateList('not foo', 'bar'); + t.equal(Object.keys(variables).length, 1); + t.equal(listVar.id, 'foo'); + t.equal(listVar.name, 'bar'); + + t.end(); +}); + test('lookupBroadcastMsg returns the var with given id if exists', t => { const target = new Target(); const variables = target.variables; @@ -263,3 +282,147 @@ test('creating a comment with a blockId also updates the comment property on the t.end(); }); + +test('fixUpVariableReferences fixes sprite global var conflicting with project global var', t => { + const runtime = new Runtime(); + + const stage = new Target(runtime); + stage.isStage = true; + + const target = new Target(runtime); + target.isStage = false; + + runtime.targets = [stage, target]; + + // Create a global variable + stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE); + + target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); + + t.equal(Object.keys(target.variables).length, 0); + t.equal(Object.keys(stage.variables).length, 1); + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + + target.fixUpVariableReferences(); + + t.equal(Object.keys(target.variables).length, 0); + t.equal(Object.keys(stage.variables).length, 1); + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'pre-existing global var id'); + + t.end(); +}); + +test('fixUpVariableReferences fixes sprite local var conflicting with project global var', t => { + const runtime = new Runtime(); + + const stage = new Target(runtime); + stage.isStage = true; + + const target = new Target(runtime); + target.isStage = false; + target.getName = () => 'Target'; + + runtime.targets = [stage, target]; + + // Create a global variable + stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE); + target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); + + target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); + + t.equal(Object.keys(target.variables).length, 1); + t.equal(Object.keys(stage.variables).length, 1); + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + t.equal(target.variables['mock var id'].name, 'a mock variable'); + + target.fixUpVariableReferences(); + + t.equal(Object.keys(target.variables).length, 1); + t.equal(Object.keys(stage.variables).length, 1); + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + t.equal(target.variables['mock var id'].name, 'Target: a mock variable'); + + t.end(); +}); + +test('fixUpVariableReferences fixes conflicting sprite local var without blocks referencing var', t => { + const runtime = new Runtime(); + + const stage = new Target(runtime); + stage.isStage = true; + + const target = new Target(runtime); + target.isStage = false; + target.getName = () => 'Target'; + + runtime.targets = [stage, target]; + + // Create a global variable + stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE); + target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); + + + t.equal(Object.keys(target.variables).length, 1); + t.equal(Object.keys(stage.variables).length, 1); + t.equal(target.variables['mock var id'].name, 'a mock variable'); + + target.fixUpVariableReferences(); + + t.equal(Object.keys(target.variables).length, 1); + t.equal(Object.keys(stage.variables).length, 1); + t.equal(target.variables['mock var id'].name, 'Target: a mock variable'); + + t.end(); +}); + +test('fixUpVariableReferences does not change variable name if there is no variable conflict', t => { + const runtime = new Runtime(); + + const stage = new Target(runtime); + stage.isStage = true; + + const target = new Target(runtime); + target.isStage = false; + target.getName = () => 'Target'; + + runtime.targets = [stage, target]; + + // Create a global variable + stage.createVariable('pre-existing global var id', 'a variable', Variable.SCALAR_TYPE); + stage.createVariable('pre-existing global list id', 'a mock variable', Variable.LIST_TYPE); + target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); + + target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); + + t.equal(Object.keys(target.variables).length, 1); + t.equal(Object.keys(stage.variables).length, 2); + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + t.equal(target.variables['mock var id'].name, 'a mock variable'); + + target.fixUpVariableReferences(); + + t.equal(Object.keys(target.variables).length, 1); + t.equal(Object.keys(stage.variables).length, 2); + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + t.equal(target.variables['mock var id'].name, 'a mock variable'); + + t.end(); +});