diff --git a/src/engine/blocks.js b/src/engine/blocks.js index ccc4cb209..e73f4b27f 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -5,6 +5,7 @@ const MonitorRecord = require('./monitor-record'); const Clone = require('../util/clone'); const {Map} = require('immutable'); const BlocksExecuteCache = require('./blocks-execute-cache'); +const log = require('../util/log'); /** * @fileoverview @@ -734,19 +735,21 @@ class Blocks { /** * Encode all of `this._blocks` as an XML string usable * by a Blockly/scratch-blocks workspace. + * @param {object} comments Map of comments referenced by id * @return {string} String of XML representing this object's blocks. */ - toXML () { - return this._scripts.map(script => this.blockToXML(script)).join(); + toXML (comments) { + return this._scripts.map(script => this.blockToXML(script, comments)).join(); } /** * Recursively encode an individual block and its children * into a Blockly/scratch-blocks XML string. * @param {!string} blockId ID of block to encode. + * @param {object} comments Map of comments referenced by id * @return {string} String of XML representing this block and any children. */ - blockToXML (blockId) { + blockToXML (blockId, comments) { const block = this._blocks[blockId]; // Encode properties of this block. const tagName = (block.shadow) ? 'shadow' : 'block'; @@ -756,6 +759,18 @@ class Blocks { type="${block.opcode}" ${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''} >`; + const commentId = block.comment; + if (commentId) { + if (comments) { + if (comments.hasOwnProperty(commentId)) { + xmlString += comments[commentId].toXML(); + } else { + log.warn(`Could not find comment with id: ${commentId} in provided comment descriptions.`); + } + } else { + log.warn(`Cannot serialize comment with id: ${commentId}; no comment descriptions provided.`); + } + } // Add any mutation. Must come before inputs. if (block.mutation) { xmlString += this.mutationToXML(block.mutation); @@ -768,11 +783,11 @@ class Blocks { if (blockInput.block || blockInput.shadow) { xmlString += ``; if (blockInput.block) { - xmlString += this.blockToXML(blockInput.block); + xmlString += this.blockToXML(blockInput.block, comments); } if (blockInput.shadow && blockInput.shadow !== blockInput.block) { // Obscured shadow. - xmlString += this.blockToXML(blockInput.shadow); + xmlString += this.blockToXML(blockInput.shadow, comments); } xmlString += ''; } @@ -798,7 +813,7 @@ class Blocks { } // Add blocks connected to the next connection. if (block.next) { - xmlString += `${this.blockToXML(block.next)}`; + xmlString += `${this.blockToXML(block.next, comments)}`; } xmlString += ``; return xmlString; diff --git a/src/engine/comment.js b/src/engine/comment.js new file mode 100644 index 000000000..6dec2f0f4 --- /dev/null +++ b/src/engine/comment.js @@ -0,0 +1,56 @@ +/** + * @fileoverview + * Object representing a Scratch Comment (block or workspace). + */ + +const uid = require('../util/uid'); +const Cast = require('../util/cast'); + +class Comment { + /** + * @param {string} id Id of the comment. + * @param {string} text Text content of the comment. + * @param {number} x X position of the comment on the workspace. + * @param {number} y Y position of the comment on the workspace. + * @param {number} width The width of the comment when it is full size. + * @param {number} height The height of the comment when it is full size. + * @param {boolean} minimized Whether the comment is minimized. + * @constructor + */ + constructor (id, text, x, y, width, height, minimized) { + this.id = id || uid(); + this.text = text; + this.x = x; + this.y = y; + this.width = Math.max(Cast.toNumber(width), Comment.MIN_WIDTH); + this.height = Math.max(Cast.toNumber(height), Comment.MIN_HEIGHT); + this.minimized = minimized || false; + this.blockId = null; + } + + toXML () { + return `${this.text}`; + } + + // TODO choose min and defaults for width and height + static get MIN_WIDTH () { + return 20; + } + + static get MIN_HEIGHT () { + return 20; + } + + static get DEFAULT_WIDTH () { + return 100; + } + + static get DEFAULT_HEIGHT () { + return 100; + } + +} + +module.exports = Comment; diff --git a/src/engine/target.js b/src/engine/target.js index 9861c455d..223254221 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -43,16 +43,16 @@ class Target extends EventEmitter { this.blocks = blocks; /** * Dictionary of variables and their values for this target. - * Key is the variable name. + * Key is the variable id. * @type {Object.} */ this.variables = {}; /** - * Dictionary of lists and their contents for this target. - * Key is the list name. + * Dictionary of comments for this target. + * Key is the comment id. * @type {Object.} */ - this.lists = {}; + this.comments = {}; /** * Dictionary of custom state for this target. * This can be used to store target-specific custom state for blocks which need it. diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 4ef47ba15..5efd5c9da 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -13,6 +13,7 @@ const log = require('../util/log'); const uid = require('../util/uid'); const StringUtil = require('../util/string-util'); const specMap = require('./sb2_specmap'); +const Comment = require('../engine/comment'); const Variable = require('../engine/variable'); const MonitorRecord = require('../engine/monitor-record'); const StageLayering = require('../engine/stage-layering'); @@ -36,6 +37,12 @@ const CORE_EXTENSIONS = [ 'sound' ]; +// Adjust script coordinates to account for +// larger block size in scratch-blocks. +// @todo: Determine more precisely the right formulas here. +const WORKSPACE_X_SCALE = 1.5; +const WORKSPACE_Y_SCALE = 2.2; + /** * Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n") * into an argument map. This allows us to provide the expected inputs @@ -110,15 +117,26 @@ const flatten = function (blocks) { * @param {Function} addBroadcastMsg function to update broadcast message name map * @param {Function} getVariableId function to retreive a variable's ID based on name * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. - * @return {Array.} Scratch VM-format block list. + * @param {object} comments - Comments from sb2 project that need to be attached to blocks. + * They are indexed in this object by the sb2 flattened block list index indicating + * which block they should attach to. + * @param {int} commentIndex The current index of the top block in this list if it were in a flattened + * list of all blocks for the target + * @return {[Array., int]} Tuple where first item is the Scratch VM-format block list, and + * second item is the updated comment index */ -const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions) { +const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions, comments, commentIndex) { const resultingList = []; let previousBlock = null; // For setting next. for (let i = 0; i < blockList.length; i++) { const block = blockList[i]; // eslint-disable-next-line no-use-before-define - const parsedBlock = parseBlock(block, addBroadcastMsg, getVariableId, extensions); + const parsedBlockAndComments = parseBlock(block, addBroadcastMsg, getVariableId, + extensions, comments, commentIndex); + const parsedBlock = parsedBlockAndComments[0]; + // Update commentIndex + commentIndex = parsedBlockAndComments[1]; + if (typeof parsedBlock === 'undefined') continue; if (previousBlock) { parsedBlock.parent = previousBlock.id; @@ -127,7 +145,7 @@ const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, exte previousBlock = parsedBlock; resultingList.push(parsedBlock); } - return resultingList; + return [resultingList, commentIndex]; }; /** @@ -138,20 +156,24 @@ const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, exte * @param {Function} addBroadcastMsg function to update broadcast message name map * @param {Function} getVariableId function to retreive a variable's ID based on name * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. + * @param {object} comments Comments that need to be attached to the blocks that need to be parsed */ -const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions) { +const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions, comments) { + // Keep track of the index of the current script being + // parsed in order to attach block comments correctly + let scriptIndexForComment = 0; + for (let i = 0; i < scripts.length; i++) { const script = scripts[i]; const scriptX = script[0]; const scriptY = script[1]; const blockList = script[2]; - const parsedBlockList = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions); + const [parsedBlockList, newCommentIndex] = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions, + comments, scriptIndexForComment); + scriptIndexForComment = newCommentIndex; if (parsedBlockList[0]) { - // Adjust script coordinates to account for - // larger block size in scratch-blocks. - // @todo: Determine more precisely the right formulas here. - parsedBlockList[0].x = scriptX * 1.5; - parsedBlockList[0].y = scriptY * 2.2; + parsedBlockList[0].x = scriptX * WORKSPACE_X_SCALE; + parsedBlockList[0].y = scriptY * WORKSPACE_Y_SCALE; parsedBlockList[0].topLevel = true; parsedBlockList[0].parent = null; } @@ -242,11 +264,13 @@ const parseMonitorObject = (object, runtime, targets, extensions) => { // Create var id getter to make block naming/parsing easier, variables already created. const getVariableId = generateVariableIdGetter(target.id, false); // eslint-disable-next-line no-use-before-define - const block = parseBlock( + const [block, _] = parseBlock( [object.cmd, object.param], // Scratch 2 monitor blocks only have one param. null, // `addBroadcastMsg`, not needed for monitor blocks. getVariableId, - extensions + extensions, + null, // `comments`, not needed for monitor blocks + null // `commentIndex`, not needed for monitor blocks ); // Monitor blocks have special IDs to match the toolbox obtained from the getId @@ -423,9 +447,63 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) } } + // If included, parse any and all comments on the object (this includes top-level + // workspace comments as well as comments attached to specific blocks) + const blockComments = {}; + if (object.hasOwnProperty('scriptComments')) { + const comments = object.scriptComments.map(commentDesc => { + const [ + commentX, + commentY, + commentWidth, + commentHeight, + commentFullSize, + flattenedBlockIndex, + commentText + ] = commentDesc; + const isBlockComment = commentDesc[5] >= 0; + const newComment = new Comment( + null, // generate a new id for this comment + commentText, // text content of sb2 comment + // Only serialize x & y position of comment if it's a workspace comment + // If it's a block comment, we'll let scratch-blocks handle positioning + isBlockComment ? null : commentX * WORKSPACE_X_SCALE, + isBlockComment ? null : commentY * WORKSPACE_Y_SCALE, + commentWidth * WORKSPACE_X_SCALE, + commentHeight * WORKSPACE_Y_SCALE, + !commentFullSize + ); + if (isBlockComment) { + // commentDesc[5] refers to the index of the block that this + // comment is attached to -- in a flattened version of the + // scripts array. + // If commentDesc[5] is -1, this is a workspace comment (we don't need to do anything + // extra at this point), otherwise temporarily save the flattened script array + // index as the blockId property of the new comment. We will + // change this to refer to the actual block id of the corresponding + // block when that block gets created + newComment.blockId = flattenedBlockIndex; + // Add this comment to the block comments object with its script index + // as the key + if (blockComments.hasOwnProperty(flattenedBlockIndex)) { + blockComments[flattenedBlockIndex].push(newComment); + } else { + blockComments[flattenedBlockIndex] = [newComment]; + } + } + return newComment; + }); + + // Add all the comments that were just created to the target.comments, + // referenced by id + comments.forEach(comment => { + target.comments[comment.id] = comment; + }); + } + // If included, parse any and all scripts/blocks on the object. if (object.hasOwnProperty('scripts')) { - parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions); + parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions, blockComments); } // Update stage specific blocks (e.g. sprite clicked <=> stage clicked) @@ -619,9 +697,15 @@ const specMapBlock = function (block) { * @param {Function} addBroadcastMsg function to update broadcast message name map * @param {Function} getVariableId function to retrieve a variable's ID based on name * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. - * @return {object} Scratch VM format block, or null if unsupported object. + * @param {object} comments - Comments from sb2 project that need to be attached to blocks. + * They are indexed in this object by the sb2 flattened block list index indicating + * which block they should attach to. + * @param {int} commentIndex The comment index for the block to be parsed if it were in a flattened + * list of all blocks for the target + * @return {[object, int]} Tuple where first item is the Scratch VM-format block (or null if unsupported object), + * and second item is the updated comment index (after this block and its children are parsed) */ -const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions) { +const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions, comments, commentIndex) { const blockMetadata = specMapBlock(sb2block); if (!blockMetadata) { return; @@ -645,6 +729,21 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension shadow: false, // No shadow blocks in an SB2 by default. children: [] // Store any generated children, flattened in `flatten`. }; + + // Attach any comments to this block.. + const commentsForParsedBlock = (comments && typeof commentIndex === 'number' && !isNaN(commentIndex)) ? + comments[commentIndex] : null; + if (commentsForParsedBlock) { + // TODO currently only attaching the last comment to the block if there are multiple... + // not sure what to do here.. concatenate all the messages in all the comments and only + // keep one around? + activeBlock.comment = commentsForParsedBlock[commentsForParsedBlock.length - 1].id; + commentsForParsedBlock.forEach(comment => { + comment.blockId = activeBlock.id; + }); + } + commentIndex++; + // For a procedure call, generate argument map from proc string. if (oldOpcode === 'call') { blockMetadata.argMap = parseProcedureArgMap(sb2block[1]); @@ -671,10 +770,15 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension let innerBlocks; if (typeof providedArg[0] === 'object' && providedArg[0]) { // Block list occupies the input. - innerBlocks = parseBlockList(providedArg, addBroadcastMsg, getVariableId, extensions); + [innerBlocks, commentIndex] = parseBlockList(providedArg, addBroadcastMsg, getVariableId, + extensions, comments, commentIndex); } else { // Single block occupies the input. - innerBlocks = [parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions)]; + const parsedBlockDesc = parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions, + comments, commentIndex); + innerBlocks = [parsedBlockDesc[0]]; + // Update commentIndex + commentIndex = parsedBlockDesc[1]; } let previousBlock = null; for (let j = 0; j < innerBlocks.length; j++) { @@ -914,7 +1018,7 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension break; } } - return activeBlock; + return [activeBlock, commentIndex]; }; module.exports = { diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index f8a4bb1eb..b6df05e6c 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -930,7 +930,6 @@ class RenderedTarget extends Target { newClone.rotationStyle = this.rotationStyle; newClone.effects = JSON.parse(JSON.stringify(this.effects)); newClone.variables = JSON.parse(JSON.stringify(this.variables)); - newClone.lists = JSON.parse(JSON.stringify(this.lists)); newClone.initDrawable(StageLayering.SPRITE_LAYER); newClone.updateAllDrawableProperties(); // Place behind the current target. @@ -957,7 +956,6 @@ class RenderedTarget extends Target { newTarget.rotationStyle = this.rotationStyle; newTarget.effects = JSON.parse(JSON.stringify(this.effects)); newTarget.variables = JSON.parse(JSON.stringify(this.variables)); - newTarget.lists = JSON.parse(JSON.stringify(this.lists)); newTarget.updateAllDrawableProperties(); newTarget.goBehindOther(this); return newTarget; @@ -1049,7 +1047,6 @@ class RenderedTarget extends Target { rotationStyle: this.rotationStyle, blocks: this.blocks._blocks, variables: this.variables, - lists: this.lists, costumes: costumes, sounds: this.getSounds(), tempo: this.tempo, diff --git a/src/virtual-machine.js b/src/virtual-machine.js index d236650ff..ca6d8d173 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1005,12 +1005,16 @@ class VirtualMachine extends EventEmitter { ); const variables = Object.keys(variableMap).map(k => variableMap[k]); + const workspaceComments = Object.keys(this.editingTarget.comments) + .map(k => this.editingTarget.comments[k]) + .filter(c => c.blockId === null); const xmlString = ` ${variables.map(v => v.toXML()).join()} - ${this.editingTarget.blocks.toXML()} + ${workspaceComments.map(c => c.toXML()).join()} + ${this.editingTarget.blocks.toXML(this.editingTarget.comments)} `; this.emit('workspaceUpdate', {xml: xmlString}); diff --git a/test/fixtures/comments.sb2 b/test/fixtures/comments.sb2 new file mode 100644 index 000000000..081fbe68a Binary files /dev/null and b/test/fixtures/comments.sb2 differ diff --git a/test/integration/comments.js b/test/integration/comments.js new file mode 100644 index 000000000..5962b5eb6 --- /dev/null +++ b/test/integration/comments.js @@ -0,0 +1,91 @@ +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 projectUri = path.resolve(__dirname, '../fixtures/comments.sb2'); +const project = readFileToBuffer(projectUri); + +test('importing sb2 project with comments', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.equal(threads.length, 0); + + const stage = vm.runtime.targets[0]; + const target = vm.runtime.targets[1]; + + const stageComments = Object.values(stage.comments); + + // Stage has 1 comment, and it is minimized. + t.equal(stageComments.length, 1); + t.equal(stageComments[0].minimized, true); + t.equal(stageComments[0].text, 'A minimized stage comment.'); + // The stage comment is a workspace comment + t.equal(stageComments[0].blockId, null); + + // Sprite 1 has 6 Comments, 1 workspace comment, and 5 block comments + const targetComments = Object.values(target.comments); + t.equal(targetComments.length, 6); + const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null); + t.equal(spriteWorkspaceComments.length, 1); + t.equal(spriteWorkspaceComments[0].minimized, false); + t.equal(spriteWorkspaceComments[0].text, 'This is a workspace comment.'); + + // Test the sprite block comments + const blockComments = targetComments.filter(comment => !!comment.blockId); + t.equal(blockComments.length, 5); + + t.equal(blockComments[0].minimized, true); + t.equal(blockComments[0].text, '1. Green Flag Comment.'); + const greenFlagBlock = target.blocks.getBlock(blockComments[0].blockId); + t.equal(greenFlagBlock.comment, blockComments[0].id); + t.equal(greenFlagBlock.opcode, 'event_whenflagclicked'); + + t.equal(blockComments[1].minimized, true); + t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.'); + const turnRightBlock = target.blocks.getBlock(blockComments[1].blockId); + t.equal(turnRightBlock.comment, blockComments[1].id); + t.equal(turnRightBlock.opcode, 'motion_turnright'); + + t.equal(blockComments[2].minimized, false); + t.equal(blockComments[2].text, '3. Comment for a loop.'); + const repeatBlock = target.blocks.getBlock(blockComments[2].blockId); + t.equal(repeatBlock.comment, blockComments[2].id); + t.equal(repeatBlock.opcode, 'control_repeat'); + + t.equal(blockComments[3].minimized, false); + t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.'); + const changeColorBlock = target.blocks.getBlock(blockComments[3].blockId); + t.equal(changeColorBlock.comment, blockComments[3].id); + t.equal(changeColorBlock.opcode, 'looks_changeeffectby'); + + t.equal(blockComments[4].minimized, false); + t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.'); + const stopAllBlock = target.blocks.getBlock(blockComments[4].blockId); + t.equal(stopAllBlock.comment, blockComments[4].id); + t.equal(stopAllBlock.opcode, 'control_stop'); + + 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); + }); + }); +}); diff --git a/test/integration/import_sb2.js b/test/integration/import_sb2.js index 04c41d46a..fb6fe8508 100644 --- a/test/integration/import_sb2.js +++ b/test/integration/import_sb2.js @@ -30,7 +30,7 @@ test('default', t => { t.type(targets[0].id, 'string'); t.type(targets[0].blocks, 'object'); t.type(targets[0].variables, 'object'); - t.type(targets[0].lists, 'object'); + t.type(targets[0].comments, 'object'); t.equal(targets[0].isOriginal, true); t.equal(targets[0].currentCostume, 0); @@ -41,7 +41,7 @@ test('default', t => { t.type(targets[1].id, 'string'); t.type(targets[1].blocks, 'object'); t.type(targets[1].variables, 'object'); - t.type(targets[1].lists, 'object'); + t.type(targets[1].comments, 'object'); t.equal(targets[1].isOriginal, true); t.equal(targets[1].currentCostume, 0); diff --git a/test/unit/engine_target.js b/test/unit/engine_target.js index 30ff5eb82..f6bb8df28 100644 --- a/test/unit/engine_target.js +++ b/test/unit/engine_target.js @@ -12,7 +12,7 @@ test('spec', t => { t.type(target.id, 'string'); t.type(target.blocks, 'object'); t.type(target.variables, 'object'); - t.type(target.lists, 'object'); + t.type(target.comments, 'object'); t.type(target._customState, 'object'); t.type(target.createVariable, 'function'); diff --git a/test/unit/serialization_sb2.js b/test/unit/serialization_sb2.js index 4d8c906b4..2901eb661 100644 --- a/test/unit/serialization_sb2.js +++ b/test/unit/serialization_sb2.js @@ -29,7 +29,7 @@ test('default', t => { t.type(targets[0].id, 'string'); t.type(targets[0].blocks, 'object'); t.type(targets[0].variables, 'object'); - t.type(targets[0].lists, 'object'); + t.type(targets[0].comments, 'object'); t.equal(targets[0].isOriginal, true); t.equal(targets[0].currentCostume, 0); @@ -40,7 +40,7 @@ test('default', t => { t.type(targets[1].id, 'string'); t.type(targets[1].blocks, 'object'); t.type(targets[1].variables, 'object'); - t.type(targets[1].lists, 'object'); + t.type(targets[1].comments, 'object'); t.equal(targets[1].isOriginal, true); t.equal(targets[1].currentCostume, 0); diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js index f6731b194..70d06fee3 100644 --- a/test/unit/virtual-machine.js +++ b/test/unit/virtual-machine.js @@ -307,6 +307,17 @@ test('duplicateSprite assigns duplicated sprite a fresh name', t => { 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, @@ -316,7 +327,13 @@ test('emitWorkspaceUpdate', t => { } }, blocks: { - toXML: () => 'blocks' + toXML: blocksToXML + }, + comments: { + aStageComment: { + toXML: () => 'aStageComment', + blockId: null + } } }, { variables: { @@ -325,7 +342,13 @@ test('emitWorkspaceUpdate', t => { } }, blocks: { - toXML: () => 'blocks' + toXML: blocksToXML + }, + comments: { + someBlockComment: { + toXML: () => 'someBlockComment', + blockId: 'someBlockId' + } } }, { variables: { @@ -334,7 +357,17 @@ test('emitWorkspaceUpdate', t => { } }, blocks: { - toXML: () => 'blocks' + toXML: blocksToXML + }, + comments: { + someOtherComment: { + toXML: () => 'someOtherComment', + blockId: null + }, + aBlockComment: { + toXML: () => 'aBlockComment', + blockId: 'a block' + } } } ]; @@ -347,6 +380,10 @@ test('emitWorkspaceUpdate', t => { 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(); });