From 30f033973ffe4d77b08b59581f907b18c89a6bf8 Mon Sep 17 00:00:00 2001 From: Simon Bates Date: Thu, 26 Sep 2024 09:32:42 -0400 Subject: [PATCH 1/2] Tests for ProgramBlockEditor change loop iterations --- src/ProgramBlockEditor.test.js | 86 ++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/ProgramBlockEditor.test.js b/src/ProgramBlockEditor.test.js index 0d3dbaea..e1c24214 100644 --- a/src/ProgramBlockEditor.test.js +++ b/src/ProgramBlockEditor.test.js @@ -145,6 +145,11 @@ function getProgramBlockLoopLabel(programBlockEditorWrapper, index: number) { .find('.command-block-loop-label-container').getDOMNode().textContent; } +function getProgramBlockLoopIterationsInput(programBlockEditorWrapper, index: number) { + return getProgramBlocks(programBlockEditorWrapper).at(index) + .find('.command-block-loop-iterations'); +} + function getProgramBlockLoopIterations(programBlockEditorWrapper, index: number) { return ((getProgramBlocks(programBlockEditorWrapper).at(index) .find('.command-block-loop-iterations') @@ -473,6 +478,87 @@ describe('Active loop container highlight', () => { }); }); +describe('Change loop iterations', () => { + test('The ProgramSequence should be updated when the loop iterations are changed when the program is stopped', () => { + const { wrapper, mockChangeProgramSequenceHandler } = createMountProgramBlockEditor({ + runningState: 'stopped', + programSequence: new ProgramSequence( + [ + {block: 'startLoop', label: 'A', iterations: 2}, + {block: 'endLoop', label: 'A'} + ], + 0, + 0, + new Map([['A', 2]]) + ) + }); + + const input = getProgramBlockLoopIterationsInput(wrapper, 0); + ((input.getDOMNode(): any): HTMLInputElement).value = '4'; + input.simulate('change'); + input.getDOMNode().dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'})); + + expect(mockChangeProgramSequenceHandler.mock.calls.length).toBe(1); + expect(mockChangeProgramSequenceHandler.mock.calls[0][0]).toStrictEqual( + new ProgramSequence( + [ + { + block: 'startLoop', + label: 'A', + iterations: 4 + }, + { + block: 'endLoop', + label: 'A' + } + ], + 0, + 0, + new Map([['A', 2]]) + ) + ); + }); + test('The ProgramSequence and loopIterationsLeft should be updated when the loop iterations are changed when the program is paused', () => { + const { wrapper, mockChangeProgramSequenceHandler } = createMountProgramBlockEditor({ + runningState: 'paused', + programSequence: new ProgramSequence( + [ + {block: 'startLoop', label: 'A', iterations: 2}, + {block: 'endLoop', label: 'A'} + ], + 0, + 0, + new Map([['A', 1]]) + ) + }); + + const input = getProgramBlockLoopIterationsInput(wrapper, 0); + ((input.getDOMNode(): any): HTMLInputElement).value = '4'; + input.simulate('change'); + input.getDOMNode().dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'})); + + expect(mockChangeProgramSequenceHandler.mock.calls.length).toBe(1); + expect(mockChangeProgramSequenceHandler.mock.calls[0][0]).toStrictEqual( + new ProgramSequence( + [ + { + block: 'startLoop', + label: 'A', + iterations: 4 + }, + { + block: 'endLoop', + label: 'A' + } + ], + 0, + 0, + new Map([['A', 4]]) + ) + ); + }); +}); + describe('The expand add node toggle switch should be configurable via properties', () => { describe('Given that addNodeExpandedMode is false', () => { test('Then the toggle switch should be off, and the change handler should be wired up', () => { From 3e7fb0323cbafe841d0c1481bd5e50608daa4acd Mon Sep 17 00:00:00 2001 From: Simon Bates Date: Thu, 26 Sep 2024 11:35:43 -0400 Subject: [PATCH 2/2] Add ProgramBlockCache and new ProgramBlock types * Replace the Map used for the program block cache with a new ProgramBlockCache class, with typed accessors for the loop label and position * Add new separate program block types for: MovementProgramBlock, StartLoopProgramBlock, and EndLoopProgramBlock --- src/ActionPanel.js | 36 ++--- src/ActionPanel.test.js | 31 +--- src/ActionsHandler.js | 4 +- src/ActionsHandler.test.js | 4 +- src/AnnouncementBuilder.js | 50 ++++--- src/AnnouncementBuilder.test.js | 3 +- src/Interpreter.js | 9 +- src/Interpreter.test.js | 4 +- src/ProgramBlockCache.js | 19 +++ src/ProgramBlockEditor.js | 60 ++++---- src/ProgramBlockEditor.test.js | 57 ++++--- src/ProgramChangeController.test.js | 17 ++- src/ProgramSequence.js | 141 +++++++++-------- src/ProgramSequence.test.js | 225 +++++++++------------------- src/ProgramSerializer.js | 15 +- src/Utils.js | 14 +- src/types.js | 27 +++- 17 files changed, 337 insertions(+), 379 deletions(-) create mode 100644 src/ProgramBlockCache.js diff --git a/src/ActionPanel.js b/src/ActionPanel.js index 109264b0..dc8acc0e 100644 --- a/src/ActionPanel.js +++ b/src/ActionPanel.js @@ -38,8 +38,8 @@ class ActionPanel extends React.Component { let stepNumber = this.props.pressedStepIndex + 1; const cachedCurrentStepLoopData = currentStep.cache; - if (cachedCurrentStepLoopData != null && cachedCurrentStepLoopData.get('containingLoopPosition') != null) { - stepNumber = cachedCurrentStepLoopData.get('containingLoopPosition'); + if (cachedCurrentStepLoopData != null) { + stepNumber = cachedCurrentStepLoopData.getContainingLoopPosition(); } let stepName = ''; @@ -78,15 +78,14 @@ class ActionPanel extends React.Component { if (this.props.pressedStepIndex > 0) { const prevStep = this.props.programSequence.getProgramStepAt(this.props.pressedStepIndex - 1); const cachedPreviousStepLoopData = prevStep.cache; - const prevStepName = prevStep.block; // When previous step is startLoop, aria-label communicates that movePrevious will move out of the current loop - if (prevStepName === 'startLoop' && currentStep.block !== 'endLoop') { + if (prevStep.block === 'startLoop' && currentStep.block !== 'endLoop') { return this.props.intl.formatMessage( { id: 'CommandInfo.previousStep.startLoop' }, { loopLabel: prevStep.label } ); // When previous step is endLoop, aria-label communicates that movePrevious will move into a loop - } else if (prevStepName === 'endLoop') { + } else if (prevStep.block === 'endLoop') { return this.props.intl.formatMessage( { id: 'CommandInfo.previousStep.endLoop'}, { loopLabel: prevStep.label } @@ -107,15 +106,13 @@ class ActionPanel extends React.Component { ) } // When previous step is wrapped in a loop, aria-label communicates position within a loop - } else if (cachedPreviousStepLoopData != null && - cachedPreviousStepLoopData.get('containingLoopPosition') != null && - cachedPreviousStepLoopData.get('containingLoopLabel') != null) { + } else if (cachedPreviousStepLoopData != null) { return this.props.intl.formatMessage( { id: 'CommandInfo.previousStep.inLoop'}, { - previousStepNumber: cachedPreviousStepLoopData.get('containingLoopPosition'), - command: this.props.intl.formatMessage({id: `Command.${prevStepName}`}), - loopLabel: cachedPreviousStepLoopData.get('containingLoopLabel') + previousStepNumber: cachedPreviousStepLoopData.getContainingLoopPosition(), + command: this.props.intl.formatMessage({id: `Command.${prevStep.block}`}), + loopLabel: cachedPreviousStepLoopData.getContainingLoopLabel(), } ) // When previous step is a movements step and not in a loop, aria-label communicates position within the program @@ -124,7 +121,7 @@ class ActionPanel extends React.Component { { id: 'CommandInfo.previousStep'}, { previousStepNumber: this.props.pressedStepIndex, - command: this.props.intl.formatMessage({id: `Command.${prevStepName}`}) + command: this.props.intl.formatMessage({id: `Command.${prevStep.block}`}) } ); } @@ -137,15 +134,14 @@ class ActionPanel extends React.Component { if (this.props.pressedStepIndex < (this.props.programSequence.getProgramLength() - 1)) { const nextStep = this.props.programSequence.getProgramStepAt(this.props.pressedStepIndex + 1); const cachedNextStepLoopData = nextStep.cache; - const nextStepName = nextStep.block; // When next step is startLoop, aria-label communicates that moveNext will move into a loop - if (nextStepName === 'startLoop') { + if (nextStep.block === 'startLoop') { return this.props.intl.formatMessage( { id: 'CommandInfo.nextStep.startLoop'}, { loopLabel: nextStep.label } ); // When next step is endLoop, aria-label communicates that moveNext will move out of the current loop - } else if (nextStepName === 'endLoop' && currentStep.block !== 'startLoop') { + } else if (nextStep.block === 'endLoop' && currentStep.block !== 'startLoop') { return this.props.intl.formatMessage( { id: 'CommandInfo.nextStep.endLoop'}, { loopLabel: nextStep.label } @@ -166,15 +162,13 @@ class ActionPanel extends React.Component { ); } // When next step is wrapped in a loop, aria-label communicates position within a loop - } else if (cachedNextStepLoopData != null && - cachedNextStepLoopData.get('containingLoopPosition') != null && - cachedNextStepLoopData.get('containingLoopLabel') != null) { + } else if (cachedNextStepLoopData != null) { return this.props.intl.formatMessage( { id: 'CommandInfo.nextStep.inLoop'}, { - nextStepNumber: cachedNextStepLoopData.get('containingLoopPosition'), - command: this.props.intl.formatMessage({id: `Command.${nextStepName}`}), - loopLabel: cachedNextStepLoopData.get('containingLoopLabel') + nextStepNumber: cachedNextStepLoopData.getContainingLoopPosition(), + command: this.props.intl.formatMessage({id: `Command.${nextStep.block}`}), + loopLabel: cachedNextStepLoopData.getContainingLoopLabel() } ); // When next step is a movements step and not in a loop, aria-label communicates position within the program diff --git a/src/ActionPanel.test.js b/src/ActionPanel.test.js index eca19e01..aacb778d 100644 --- a/src/ActionPanel.test.js +++ b/src/ActionPanel.test.js @@ -5,6 +5,7 @@ import Adapter from 'enzyme-adapter-react-16'; import { configure, mount } from 'enzyme'; import { IntlProvider } from 'react-intl'; import ActionPanel from './ActionPanel'; +import ProgramBlockCache from './ProgramBlockCache'; import ProgramSequence from './ProgramSequence'; import messages from './messages.json'; @@ -233,10 +234,7 @@ describe('ActionPanel options', () => { }, { block: 'forward1', - cache: new Map([ - ['containingLoopPosition', 1], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', @@ -288,17 +286,11 @@ describe('ActionPanel options', () => { }, { block: 'forward1', - cache: new Map([ - ['containingLoopPosition', 1], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'forward2', - cache: new Map([ - ['containingLoopPosition', 2], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 2) }, { block: 'endLoop', @@ -443,10 +435,7 @@ describe('ActionPanel options', () => { }, { block: 'forward1', - cache: new Map([ - ['containingLoopPosition', 1], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', @@ -498,17 +487,11 @@ describe('ActionPanel options', () => { }, { block: 'forward1', - cache: new Map([ - ['containingLoopPosition', 1], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'forward2', - cache: new Map([ - ['containingLoopPosition', 2], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 2) }, { block: 'endLoop', diff --git a/src/ActionsHandler.js b/src/ActionsHandler.js index 9b26ac2e..c4101fde 100644 --- a/src/ActionsHandler.js +++ b/src/ActionsHandler.js @@ -6,7 +6,7 @@ import CharacterMessageBuilder from './CharacterMessageBuilder'; import type { CharacterUpdate } from './CharacterState'; import type { IntlShape } from 'react-intl'; import SceneDimensions from './SceneDimensions'; -import type { AudioManager, BlockName } from './types'; +import type { AudioManager, MovementBlockName } from './types'; // The ActionsHandler is called by the Interpreter for each program // step action as the program is running, and is responsible for @@ -27,7 +27,7 @@ export default class ActionsHandler { this.characterMessageBuilder = new CharacterMessageBuilder(sceneDimensions, intl); } - doAction(action: BlockName, stepTimeMs: number): Promise { + doAction(action: MovementBlockName, stepTimeMs: number): Promise { switch(action) { case 'forward1': return this.forward(1, action, stepTimeMs); diff --git a/src/ActionsHandler.test.js b/src/ActionsHandler.test.js index 1fdbc453..bef8313d 100644 --- a/src/ActionsHandler.test.js +++ b/src/ActionsHandler.test.js @@ -8,7 +8,7 @@ import CharacterState from './CharacterState'; import CustomBackground from './CustomBackground'; import { createIntl } from 'react-intl'; import SceneDimensions from './SceneDimensions'; -import type { BlockName } from './types'; +import type { MovementBlockName } from './types'; import messages from './messages.json'; @@ -49,7 +49,7 @@ function createActionsHandler() { } type MovementTestCase = {| - action: BlockName, + action: MovementBlockName, x: number, y: number, direction: number, diff --git a/src/AnnouncementBuilder.js b/src/AnnouncementBuilder.js index 866cceec..dbfb0be6 100644 --- a/src/AnnouncementBuilder.js +++ b/src/AnnouncementBuilder.js @@ -60,30 +60,38 @@ export default class AnnouncementBuilder { } buildDeleteStepAnnouncement(programBlock: ProgramBlock): AnnouncementData { - let commandType = null; if (programBlock.block === 'startLoop' || programBlock.block === 'endLoop') { - commandType = this.intl.formatMessage({ - id: "Announcement.control" - }); + return { + messageIdSuffix: 'delete', + values: { + commandType: this.intl.formatMessage({ + id: "Announcement.control" + }), + command: this.intl.formatMessage( + { + id: `Announcement.${programBlock.block}` + }, + { + loopLabel: programBlock.label + } + ) + } + }; } else { - commandType = this.intl.formatMessage({ - id: "Announcement.movement" - }); + return { + messageIdSuffix: 'delete', + values: { + commandType: this.intl.formatMessage({ + id: "Announcement.movement" + }), + command: this.intl.formatMessage( + { + id: `Announcement.${programBlock.block}` + } + ) + } + }; } - return { - messageIdSuffix: 'delete', - values: { - commandType: commandType, - command: this.intl.formatMessage( - { - id: `Announcement.${programBlock.block}` - }, - { - loopLabel: programBlock.label - } - ) - } - }; } buildReplaceStepAnnouncement(programBlock: ProgramBlock, diff --git a/src/AnnouncementBuilder.test.js b/src/AnnouncementBuilder.test.js index d0b7208b..b9858765 100644 --- a/src/AnnouncementBuilder.test.js +++ b/src/AnnouncementBuilder.test.js @@ -64,7 +64,8 @@ describe('Test buildDeleteStepAnnouncement()', () => { const startLoopBlock = { block: 'startLoop', - label: 'A' + label: 'A', + iterations: 1 }; expect(announcementBuilder.buildDeleteStepAnnouncement(startLoopBlock)).toStrictEqual({ diff --git a/src/Interpreter.js b/src/Interpreter.js index 4d24e23b..95b9a2cf 100644 --- a/src/Interpreter.js +++ b/src/Interpreter.js @@ -4,7 +4,7 @@ import { App } from './App'; import ActionsHandler from './ActionsHandler'; import type { ActionResult } from './ActionsHandler'; import ProgramSequence from './ProgramSequence'; -import type { ProgramBlock } from './types'; +import type { MovementProgramBlock } from './types'; export default class Interpreter { stepTimeMs: number; @@ -84,14 +84,13 @@ export default class Interpreter { resolve('success'); } else { const currentProgramStep = programSequence.getCurrentProgramStep(); - const block = currentProgramStep.block; - if (block === 'startLoop') { + if (currentProgramStep.block === 'startLoop') { this.doStartLoop(programSequence).then(() => { this.app.advanceProgramCounter(() => { resolve('success'); }); }); - } else if (block === 'endLoop') { + } else if (currentProgramStep.block === 'endLoop') { // We don't intend for the programCounter to ever be on an // 'endLoop' block, but we might have a bug that would // cause that case to happen and we want to handle it @@ -131,7 +130,7 @@ export default class Interpreter { } } - doAction(programStep: ProgramBlock): Promise { + doAction(programStep: MovementProgramBlock): Promise { return this.actionsHandler.doAction(programStep.block, this.stepTimeMs); } } diff --git a/src/Interpreter.test.js b/src/Interpreter.test.js index 37cd351d..17bf0bf8 100644 --- a/src/Interpreter.test.js +++ b/src/Interpreter.test.js @@ -6,7 +6,7 @@ import Interpreter from './Interpreter'; import ProgramSequence from './ProgramSequence'; import type { IntlShape } from 'react-intl'; import SceneDimensions from './SceneDimensions'; -import type { AudioManager, BlockName } from './types'; +import type { AudioManager, MovementBlockName } from './types'; jest.mock('./ActionsHandler'); jest.mock('./App'); @@ -30,7 +30,7 @@ function createInterpreter() { ((null: any): IntlShape) ); - actionsHandlerMock.doAction.mockImplementation((action: BlockName) => { + actionsHandlerMock.doAction.mockImplementation((action: MovementBlockName) => { // Mock ActionsHandler behaviour to test handling of different // Promise results switch(action) { diff --git a/src/ProgramBlockCache.js b/src/ProgramBlockCache.js new file mode 100644 index 00000000..ae68307b --- /dev/null +++ b/src/ProgramBlockCache.js @@ -0,0 +1,19 @@ +// @flow + +export default class ProgramBlockCache { + containingLoopLabel: string; + containingLoopPosition: number; + + constructor(containingLoopLabel: string, containingLoopPosition: number) { + this.containingLoopLabel = containingLoopLabel; + this.containingLoopPosition = containingLoopPosition; + } + + getContainingLoopLabel(): string { + return this.containingLoopLabel; + } + + getContainingLoopPosition(): number { + return this.containingLoopPosition; + } +}; diff --git a/src/ProgramBlockEditor.js b/src/ProgramBlockEditor.js index cfbfe8e9..156a4c17 100644 --- a/src/ProgramBlockEditor.js +++ b/src/ProgramBlockEditor.js @@ -17,6 +17,7 @@ import IconButton from './IconButton'; import ProgramIterator from './ProgramIterator'; import ProgramSequence from './ProgramSequence'; import ToggleSwitch from './ToggleSwitch'; +import { copyProgramBlock } from './Utils'; import { ReactComponent as AddIcon } from './svg/Add.svg'; import { ReactComponent as DeleteAllIcon } from './svg/DeleteAll.svg'; import './ProgramBlockEditor.scss'; @@ -305,30 +306,28 @@ export class ProgramBlockEditor extends React.Component { - // $FlowFixMe: property 'dataset' is missing in 'EventTarget' - if (e.currentTarget.dataset.command === 'startLoop' || e.currentTarget.dataset.command === 'endLoop') { - const loopLabel = this.props.programSequence.getProgramStepAt( - parseInt(e.currentTarget.dataset.stepnumber, 10) - ).label; - if (loopLabel != null) { - this.setState({ - loopLabelOfFocusedLoopBlock: loopLabel - }); - } + const block = this.props.programSequence.getProgramStepAt( + // $FlowFixMe: property 'dataset' is missing in 'EventTarget' + parseInt(e.currentTarget.dataset.stepnumber, 10) + ); + if (block.block === 'startLoop' || block.block === 'endLoop') { + this.setState({ + loopLabelOfFocusedLoopBlock: block.label + }); } }; @@ -449,7 +448,8 @@ export class ProgramBlockEditor extends React.Component { expect(getProgramBlocks(wrapper).length).toBe(2); expect(getProgramBlocks(wrapper).at(0).prop('data-command')).toBe('startLoop'); expect(getProgramBlockLoopLabel(wrapper, 0)).toBe('A'); - expect(getProgramBlockLoopIterations(wrapper, 0)).toBe('1'); + expect(getProgramBlockLoopIterationsInputValue(wrapper, 0)).toBe('1'); expect(getProgramBlocks(wrapper).at(1).prop('data-command')).toBe('endLoop'); expect(getProgramBlockLoopLabel(wrapper, 1)).toBe('A'); wrapper.setProps({ runningState: 'stopped' }); - expect(getProgramBlockLoopIterations(wrapper, 0)).toBe('2'); + expect(getProgramBlockLoopIterationsInputValue(wrapper, 0)).toBe('2'); }); test('Loop blocks should be wrapped in a container', () => { const { wrapper } = createMountProgramBlockEditor({ programSequence: new ProgramSequence( [ - {block: 'startLoop', label: 'A', iterations: 2}, - {block: 'forward1', cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ])}, - {block: 'startLoop', label: 'B', iterations: 2, cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 2] - ])}, - {block: 'left45', cache: new Map([ - ['containingLoopLabel', 'B'], - ['containingLoopPosition', 3] - ])}, - {block: 'endLoop', label: 'B', cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 4] - ])}, - {block: 'endLoop', label: 'A'}, - {block: 'right45'} + { + block: 'startLoop', + label: 'A', + iterations: 2 + }, + { + block: 'forward1', + cache: new ProgramBlockCache('A', 1) + }, + { + block: 'startLoop', + label: 'B', + iterations: 2, + cache: new ProgramBlockCache('A', 2) + }, + { + block: 'left45', + cache: new ProgramBlockCache('B', 3) + }, + { + block: 'endLoop', + label: 'B', + cache: new ProgramBlockCache('A', 4) + }, + { + block: 'endLoop', + label: 'A' + }, + { + block: 'right45' + } ], 0, 0, diff --git a/src/ProgramChangeController.test.js b/src/ProgramChangeController.test.js index 48a64a95..69d6d40e 100644 --- a/src/ProgramChangeController.test.js +++ b/src/ProgramChangeController.test.js @@ -411,7 +411,22 @@ describe('Test replaceProgramStep()', () => { appMock.setState.mockImplementation((updater) => { const newState = updater({ - programSequence: new ProgramSequence([{block: 'startLoop'}, {block: 'endLoop'}], 0, 0, new Map()) + programSequence: new ProgramSequence( + [ + { + block: 'startLoop', + label: 'A', + iterations: 1 + }, + { + block: 'endLoop', + label: 'A' + } + ], + 0, + 0, + new Map() + ) }); // The program should not be updated diff --git a/src/ProgramSequence.js b/src/ProgramSequence.js index 50a43192..8e032f02 100644 --- a/src/ProgramSequence.js +++ b/src/ProgramSequence.js @@ -1,8 +1,9 @@ // @flow -import { generateLoopLabel } from './Utils'; +import { copyProgramBlock, generateLoopLabel } from './Utils'; +import ProgramBlockCache from './ProgramBlockCache'; import type { ProgramParserResult } from './ProgramParser'; -import type { CommandName, Program, ProgramBlock, ProgramBlockCache } from './types'; +import type { CommandName, EndLoopProgramBlock, Program, ProgramBlock, StartLoopProgramBlock } from './types'; // When a new loop is added to the program, initialize the number of // iterations to this value: @@ -114,7 +115,7 @@ export default class ProgramSequence { // loopStack is a stack that stores loop labels from startLoop blocks // while iterating through the program to keep track of direct parent loop - const loopStack = []; + const loopStack: Array = []; // loopPositionStack is a stack that stores position of a program step within a direct parent loop const loopPositionStack = []; let containingLoopPosition = 0; @@ -128,18 +129,14 @@ export default class ProgramSequence { } if (loopStack.length > 0) { containingLoopPosition++; - const cache: ProgramBlockCache = new Map(); - cache.set('containingLoopLabel', ((loopStack[loopStack.length - 1]: any): string)); - cache.set('containingLoopPosition', containingLoopPosition); - resultProgram.push(Object.assign( - {}, - block, - { - cache - } - )); + const blockWithCache = copyProgramBlock(block); + blockWithCache.cache = new ProgramBlockCache( + loopStack[loopStack.length - 1], + containingLoopPosition + ); + resultProgram.push(blockWithCache); } else { - resultProgram.push(Object.assign({}, block)); + resultProgram.push(copyProgramBlock(block)); delete resultProgram[resultProgram.length - 1]['cache']; } if (block.block === 'startLoop') { @@ -215,59 +212,55 @@ export default class ProgramSequence { while (newProgramCounter < this.getProgramLength() && this.program[newProgramCounter].block === 'endLoop') { const label = this.program[newProgramCounter].label; - if (label != null) { - const currentIterationsLeft = newLoopIterationsLeft.get(label); - if (currentIterationsLeft != null) { - // If the number of iterations left for the loop is > 0, - // decrement it - let newIterationsLeft = currentIterationsLeft; - if (currentIterationsLeft > 0) { - newIterationsLeft = currentIterationsLeft - 1 - newLoopIterationsLeft.set(label, newIterationsLeft); - } - if (newIterationsLeft > 0) { - // Look for startLoop blocks - for (let i = newProgramCounter; i > -1; i--) { - const block = this.program[i]; - if (block.block === 'startLoop') { - // Check if the startLoop has same label as the endLoop - if (block.label != null && block.label === label) { - // The startLoop block has the same label - // as the endLoop block: we have found the - // corresponding startLoop block - if (advancePastEmptyLoopEntirely && i === newProgramCounter - 1) { - newIterationsLeft = 0 - newLoopIterationsLeft.set(label, newIterationsLeft); - newProgramCounter += 1; - break; - } else { - // Set the newProgramCounter to the start of the loop - newProgramCounter = i; - break; - } + const currentIterationsLeft = newLoopIterationsLeft.get(label); + if (currentIterationsLeft != null) { + // If the number of iterations left for the loop is > 0, + // decrement it + let newIterationsLeft = currentIterationsLeft; + if (currentIterationsLeft > 0) { + newIterationsLeft = currentIterationsLeft - 1 + newLoopIterationsLeft.set(label, newIterationsLeft); + } + if (newIterationsLeft > 0) { + // Look for startLoop blocks + for (let i = newProgramCounter; i > -1; i--) { + const block = this.program[i]; + if (block.block === 'startLoop') { + // Check if the startLoop has same label as the endLoop + if (block.label === label) { + // The startLoop block has the same label + // as the endLoop block: we have found the + // corresponding startLoop block + if (advancePastEmptyLoopEntirely && i === newProgramCounter - 1) { + newIterationsLeft = 0 + newLoopIterationsLeft.set(label, newIterationsLeft); + newProgramCounter += 1; + break; } else { - // When the startLoop block has a different - // label than the endLoop block, we have - // found a nested loop: - // reset its iterationsLeft - const nestedLoopLabel = this.program[i].label; - const nestLoopIterations = this.program[i].iterations; - if (nestedLoopLabel != null && nestLoopIterations != null) { - newLoopIterationsLeft.set(nestedLoopLabel, nestLoopIterations); - } + // Set the newProgramCounter to the start of the loop + newProgramCounter = i; + break; } + } else { + // When the startLoop block has a different + // label than the endLoop block, we have + // found a nested loop: + // reset its iterationsLeft + const nestedLoopLabel = block.label; + const nestLoopIterations = block.iterations; + newLoopIterationsLeft.set(nestedLoopLabel, nestLoopIterations); } } - } else { - // When there's no more iterations left, - // increment the newProgramCounter - newProgramCounter += 1; } } else { - // Iterations left is missing for the loop, we can't - // process it - break; + // When there's no more iterations left, + // increment the newProgramCounter + newProgramCounter += 1; } + } else { + // Iterations left is missing for the loop, we can't + // process it + break; } } return this.updateProgramCounterAndLoopIterationsLeft( @@ -288,12 +281,12 @@ export default class ProgramSequence { loopCounter = 1; } const loopLabel = generateLoopLabel(loopCounter); - const startLoopObject = { + const startLoopObject: StartLoopProgramBlock = { block: 'startLoop', iterations: newLoopNumberOfIterations, label: loopLabel }; - const endLoopObject = { + const endLoopObject: EndLoopProgramBlock = { block: 'endLoop', label: loopLabel }; @@ -328,12 +321,12 @@ export default class ProgramSequence { loopCounter = 1; } const loopLabel = generateLoopLabel(loopCounter); - const startLoopObject = { + const startLoopObject: StartLoopProgramBlock = { block: 'startLoop', iterations: newLoopNumberOfIterations, label: loopLabel }; - const endLoopObject = { + const endLoopObject: EndLoopProgramBlock = { block: 'endLoop', label: loopLabel }; @@ -425,10 +418,10 @@ export default class ProgramSequence { if (indexFrom < 0 || indexFrom >= programLastIndex) { return true; } - const { block, label } = this.program[indexFrom]; - if (block === 'startLoop') { + const block = this.program[indexFrom]; + if (block.block === 'startLoop') { const lastProgramStep = this.program[programLastIndex]; - if (lastProgramStep.block === 'endLoop' && lastProgramStep.label === label) { + if (lastProgramStep.block === 'endLoop' && lastProgramStep.label === block.label) { return true; } } @@ -442,10 +435,10 @@ export default class ProgramSequence { if (indexFrom <= 0 || indexFrom >= this.program.length) { return true; } - const { block, label } = this.program[indexFrom]; - if (block === 'endLoop') { + const block = this.program[indexFrom]; + if (block.block === 'endLoop') { const firstProgramStep = this.program[0]; - if (firstProgramStep.block === 'startLoop' && firstProgramStep.label === label) { + if (firstProgramStep.block === 'startLoop' && firstProgramStep.label === block.label) { return true; } } @@ -509,7 +502,7 @@ export default class ProgramSequence { usesAction(action: CommandName): boolean { for (let index = 0; index < this.program.length; index++) { const stepAction = this.program[index].block; - if (stepAction === action || (action === "loop" && (stepAction === "startLoop" || stepAction === "endLoop")) ) { + if (stepAction === action || (action === 'loop' && (stepAction === 'startLoop' || stepAction === 'endLoop')) ) { return true; } } @@ -520,9 +513,9 @@ export default class ProgramSequence { initiateProgramRun(): ProgramSequence { const loopIterationsLeft = new Map(); for (let i = 0; i < this.program.length; i++) { - const { block, label, iterations } = this.program[i]; - if (block === 'startLoop' && label != null && iterations != null) { - loopIterationsLeft.set(label, iterations); + const block = this.program[i]; + if (block.block === 'startLoop') { + loopIterationsLeft.set(block.label, block.iterations); } } return new ProgramSequence(this.program, 0, this.loopCounter, loopIterationsLeft); diff --git a/src/ProgramSequence.test.js b/src/ProgramSequence.test.js index 37d8dde7..a9e5fd44 100644 --- a/src/ProgramSequence.test.js +++ b/src/ProgramSequence.test.js @@ -1,5 +1,6 @@ // @flow +import ProgramBlockCache from './ProgramBlockCache'; import ProgramSequence from './ProgramSequence'; import type { CommandName, Program } from './types'; @@ -143,63 +144,39 @@ test('calculateCachedLoopData returns a program with additional loop data', () = block: 'startLoop', iterations: 2, label: 'B', - cache: new Map([ - ['containingLoopPosition', 1], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'forward3', - cache: new Map([ - ['containingLoopPosition', 1], - ['containingLoopLabel', 'B'] - ]) + cache: new ProgramBlockCache('B', 1) }, { block: 'startLoop', iterations: 1, label: 'C', - cache: new Map([ - ['containingLoopPosition', 2], - ['containingLoopLabel', 'B'] - ]) + cache: new ProgramBlockCache('B', 2) }, { block: 'forward1', - cache: new Map([ - ['containingLoopPosition', 1], - ['containingLoopLabel', 'C'] - ]) + cache: new ProgramBlockCache('C', 1) }, { block: 'forward2', - cache: new Map([ - ['containingLoopPosition', 2], - ['containingLoopLabel', 'C'] - ]) + cache: new ProgramBlockCache('C', 2) }, { block: 'endLoop', label: 'C', - cache: new Map([ - ['containingLoopPosition', 5], - ['containingLoopLabel', 'B'] - ]) + cache: new ProgramBlockCache('B', 5) }, { block: 'forward3', - cache: new Map([ - ['containingLoopPosition', 6], - ['containingLoopLabel', 'B'] - ]) + cache: new ProgramBlockCache('B', 6) }, { block: 'endLoop', label: 'B', - cache: new Map([ - ['containingLoopPosition', 8], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 8) }, { block: 'endLoop', @@ -220,10 +197,7 @@ test('calculateCachedLoopData replaces existing cached loop data and removes cac }, { block: 'forward1', - cache: new Map([ - ['containingLoopPosition', 2], - ['containingLoopLabel', 'B'] - ]) + cache: new ProgramBlockCache('B', 2) }, { block: 'endLoop', @@ -231,10 +205,7 @@ test('calculateCachedLoopData replaces existing cached loop data and removes cac }, { block: 'forward2', - cache: new Map([ - ['containingLoopPosition', 2], - ['containingLoopLabel', 'B'] - ]) + cache: new ProgramBlockCache('B', 2) } ]; @@ -246,10 +217,7 @@ test('calculateCachedLoopData replaces existing cached loop data and removes cac }, { block: 'forward1', - cache: new Map([ - ['containingLoopPosition', 1], - ['containingLoopLabel', 'A'] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', @@ -314,7 +282,7 @@ test.each(([ }, { program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'endLoop', label: 'A' } ], programCounter: 0, @@ -325,7 +293,7 @@ test.each(([ }, { program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'endLoop', label: 'A' } ], programCounter: 0, @@ -336,7 +304,7 @@ test.each(([ }, { program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'endLoop', label: 'A' } ], programCounter: 0, @@ -347,7 +315,7 @@ test.each(([ }, { program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1' }, { block: 'endLoop', label: 'A' } ], @@ -359,7 +327,7 @@ test.each(([ }, { program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1' }, { block: 'endLoop', label: 'A' } ], @@ -371,7 +339,7 @@ test.each(([ }, { program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1' }, { block: 'endLoop', label: 'A' } ], @@ -383,7 +351,7 @@ test.each(([ }, { program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1' }, { block: 'endLoop', label: 'A' } ], @@ -1161,18 +1129,12 @@ test.each(([ block: 'startLoop', iterations: 1, label: 'B', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ]), + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', label: 'B', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 2] - ]), + cache: new ProgramBlockCache('A', 2) }, { block: 'endLoop', @@ -1341,26 +1303,20 @@ test.each(([ // Move a block into a loop program: [ { block: 'forward1' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward2' }, { block: 'endLoop', label: 'A' } ], indexFrom: 0, expectedProgram: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'forward2', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 2] - ]) + cache: new ProgramBlockCache('A', 2) }, { block: 'endLoop', label: 'A' } ] @@ -1369,14 +1325,14 @@ test.each(([ // Move a block out of a loop program: [ { block: 'forward1' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward2' }, { block: 'endLoop', label: 'A' } ], indexFrom: 2, expectedProgram: [ { block: 'forward1' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'endLoop', label: 'A' }, { block: 'forward2' } ] @@ -1384,7 +1340,7 @@ test.each(([ { // Move a loop using startLoop program: [ - { block: 'startLoop', label: 'A'}, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1' }, { block: 'endLoop', label: 'A' }, { block: 'forward2' } @@ -1392,13 +1348,10 @@ test.each(([ indexFrom: 0, expectedProgram: [ { block: 'forward2' }, - { block: 'startLoop', label: 'A'}, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', label: 'A' } ] @@ -1406,7 +1359,7 @@ test.each(([ { // Move a loop using endLoop program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1' }, { block: 'endLoop', label: 'A' }, { block: 'forward2' } @@ -1414,13 +1367,10 @@ test.each(([ indexFrom: 2, expectedProgram: [ { block: 'forward2' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', label: 'A' } ] @@ -1428,37 +1378,29 @@ test.each(([ { // Move a loop into another loop program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1' }, { block: 'endLoop', label: 'A' }, - { block: 'startLoop', label: 'B' }, + { block: 'startLoop', label: 'B', iterations: 20 }, { block: 'endLoop', label: 'B' } ], indexFrom: 0, expectedProgram: [ - { block: 'startLoop', label: 'B' }, + { block: 'startLoop', label: 'B', iterations: 20 }, { block: 'startLoop', label: 'A', - cache: new Map([ - ['containingLoopLabel', 'B'], - ['containingLoopPosition', 1] - ]) + iterations: 10, + cache: new ProgramBlockCache('B', 1) }, { block: 'forward1', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', label: 'A', - cache: new Map([ - ['containingLoopLabel', 'B'], - ['containingLoopPosition', 3] - ]) + cache: new ProgramBlockCache('B', 3) }, { block: 'endLoop', label: 'B' } ] @@ -1466,23 +1408,20 @@ test.each(([ { // Move a loop out of another loop program: [ - { block: 'startLoop', label: 'A' }, - { block: 'startLoop', label: 'B' }, + { block: 'startLoop', label: 'A', iterations: 10 }, + { block: 'startLoop', label: 'B', iterations: 20 }, { block: 'forward1' }, { block: 'endLoop', label: 'B' }, { block: 'endLoop', label: 'A' } ], indexFrom: 1, expectedProgram: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'endLoop', label: 'A' }, - { block: 'startLoop', label: 'B' }, + { block: 'startLoop', label: 'B' , iterations: 20 }, { block: 'forward1', - cache: new Map([ - ['containingLoopLabel', 'B'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('B', 1) }, { block: 'endLoop', label: 'B' } ] @@ -1521,27 +1460,21 @@ test.each(([ { // Move a block into a loop program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1' }, { block: 'endLoop', label: 'A' }, { block: 'forward2' } ], indexFrom: 3, expectedProgram: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward1', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'forward2', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 2] - ]) + cache: new ProgramBlockCache('A', 2) }, { block: 'endLoop', label: 'A' } ] @@ -1550,7 +1483,7 @@ test.each(([ // Move a block out of a loop program: [ { block: 'forward1' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward2' }, { block: 'endLoop', label: 'A' } ], @@ -1558,7 +1491,7 @@ test.each(([ expectedProgram: [ { block: 'forward1' }, { block: 'forward2' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'endLoop', label: 'A' } ] }, @@ -1566,19 +1499,16 @@ test.each(([ // Move a loop using startLoop program: [ { block: 'forward1' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward2' }, { block: 'endLoop', label: 'A' } ], indexFrom: 1, expectedProgram: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward2', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', label: 'A' }, { block: 'forward1' } @@ -1588,18 +1518,16 @@ test.each(([ // Move a loop using endLoop program: [ { block: 'forward1' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward2' }, { block: 'endLoop', label: 'A' } ], indexFrom: 3, expectedProgram: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'forward2', - cache: new Map([ - ['containingLoopLabel', 'A'], ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('A', 1) }, { block: 'endLoop', label: 'A' }, { block: 'forward1' } @@ -1608,37 +1536,33 @@ test.each(([ { // Move a loop into another loop program: [ - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'endLoop', label: 'A' }, - { block: 'startLoop', label: 'B' }, + { block: 'startLoop', label: 'B', iterations: 20 }, { block: 'forward1' }, { block: 'endLoop', label: 'B' } ], indexFrom: 2, expectedProgram: [ - { block: 'startLoop', label: 'A' }, + { + block: 'startLoop', + label: 'A', + iterations: 10 + }, { block: 'startLoop', label: 'B', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 1] - ]) + iterations: 20, + cache: new ProgramBlockCache('A', 1) }, { block: 'forward1', - cache: new Map([ - ['containingLoopLabel', 'B'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('B', 1) }, { block: 'endLoop', label: 'B', - cache: new Map([ - ['containingLoopLabel', 'A'], - ['containingLoopPosition', 3] - ]) + cache: new ProgramBlockCache('A', 3) }, { block: 'endLoop', label: 'A' } ] @@ -1646,24 +1570,21 @@ test.each(([ { // Move a loop out of another loop program: [ - { block: 'startLoop', label: 'A' }, - { block: 'startLoop', label: 'B' }, + { block: 'startLoop', label: 'A', iterations: 10 }, + { block: 'startLoop', label: 'B', iterations: 20 }, { block: 'forward1' }, { block: 'endLoop', label: 'B' }, { block: 'endLoop', label: 'A' } ], indexFrom: 1, expectedProgram: [ - { block: 'startLoop', label: 'B' }, + { block: 'startLoop', label: 'B', iterations: 20 }, { block: 'forward1', - cache: new Map([ - ['containingLoopLabel', 'B'], - ['containingLoopPosition', 1] - ]) + cache: new ProgramBlockCache('B', 1) }, { block: 'endLoop', label: 'B' }, - { block: 'startLoop', label: 'A' }, + { block: 'startLoop', label: 'A', iterations: 10 }, { block: 'endLoop', label: 'A' } ] } diff --git a/src/ProgramSerializer.js b/src/ProgramSerializer.js index 1ea84e40..e5774ac4 100644 --- a/src/ProgramSerializer.js +++ b/src/ProgramSerializer.js @@ -13,9 +13,8 @@ export default class ProgramSerializer { serialize(program: Program): string { let programText = ''; - for (let i=0; i; +export type MovementProgramBlock = { + block: MovementBlockName, + cache?: ProgramBlockCache; +}; -export type ProgramBlock = { - block: BlockName, - iterations?: number, - label?: string, +export type StartLoopProgramBlock = { + block: 'startLoop', + label: string, + iterations: number, cache?: ProgramBlockCache; }; +export type EndLoopProgramBlock = { + block: 'endLoop', + label: string, + cache?: ProgramBlockCache; +}; + +export type ProgramBlock = MovementProgramBlock | StartLoopProgramBlock | EndLoopProgramBlock; + export type Program = Array; export type PathSegment = {