diff --git a/test/fixtures/monitors.sb3 b/test/fixtures/monitors.sb3 new file mode 100644 index 000000000..1343243dd Binary files /dev/null and b/test/fixtures/monitors.sb3 differ diff --git a/test/integration/monitors.js b/test/integration/monitors_sb2.js similarity index 100% rename from test/integration/monitors.js rename to test/integration/monitors_sb2.js diff --git a/test/integration/monitors_sb3.js b/test/integration/monitors_sb3.js new file mode 100644 index 000000000..60a280e98 --- /dev/null +++ b/test/integration/monitors_sb3.js @@ -0,0 +1,264 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const Variable = require('../../src/engine/variable'); + +const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb3'); +const project = readFileToBuffer(projectUri); + +test('importing sb2 project with monitors', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + // All monitors should create threads that finish during the step and + // are revoved from runtime.threads. + t.equal(threads.length, 0); + t.equal(vm.runtime._lastStepDoneThreads.length, 17); + // There should be one additional hidden monitor that is in the monitorState but + // does not start a thread. + t.equal(vm.runtime._monitorState.size, 18); + + const stage = vm.runtime.targets[0]; + const shirtSprite = vm.runtime.targets[1]; + const heartSprite = vm.runtime.targets[2]; + + // Global variable named "my variable" exists + let variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'my variable')[0]; + let monitorRecord = vm.runtime._monitorState.get(variableId); + let monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'default'); + // The following few properties are imported for all monitors, just check once. + // sliderMin and sliderMax are currently not implemented, + // but should still get default values serialized and deserialized correctly + t.equal(monitorRecord.sliderMin, 0); + t.equal(monitorRecord.sliderMax, 100); + t.equal(monitorRecord.x, 10); + t.equal(monitorRecord.y, 62); + // Height and width are only used for list monitors and should default to 0 + // for all other monitors + t.equal(monitorRecord.width, 0); + t.equal(monitorRecord.height, 0); + t.equal(monitorRecord.visible, true); + t.type(monitorRecord.params, 'object'); + // The variable name should be stored in the monitor params + t.equal(monitorRecord.params.VARIABLE, 'my variable'); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.VARIABLE.value, 'my variable'); + t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE'); + t.equal(monitorBlock.fields.VARIABLE.id, variableId); + t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE); + + // There is a global variable named 'secret_slide' which has a hidden monitor + variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'secret_slide')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'slider'); + t.equal(monitorRecord.visible, false); + t.equal(monitorRecord.sliderMin, 0); + t.equal(monitorRecord.sliderMax, 100); + t.type(monitorRecord.params, 'object'); + t.equal(monitorRecord.params.VARIABLE, 'secret_slide'); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.VARIABLE.value, 'secret_slide'); + t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE'); + t.equal(monitorBlock.fields.VARIABLE.id, variableId); + t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE); + + + // Shirt sprite has a local list named "fashion" + variableId = Object.keys(shirtSprite.variables).filter(k => shirtSprite.variables[k].name === 'fashion')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_listcontents'); + t.equal(monitorRecord.mode, 'list'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.height, 122); + t.equal(monitorRecord.width, 104); + t.type(monitorRecord.params, 'object'); + t.equal(monitorRecord.params.LIST, 'fashion'); // The list name should be stored in the monitor params + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.LIST.value, 'fashion'); + t.equal(monitorBlock.fields.LIST.name, 'LIST'); + t.equal(monitorBlock.fields.LIST.id, variableId); + t.equal(monitorBlock.fields.LIST.variableType, Variable.LIST_TYPE); + + // Shirt sprite has a local variable named "tee" + variableId = Object.keys(shirtSprite.variables).filter(k => shirtSprite.variables[k].name === 'tee')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'slider'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.sliderMin, 0); + t.equal(monitorRecord.sliderMax, 100); + t.type(monitorRecord.params, 'object'); + t.equal(monitorRecord.params.VARIABLE, 'tee'); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.VARIABLE.value, 'tee'); + t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE'); + t.equal(monitorBlock.fields.VARIABLE.id, variableId); + t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE); + + // Heart sprite has a local list named "hearty" + variableId = Object.keys(heartSprite.variables).filter(k => heartSprite.variables[k].name === 'hearty')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.type(monitorRecord.params, 'object'); + t.equal(monitorRecord.params.VARIABLE, 'hearty'); // The variable name should be stored in the monitor params + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.VARIABLE.value, 'hearty'); + t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE'); + t.equal(monitorBlock.fields.VARIABLE.id, variableId); + t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE); + + // Backdrop name monitor is visible, not sprite specific + // should get imported with id that references the name parameter + // via '_name' at the end since the 3.0 block has a dropdown. + let monitorId = 'backdropnumbername_name'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'looks_backdropnumbername'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.NUMBER_NAME.value, 'name'); + + // Backdrop name monitor is visible, not sprite specific + // should get imported with id that references the name parameter + // via '_number' at the end since the 3.0 block has a dropdown. + monitorId = 'backdropnumbername_number'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'looks_backdropnumbername'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.NUMBER_NAME.value, 'number'); + + // x position monitor is in large mode, specific to shirt sprite + monitorId = `${shirtSprite.id}_xposition`; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'motion_xposition'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Shirt-T'); + t.equal(monitorRecord.targetId, shirtSprite.id); + + // y position monitor is in large mode, specific to shirt sprite + monitorId = `${shirtSprite.id}_yposition`; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'motion_yposition'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Shirt-T'); + t.equal(monitorRecord.targetId, shirtSprite.id); + + // direction monitor is in large mode, specific to shirt sprite + monitorId = `${shirtSprite.id}_direction`; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'motion_direction'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Shirt-T'); + t.equal(monitorRecord.targetId, shirtSprite.id); + + monitorId = `${shirtSprite.id}_size`; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'looks_size'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Shirt-T'); + t.equal(monitorRecord.targetId, shirtSprite.id); + + // The monitor IDs for the sensing_current block should be unique + // to the parameter that is selected on the block being monitored. + // The paramater portion of the id should be lowercase even + // though the field value on the block is uppercase. + monitorId = 'current_date'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'DATE'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + monitorId = 'current_year'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'YEAR'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + monitorId = 'current_month'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'MONTH'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + // Extension Monitors + monitorId = 'music_getTempo'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'music_getTempo'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + t.equal(vm.extensionManager.isExtensionLoaded('music'), true); + + monitorId = 'ev3_getDistance'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'ev3_getDistance'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + t.equal(vm.extensionManager.isExtensionLoaded('ev3'), true); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +});