const tap = require('tap'); const VirtualMachine = require('../../src/virtual-machine'); const Sprite = require('../../src/sprites/sprite'); const Variable = require('../../src/engine/variable'); const adapter = require('../../src/engine/adapter'); const events = require('../fixtures/events.json'); const Renderer = require('../fixtures/fake-renderer'); const Runtime = require('../../src/engine/runtime'); const RenderedTarget = require('../../src/sprites/rendered-target'); tap.tearDown(() => process.nextTick(process.exit)); const test = tap.test; test('deleteSound returns function after deleting or null if nothing was deleted', t => { const vm = new VirtualMachine(); const rt = new Runtime(); const sprite = new Sprite(null, rt); sprite.sounds = [{id: 1}, {id: 2}, {id: 3}]; const target = new RenderedTarget(sprite, rt); vm.editingTarget = target; const addFun = vm.deleteSound(1); t.equal(sprite.sounds.length, 2); t.equal(sprite.sounds[0].id, 1); t.equal(sprite.sounds[1].id, 3); t.type(addFun, 'function'); const noAddFun = vm.deleteSound(2); t.equal(sprite.sounds.length, 2); t.equal(sprite.sounds[0].id, 1); t.equal(sprite.sounds[1].id, 3); t.equal(noAddFun, null); t.end(); }); test('deleteCostume returns function after deleting or null if nothing was deleted', t => { const vm = new VirtualMachine(); const rt = new Runtime(); const sprite = new Sprite(null, rt); sprite.costumes = [{id: 1}, {id: 2}, {id: 3}]; sprite.currentCostume = 0; const target = new RenderedTarget(sprite, rt); vm.editingTarget = target; const addFun = vm.deleteCostume(1); t.equal(sprite.costumes.length, 2); t.equal(sprite.costumes[0].id, 1); t.equal(sprite.costumes[1].id, 3); t.type(addFun, 'function'); const noAddFun = vm.deleteCostume(2); t.equal(sprite.costumes.length, 2); t.equal(sprite.costumes[0].id, 1); t.equal(sprite.costumes[1].id, 3); t.equal(noAddFun, null); t.end(); }); test('addSprite throws on invalid string', t => { const vm = new VirtualMachine(); vm.addSprite('this is not a sprite') .catch(e => { t.equal(e.startsWith('Sprite Upload Error:'), true); t.end(); }); }); test('renameSprite throws when there is no sprite with that id', t => { const 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', t => { const vm = new VirtualMachine(); const 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', t => { const vm = new VirtualMachine(); const 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', t => { const vm = new VirtualMachine(); const 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', t => { const vm = new VirtualMachine(); const 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', t => { const vm = new VirtualMachine(); const 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', t => { const vm = new VirtualMachine(); vm.emitTargetsUpdate = () => {}; const spr1 = new Sprite(null, vm.runtime); const target1 = spr1.createClone(); const spr2 = new Sprite(null, vm.runtime); const target2 = spr2.createClone(); vm.runtime.targets = [target1, target2]; vm.renameSprite(target1.id, 'foo'); t.equal(vm.runtime.targets[0].sprite.name, 'foo'); vm.renameSprite(target2.id, 'foo'); t.equal(vm.runtime.targets[1].sprite.name, 'foo2'); t.end(); }); test('renameSprite does not increment when renaming to the same name', t => { const vm = new VirtualMachine(); vm.emitTargetsUpdate = () => {}; const spr = new Sprite(null, vm.runtime); spr.name = 'foo'; const target = spr.createClone(); vm.runtime.targets = [target]; t.equal(vm.runtime.targets[0].sprite.name, 'foo'); vm.renameSprite(target.id, 'foo'); t.equal(vm.runtime.targets[0].sprite.name, 'foo'); t.end(); }); test('deleteSprite throws when used on a non-sprite target', t => { const vm = new VirtualMachine(); vm.runtime.targets = [{ id: 'id', isSprite: () => false }]; t.throws( (() => vm.deleteSprite('id')), new Error('Cannot delete non-sprite targets.') ); t.end(); }); test('deleteSprite throws when there is no sprite for the given target', t => { const vm = new VirtualMachine(); vm.runtime.targets = [{ id: 'id', isSprite: () => true, sprite: null }]; t.throws( (() => vm.deleteSprite('id')), new Error('No sprite associated with this target.') ); t.end(); }); test('deleteSprite throws when there is no target with given id', t => { const vm = new VirtualMachine(); vm.runtime.targets = [{ id: 'id', isSprite: () => true, sprite: { name: 'this name' } }]; t.throws( (() => vm.deleteSprite('id1')), new Error('No target with the provided id.') ); t.end(); }); test('deleteSprite deletes a sprite when given id is associated with a known sprite', t => { const vm = new VirtualMachine(); const spr = new Sprite(null, vm.runtime); const currTarget = spr.createClone(); vm.runtime.targets = [currTarget]; t.equal(currTarget.sprite.clones.length, 1); vm.deleteSprite(currTarget.id); t.equal(currTarget.sprite.clones.length, 0); t.end(); }); // eslint-disable-next-line max-len test('deleteSprite sets editing target as null when given sprite is current editing target, and the only target in the runtime', t => { const vm = new VirtualMachine(); const spr = new Sprite(null, vm.runtime); const currTarget = spr.createClone(); vm.editingTarget = currTarget; vm.runtime.targets = [currTarget]; vm.deleteSprite(currTarget.id); t.equal(vm.runtime.targets.length, 0); t.equal(vm.editingTarget, null); t.end(); }); // eslint-disable-next-line max-len test('deleteSprite updates editingTarget when sprite being deleted is current editing target, and there is another target in the runtime', t => { const vm = new VirtualMachine(); const spr1 = new Sprite(null, vm.runtime); const spr2 = new Sprite(null, vm.runtime); const currTarget = spr1.createClone(); const otherTarget = spr2.createClone(); vm.emitWorkspaceUpdate = () => null; vm.runtime.targets = [currTarget, otherTarget]; vm.editingTarget = currTarget; t.equal(vm.runtime.targets.length, 2); vm.deleteSprite(currTarget.id); t.equal(vm.runtime.targets.length, 1); t.equal(vm.editingTarget.id, otherTarget.id); // now let's try them in the other order in the runtime.targets list // can't reuse deleted targets const currTarget2 = spr1.createClone(); const otherTarget2 = spr2.createClone(); vm.runtime.targets = [otherTarget2, currTarget2]; vm.editingTarget = currTarget2; t.equal(vm.runtime.targets.length, 2); vm.deleteSprite(currTarget2.id); t.equal(vm.editingTarget.id, otherTarget2.id); t.equal(vm.runtime.targets.length, 1); t.end(); }); test('duplicateSprite throws when there is no target with given id', t => { const vm = new VirtualMachine(); vm.runtime.targets = [{ id: 'id', isSprite: () => true, sprite: { name: 'this name' } }]; t.throws( (() => vm.duplicateSprite('id1')), new Error('No target with the provided id') ); t.end(); }); test('duplicateSprite throws when used on a non-sprite target', t => { const vm = new VirtualMachine(); vm.runtime.targets = [{ id: 'id', isSprite: () => false }]; t.throws( (() => vm.duplicateSprite('id')), new Error('Cannot duplicate non-sprite targets.') ); t.end(); }); test('duplicateSprite throws when there is no sprite for the given target', t => { const vm = new VirtualMachine(); vm.runtime.targets = [{ id: 'id', isSprite: () => true, sprite: null }]; t.throws( (() => vm.duplicateSprite('id')), new Error('No sprite associated with this target.') ); t.end(); }); test('duplicateSprite duplicates a sprite when given id is associated with known sprite', t => { const vm = new VirtualMachine(); const spr = new Sprite(null, vm.runtime); const currTarget = spr.createClone(); vm.editingTarget = currTarget; vm.emitWorkspaceUpdate = () => null; vm.runtime.targets = [currTarget]; t.equal(vm.runtime.targets.length, 1); vm.duplicateSprite(currTarget.id).then(() => { t.equal(vm.runtime.targets.length, 2); t.end(); }); }); test('duplicateSprite assigns duplicated sprite a fresh name', t => { const vm = new VirtualMachine(); const spr = new Sprite(null, vm.runtime); spr.name = 'sprite1'; const currTarget = spr.createClone(); vm.editingTarget = currTarget; vm.emitWorkspaceUpdate = () => null; vm.runtime.targets = [currTarget]; t.equal(vm.runtime.targets.length, 1); vm.duplicateSprite(currTarget.id).then(() => { t.equal(vm.runtime.targets.length, 2); t.equal(vm.runtime.targets[0].sprite.name, 'sprite1'); t.equal(vm.runtime.targets[1].sprite.name, 'sprite2'); t.end(); }); }); test('reorderCostume', t => { const vm = new VirtualMachine(); vm.emitTargetsUpdate = () => {}; const spr = new Sprite(null, vm.runtime); spr.name = 'foo'; const target = spr.createClone(); // Stub out reorder on target, tested in rendered-target tests. // Just want to know if it is called with the right params. let costumeIndex = null; let newIndex = null; target.reorderCostume = (_costumeIndex, _newIndex) => { costumeIndex = _costumeIndex; newIndex = _newIndex; return true; // Do not need all the logic about if a reorder occurred. }; vm.runtime.targets = [target]; t.equal(vm.reorderCostume('not-a-target', 0, 3), false); t.equal(costumeIndex, null); t.equal(newIndex, null); t.equal(vm.reorderCostume(target.id, 0, 3), true); t.equal(costumeIndex, 0); t.equal(newIndex, 3); t.end(); }); test('reorderSound', t => { const vm = new VirtualMachine(); vm.emitTargetsUpdate = () => {}; const spr = new Sprite(null, vm.runtime); spr.name = 'foo'; const target = spr.createClone(); // Stub out reorder on target, tested in rendered-target tests. // Just want to know if it is called with the right params. let soundIndex = null; let newIndex = null; target.reorderSound = (_soundIndex, _newIndex) => { soundIndex = _soundIndex; newIndex = _newIndex; return true; // Do not need all the logic about if a reorder occurred. }; vm.runtime.targets = [target]; t.equal(vm.reorderSound('not-a-target', 0, 3), false); t.equal(soundIndex, null); // Make sure reorder function was not called somehow. t.equal(newIndex, null); t.equal(vm.reorderSound(target.id, 0, 3), true); t.equal(soundIndex, 0); // Make sure reorder function was called correctly. t.equal(newIndex, 3); t.end(); }); test('shareCostumeToTarget', t => { const vm = new VirtualMachine(); const spr1 = new Sprite(null, vm.runtime); spr1.name = 'foo'; const target1 = spr1.createClone(); const costume1 = {name: 'costume1'}; target1.addCostume(costume1); const spr2 = new Sprite(null, vm.runtime); spr2.name = 'foo'; const target2 = spr2.createClone(); const costume2 = {name: 'another costume'}; target2.addCostume(costume2); vm.runtime.targets = [target1, target2]; vm.editingTarget = vm.runtime.targets[0]; vm.emitWorkspaceUpdate = () => null; vm.shareCostumeToTarget(0, target2.id).then(() => { t.equal(target2.currentCostume, 1); t.equal(target2.getCostumes()[1].name, 'costume1'); t.end(); }); }); test('shareSoundToTarget', t => { const vm = new VirtualMachine(); const spr1 = new Sprite(null, vm.runtime); spr1.name = 'foo'; const target1 = spr1.createClone(); const sound1 = {name: 'sound1'}; target1.addSound(sound1); const spr2 = new Sprite(null, vm.runtime); spr2.name = 'foo'; const target2 = spr2.createClone(); const sound2 = {name: 'another sound'}; target2.addSound(sound2); vm.runtime.targets = [target1, target2]; vm.editingTarget = vm.runtime.targets[0]; vm.emitWorkspaceUpdate = () => null; vm.shareSoundToTarget(0, target2.id).then(() => { t.equal(target2.getSounds()[1].name, 'sound1'); t.end(); }); }); test('reorderTarget', t => { const vm = new VirtualMachine(); vm.emitTargetsUpdate = () => {}; vm.runtime.targets = ['a', 'b', 'c', 'd']; t.equal(vm.reorderTarget(2, 2), false); t.deepEqual(vm.runtime.targets, ['a', 'b', 'c', 'd']); // Make sure clamping works t.equal(vm.reorderTarget(-100, -5), false); t.deepEqual(vm.runtime.targets, ['a', 'b', 'c', 'd']); // Reorder upwards t.equal(vm.reorderTarget(0, 2), true); t.deepEqual(vm.runtime.targets, ['b', 'c', 'a', 'd']); // Reorder downwards t.equal(vm.reorderTarget(3, 1), true); t.deepEqual(vm.runtime.targets, ['b', 'd', 'c', 'a']); t.end(); }); test('emitWorkspaceUpdate', t => { const vm = new VirtualMachine(); const blocksToXML = comments => { let blockString = 'blocks\n'; if (comments) { for (const commentId in comments) { const comment = comments[commentId]; blockString += `A Block Comment: ${comment.toXML()}`; } } return blockString; }; vm.runtime.targets = [ { isStage: true, variables: { global: { toXML: () => 'global' } }, blocks: { toXML: blocksToXML }, comments: { aStageComment: { toXML: () => 'aStageComment', blockId: null } } }, { variables: { unused: { toXML: () => 'unused' } }, blocks: { toXML: blocksToXML }, comments: { someBlockComment: { toXML: () => 'someBlockComment', blockId: 'someBlockId' } } }, { variables: { local: { toXML: () => 'local' } }, blocks: { toXML: blocksToXML }, comments: { someOtherComment: { toXML: () => 'someOtherComment', blockId: null }, aBlockComment: { toXML: () => 'aBlockComment', blockId: 'a block' } } } ]; 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.equal(xml.indexOf('aStageComment'), -1); t.equal(xml.indexOf('someBlockComment'), -1); t.notEqual(xml.indexOf('someOtherComment'), -1); t.notEqual(xml.indexOf('A Block Comment: aBlockComment'), -1); t.end(); }); test('drag IO redirect', t => { const vm = new VirtualMachine(); const sprite1Info = []; const sprite2Info = []; vm.runtime.targets = [ { id: 'sprite1', postSpriteInfo: data => sprite1Info.push(data) }, { id: 'sprite2', postSpriteInfo: data => sprite2Info.push(data), startDrag: () => {}, stopDrag: () => {} } ]; vm.editingTarget = vm.runtime.targets[0]; // Stub emitWorkspace/TargetsUpdate, it needs data we don't care about here vm.emitWorkspaceUpdate = () => null; vm.emitTargetsUpdate = () => null; // postSpriteInfo should go to the editing target by default`` vm.postSpriteInfo('sprite1 info'); t.equal(sprite1Info[0], 'sprite1 info'); // postSprite info goes to the drag target if it exists vm.startDrag('sprite2'); vm.postSpriteInfo('sprite2 info'); t.equal(sprite2Info[0], 'sprite2 info'); // stop drag should set the editing target vm.stopDrag('sprite2'); t.equal(vm.editingTarget.id, 'sprite2'); // Then postSpriteInfo should continue posting to the new editing target vm.postSpriteInfo('sprite2 info 2'); t.equal(sprite2Info[1], 'sprite2 info 2'); t.end(); }); test('select original after dragging clone', t => { const vm = new VirtualMachine(); let newEditingTargetId = null; vm.setEditingTarget = id => { newEditingTargetId = id; }; vm.runtime.targets = [ { id: 'sprite1_clone', sprite: {clones: [{id: 'sprite1_original'}]}, stopDrag: () => {} }, { id: 'sprite2', stopDrag: () => {} } ]; // Stop drag on a bare target selects that target vm.stopDrag('sprite2'); t.equal(newEditingTargetId, 'sprite2'); // Stop drag on target with parent sprite selects the 0th clone of that sprite vm.stopDrag('sprite1_clone'); t.equal(newEditingTargetId, 'sprite1_original'); t.end(); }); test('setVariableValue', t => { const vm = new VirtualMachine(); const spr = new Sprite(null, vm.runtime); const target = spr.createClone(); target.createVariable('a-variable', 'a-name', Variable.SCALAR_TYPE); vm.runtime.targets = [target]; // Returns false if there is no variable to set t.equal(vm.setVariableValue(target.id, 'not-a-variable', 100), false); // Returns false if there is no target with that id t.equal(vm.setVariableValue('not-a-target', 'a-variable', 100), false); // Returns true and updates the value if variable is present t.equal(vm.setVariableValue(target.id, 'a-variable', 100), true); t.equal(target.lookupVariableById('a-variable').value, 100); t.end(); }); test('setVariableValue requests update for cloud variable', t => { const vm = new VirtualMachine(); const spr = new Sprite(null, vm.runtime); const target = spr.createClone(); target.isStage = true; target.createVariable('a-variable', 'a-name', Variable.SCALAR_TYPE, true /* isCloud */); vm.runtime.targets = [target]; // Mock cloud io device requestUpdateVariable function let requestUpdateVarWasCalled = false; let varName; let varValue; vm.runtime.ioDevices.cloud.requestUpdateVariable = (name, value) => { requestUpdateVarWasCalled = true; varName = name; varValue = value; }; vm.setVariableValue(target.id, 'not-a-variable', 100); t.equal(requestUpdateVarWasCalled, false); vm.setVariableValue(target.id, 'a-variable', 100); t.equal(requestUpdateVarWasCalled, true); t.equal(varName, 'a-name'); t.equal(varValue, 100); t.end(); }); test('getVariableValue', t => { const vm = new VirtualMachine(); const spr = new Sprite(null, vm.runtime); const target = spr.createClone(); target.createVariable('a-variable', 'a-name', Variable.SCALAR_TYPE); vm.runtime.targets = [target]; // Returns null if there is no variable with that id t.equal(vm.getVariableValue(target.id, 'not-a-variable'), null); // Returns null if there is no target with that id t.equal(vm.getVariableValue('not-a-target', 'a-variable'), null); // Returns true and updates the value if variable is present t.equal(vm.getVariableValue(target.id, 'a-variable'), 0); vm.setVariableValue(target.id, 'a-variable', 'string'); t.equal(vm.getVariableValue(target.id, 'a-variable'), 'string'); t.end(); }); // Block Listener tests for comment test('comment_create event updates comment with null position', t => { const vm = new VirtualMachine(); const spr = new Sprite(null, vm.runtime); const target = spr.createClone(); target.createComment('a comment', null, 'some text', null, null, 200, 300, false); vm.runtime.targets = [target]; vm.editingTarget = target; vm.runtime.setEditingTarget(target); const comment = target.comments['a comment']; t.equal(comment.x, null); t.equal(comment.y, null); vm.blockListener(events.createcommentUpdatePosition); t.equal(comment.x, 10); t.equal(comment.y, 20); t.end(); }); test('shareBlocksToTarget shares global variables without any name changes', t => { const vm = new VirtualMachine(); const runtime = vm.runtime; const spr1 = new Sprite(null, runtime); const stage = spr1.createClone(); stage.isStage = true; const spr2 = new Sprite(null, runtime); const target = spr2.createClone(); runtime.targets = [stage, target]; vm.editingTarget = target; vm.runtime.setEditingTarget(target); stage.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); t.equal(Object.keys(target.variables).length, 0); t.equal(Object.keys(stage.variables).length, 1); t.equal(stage.variables['mock var id'].name, 'a mock variable'); vm.setVariableValue(stage.id, 'mock var id', 10); t.equal(vm.getVariableValue(stage.id, 'mock var id'), 10); target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); // Verify the block exists on the target, and that it references the global variable 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'); // Verify that the block does not exist on the stage t.type(stage.blocks.getBlock('a block'), 'undefined'); // Share the block to the stage vm.shareBlocksToTarget([target.blocks.getBlock('a block')], stage.id, target.id).then(() => { // Verify that the block now exists on the target as well as the stage 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'); const newBlockId = Object.keys(stage.blocks._blocks)[0]; t.type(stage.blocks.getBlock(newBlockId), 'object'); t.type(stage.blocks.getBlock(newBlockId).fields, 'object'); t.type(stage.blocks.getBlock(newBlockId).fields.VARIABLE, 'object'); t.equal(stage.blocks.getBlock(newBlockId).fields.VARIABLE.id, 'mock var id'); // Verify the shared block id is different t.notEqual(newBlockId, 'a block'); // Verify that the variables haven't changed, the variable still exists on the // stage, it should still have the same name and value, and there should be // no variables on the target. t.equal(Object.keys(target.variables).length, 0); t.equal(Object.keys(stage.variables).length, 1); t.equal(stage.variables['mock var id'].name, 'a mock variable'); t.equal(vm.getVariableValue(stage.id, 'mock var id'), 10); t.end(); }); }); test('shareBlocksToTarget shares a local variable to the stage, creating a global variable with a new name', t => { const vm = new VirtualMachine(); const runtime = vm.runtime; const spr1 = new Sprite(null, runtime); const stage = spr1.createClone(); stage.isStage = true; const spr2 = new Sprite(null, runtime); const target = spr2.createClone(); runtime.targets = [stage, target]; vm.editingTarget = target; vm.runtime.setEditingTarget(target); target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); t.equal(Object.keys(stage.variables).length, 0); t.equal(Object.keys(target.variables).length, 1); t.equal(target.variables['mock var id'].name, 'a mock variable'); vm.setVariableValue(target.id, 'mock var id', 10); t.equal(vm.getVariableValue(target.id, 'mock var id'), 10); target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); // Verify the block exists on the target, and that it references the global variable 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'); // Verify that the block does not exist on the stage t.type(stage.blocks.getBlock('a block'), 'undefined'); // Share the block to the stage vm.shareBlocksToTarget([target.blocks.getBlock('a block')], stage.id, target.id).then(() => { // Verify that the block still exists on the target and remains unchanged 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'); const newBlockId = Object.keys(stage.blocks._blocks)[0]; t.type(stage.blocks.getBlock(newBlockId), 'object'); t.type(stage.blocks.getBlock(newBlockId).fields, 'object'); t.type(stage.blocks.getBlock(newBlockId).fields.VARIABLE, 'object'); t.equal(stage.blocks.getBlock(newBlockId).fields.VARIABLE.id, 'StageVarFromLocal_mock var id'); // Verify that a new global variable was created, the old one still exists on // the target and still has the same name and value, and the new one has // a new name and value 0. t.equal(Object.keys(target.variables).length, 1); t.equal(target.variables['mock var id'].name, 'a mock variable'); t.equal(vm.getVariableValue(target.id, 'mock var id'), 10); // Verify that a new variable was created on the stage, with a new name and new id t.equal(Object.keys(stage.variables).length, 1); t.type(stage.variables['mock var id'], 'undefined'); const newGlobalVar = Object.values(stage.variables)[0]; t.equal(newGlobalVar.name, 'Stage: a mock variable'); const newId = newGlobalVar.id; t.notEqual(newId, 'mock var id'); t.equals(vm.getVariableValue(stage.id, newId), 0); t.end(); }); }); test('shareBlocksToTarget chooses a fresh name for a new global variable checking for conflicts on all sprites', t => { const vm = new VirtualMachine(); const runtime = vm.runtime; const spr1 = new Sprite(null, runtime); const stage = spr1.createClone(); stage.isStage = true; const spr2 = new Sprite(null, runtime); const target = spr2.createClone(); const spr3 = new Sprite(null, runtime); const otherTarget = spr3.createClone(); runtime.targets = [stage, target, otherTarget]; vm.editingTarget = target; vm.runtime.setEditingTarget(target); target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); t.equal(Object.keys(stage.variables).length, 0); t.equal(Object.keys(target.variables).length, 1); t.equal(target.variables['mock var id'].name, 'a mock variable'); vm.setVariableValue(target.id, 'mock var id', 10); t.equal(vm.getVariableValue(target.id, 'mock var id'), 10); target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); // Verify the block exists on the target, and that it references the global variable 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'); // Verify that the block does not exist on the stage t.type(stage.blocks.getBlock('a block'), 'undefined'); // Create a variable that conflicts with what will be the new name for the // new global variable to ensure a fresh name is chosen otherTarget.createVariable('a different var', 'Stage: a mock variable', Variable.SCALAR_TYPE); // Share the block to the stage vm.shareBlocksToTarget([target.blocks.getBlock('a block')], stage.id, target.id).then(() => { // Verify that the block still exists on the target and remains unchanged 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'); const newBlockId = Object.keys(stage.blocks._blocks)[0]; t.type(stage.blocks.getBlock(newBlockId), 'object'); t.type(stage.blocks.getBlock(newBlockId).fields, 'object'); t.type(stage.blocks.getBlock(newBlockId).fields.VARIABLE, 'object'); t.equal(stage.blocks.getBlock(newBlockId).fields.VARIABLE.id, 'StageVarFromLocal_mock var id'); // Verify that a new global variable was created, the old one still exists on // the target and still has the same name and value, and the new one has // a new name and value 0. t.equal(Object.keys(target.variables).length, 1); t.equal(target.variables['mock var id'].name, 'a mock variable'); t.equal(vm.getVariableValue(target.id, 'mock var id'), 10); // Verify that a new variable was created on the stage, with a new name and new id t.equal(Object.keys(stage.variables).length, 1); t.type(stage.variables['mock var id'], 'undefined'); const newGlobalVar = Object.values(stage.variables)[0]; t.equal(newGlobalVar.name, 'Stage: a mock variable2'); const newId = newGlobalVar.id; t.notEqual(newId, 'mock var id'); t.equals(vm.getVariableValue(stage.id, newId), 0); t.end(); }); }); test('shareBlocksToTarget loads extensions that have not yet been loaded', t => { const vm = new VirtualMachine(); const runtime = vm.runtime; const spr1 = new Sprite(null, runtime); const stage = spr1.createClone(); runtime.targets = [stage]; const fakeBlocks = [ {opcode: 'loaded_fakeblock'}, {opcode: 'notloaded_fakeblock'} ]; // Stub the extension manager const loadedIds = []; vm.extensionManager = { isExtensionLoaded: id => id === 'loaded', loadExtensionURL: id => new Promise(resolve => { loadedIds.push(id); resolve(); }) }; vm.shareBlocksToTarget(fakeBlocks, stage.id).then(() => { // Verify that only the not-loaded extension gets loaded t.deepEqual(loadedIds, ['notloaded']); t.end(); }); }); test('Setting turbo mode emits events', t => { let turboMode = null; const vm = new VirtualMachine(); vm.addListener('TURBO_MODE_ON', () => { turboMode = true; }); vm.addListener('TURBO_MODE_OFF', () => { turboMode = false; }); vm.setTurboMode(true); t.equal(turboMode, true); vm.setTurboMode(false); t.equal(turboMode, false); t.end(); }); test('Getting the renderer returns the renderer', t => { const renderer = new Renderer(); const vm = new VirtualMachine(); vm.attachRenderer(renderer); t.equal(vm.renderer, renderer); t.end(); }); test('Starting the VM emits an event', t => { let started = false; const vm = new VirtualMachine(); vm.addListener('RUNTIME_STARTED', () => { started = true; }); vm.start(); t.equal(started, true); t.end(); }); test('vm.greenFlag() emits a PROJECT_START event', t => { let greenFlagged = false; const vm = new VirtualMachine(); vm.addListener('PROJECT_START', () => { greenFlagged = true; }); vm.greenFlag(); t.equal(greenFlagged, true); t.end(); }); test('toJSON encodes Infinity/NaN as 0, not null', t => { const vm = new VirtualMachine(); const runtime = vm.runtime; const spr1 = new Sprite(null, runtime); const stage = spr1.createClone(); stage.isStage = true; stage.volume = Infinity; stage.tempo = NaN; stage.createVariable('id1', 'name1', ''); stage.variables.id1.value = Infinity; stage.createVariable('id2', 'name2', ''); stage.variables.id1.value = -Infinity; stage.createVariable('id3', 'name3', ''); stage.variables.id1.value = NaN; runtime.targets = [stage]; const json = JSON.parse(vm.toJSON()); t.equal(json.targets[0].volume, 0); t.equal(json.targets[0].tempo, 0); t.equal(json.targets[0].variables.id1[1], 0); t.equal(json.targets[0].variables.id2[1], 0); t.equal(json.targets[0].variables.id3[1], 0); t.end(); });