mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-11 10:39:56 -05:00
Merge pull request #1256 from kchadha/sprite-save-load
Sprite save load
This commit is contained in:
commit
d187517d85
8 changed files with 490 additions and 32 deletions
src
test
|
@ -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
|
||||
|
|
|
@ -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<string>} 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.
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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<object>} 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<object>} 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<object>} 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 = {
|
||||
|
|
|
@ -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();
|
||||
|
|
12
test/fixtures/events.json
vendored
12
test/fixtures/events.json
vendored
|
@ -90,5 +90,17 @@
|
|||
"type": "comment_create",
|
||||
"commentId": "a comment",
|
||||
"xy": {"x": 10, "y": 20}
|
||||
},
|
||||
"mockVariableBlock": {
|
||||
"name": "block",
|
||||
"xml": {
|
||||
"outerHTML": "<block type='data_variable' id='a block' x='0' y='0'><field name='VARIABLE' id='mock var id' variabletype=''>a mock variable</field></block>"
|
||||
}
|
||||
},
|
||||
"mockListBlock": {
|
||||
"name": "block",
|
||||
"xml": {
|
||||
"outerHTML": "<block type='data_listcontents' id='another block' x='0' y='0'><field name='LIST' id='mock list id' variabletype=''>a mock list</field></block>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue