From 7f8a0981f14a9ed3e400040abb15e8541cd90fb3 Mon Sep 17 00:00:00 2001 From: Mingze Xiao Date: Tue, 8 Sep 2020 17:13:33 -0700 Subject: [PATCH 1/7] feat(annotations): Handle annotations staged change event --- src/lib/AnnotationControls.ts | 21 ++++------ src/lib/AnnotationFSM.ts | 63 ++++++++++++++++++++++++++++ src/lib/viewers/BaseViewer.js | 38 ++++++++++------- src/lib/viewers/doc/DocBaseViewer.js | 3 +- 4 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 src/lib/AnnotationFSM.ts diff --git a/src/lib/AnnotationControls.ts b/src/lib/AnnotationControls.ts index d22f6a123..8010eaba6 100644 --- a/src/lib/AnnotationControls.ts +++ b/src/lib/AnnotationControls.ts @@ -16,12 +16,11 @@ export enum AnnotationMode { NONE = 'none', REGION = 'region', } -export type ClickHandler = ({ event }: { event: MouseEvent }) => void; +export type ClickHandler = ({ event, mode }: { event: MouseEvent; mode: AnnotationMode }) => void; export type Options = { fileId: string; + onClick?: ClickHandler; onEscape?: () => void; - onHighlightClick?: ClickHandler; - onRegionClick?: ClickHandler; showHighlightText: boolean; }; @@ -92,6 +91,8 @@ export default class AnnotationControls { this.hasInit = false; } + public getMode = (): AnnotationMode => this.currentMode; + /** * Deactivate current control button */ @@ -167,7 +168,7 @@ export default class AnnotationControls { this.updateButton(mode); } - onClick({ event }); + onClick({ event, mode: this.currentMode }); }; /** @@ -210,22 +211,16 @@ export default class AnnotationControls { /** * Initialize the annotation controls with options. */ - public init({ - fileId, - onEscape = noop, - onRegionClick = noop, - onHighlightClick = noop, - showHighlightText = false, - }: Options): void { + public init({ fileId, onEscape = noop, onClick = noop, showHighlightText = false }: Options): void { if (this.hasInit) { return; } const groupElement = this.controls.addGroup(CLASS_ANNOTATIONS_GROUP); groupElement.setAttribute('data-resin-feature', 'annotations'); - this.addButton(AnnotationMode.REGION, onRegionClick, groupElement, fileId); + this.addButton(AnnotationMode.REGION, onClick, groupElement, fileId); if (showHighlightText) { - this.addButton(AnnotationMode.HIGHLIGHT, onHighlightClick, groupElement, fileId); + this.addButton(AnnotationMode.HIGHLIGHT, onClick, groupElement, fileId); } this.onEscape = onEscape; diff --git a/src/lib/AnnotationFSM.ts b/src/lib/AnnotationFSM.ts new file mode 100644 index 000000000..07fc9fc24 --- /dev/null +++ b/src/lib/AnnotationFSM.ts @@ -0,0 +1,63 @@ +import { AnnotationMode } from './AnnotationControls'; + +export enum AnnotationState { + HIGHLIGHT = 'highlight', + HIGHLIGHT_TEMP = 'highlight_temp', + NONE = 'none', + REGION = 'region', + REGION_TEMP = 'region_temp', +} + +export enum AnnotationInput { + CANCEL = 'cancel', + CLICK = 'click', + CREATE = 'create', + SUCCESS = 'success', + UPDATE = 'update', +} + +export const modeStateMap = { + [AnnotationMode.HIGHLIGHT]: AnnotationState.HIGHLIGHT, + [AnnotationMode.NONE]: AnnotationState.NONE, + [AnnotationMode.REGION]: AnnotationState.REGION, +}; + +export const stateModeMap = { + [AnnotationState.HIGHLIGHT]: AnnotationMode.HIGHLIGHT, + [AnnotationState.HIGHLIGHT_TEMP]: AnnotationMode.HIGHLIGHT, + [AnnotationState.NONE]: AnnotationMode.NONE, + [AnnotationState.REGION]: AnnotationMode.REGION, + [AnnotationState.REGION_TEMP]: AnnotationMode.REGION, +}; + +export default class AnnotationFSM { + private currentState = AnnotationState.NONE; + + public send = (input: AnnotationInput, mode: AnnotationMode): AnnotationMode => { + if (input === AnnotationInput.CLICK) { + this.currentState = modeStateMap[mode]; + return mode; + } + + switch (this.currentState) { + case AnnotationState.NONE: + if (input === AnnotationInput.CREATE) { + this.currentState = + mode === AnnotationMode.HIGHLIGHT + ? AnnotationState.HIGHLIGHT_TEMP + : AnnotationState.REGION_TEMP; + return mode; + } + break; + case AnnotationState.HIGHLIGHT_TEMP: + case AnnotationState.REGION_TEMP: + if (input === AnnotationInput.CANCEL || input === AnnotationInput.SUCCESS) { + this.currentState = AnnotationState.NONE; + } + break; + default: + } + + return stateModeMap[this.currentState]; + }; +} diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index f27fa85d9..a1a63c5f3 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -35,6 +35,7 @@ import { EXCLUDED_EXTENSIONS } from '../extensions'; import { getIconFromExtension, getIconFromName } from '../icons/icons'; import { VIEWER_EVENT, ERROR_CODE, LOAD_METRIC, DOWNLOAD_REACHABILITY_METRICS } from '../events'; import { AnnotationMode } from '../AnnotationControls'; +import AnnotationFSM, { AnnotationInput } from '../AnnotationFSM'; import PreviewError from '../PreviewError'; import Timer from '../Timer'; @@ -140,6 +141,8 @@ class BaseViewer extends EventEmitter { this.emittedMetrics = {}; + this.annotationFSM = new AnnotationFSM(); + // Bind context for callbacks this.resetLoadTimeout = this.resetLoadTimeout.bind(this); this.preventDefault = this.preventDefault.bind(this); @@ -151,11 +154,11 @@ class BaseViewer extends EventEmitter { this.mobileZoomEndHandler = this.mobileZoomEndHandler.bind(this); this.handleAnnotatorEvents = this.handleAnnotatorEvents.bind(this); this.handleAnnotationCreateEvent = this.handleAnnotationCreateEvent.bind(this); + this.handleAnnotationControlsClick = this.handleAnnotationControlsClick.bind(this); this.handleAnnotationControlsEscape = this.handleAnnotationControlsEscape.bind(this); + this.handleAnnotationStagedChangeEvent = this.handleAnnotationStagedChangeEvent.bind(this); this.handleFullscreenEnter = this.handleFullscreenEnter.bind(this); this.handleFullscreenExit = this.handleFullscreenExit.bind(this); - this.handleHighlightClick = this.handleHighlightClick.bind(this); - this.handleRegionClick = this.handleRegionClick.bind(this); this.createAnnotator = this.createAnnotator.bind(this); this.viewerLoadHandler = this.viewerLoadHandler.bind(this); this.initAnnotations = this.initAnnotations.bind(this); @@ -1015,6 +1018,7 @@ class BaseViewer extends EventEmitter { if (this.areNewAnnotationsEnabled() && this.annotationControls) { this.annotator.addListener('annotations_create', this.handleAnnotationCreateEvent); + this.annotator.addListener('creator_staged_change', this.handleAnnotationStagedChangeEvent); } } @@ -1069,23 +1073,15 @@ class BaseViewer extends EventEmitter { } /** - * Handler for annotation toolbar highlight text button click event. - * - * @private - * @return {void} - */ - handleHighlightClick() { - this.annotator.toggleAnnotationMode(AnnotationMode.HIGHLIGHT); - } - - /** - * Handler for annotation toolbar region comment button click event. + * Handler for annotation controls button click event. * * @private + * @param {AnnotationMode} mode one of annotation modes * @return {void} */ - handleRegionClick() { - this.annotator.toggleAnnotationMode(AnnotationMode.REGION); + handleAnnotationControlsClick({ mode }) { + this.annotator.toggleAnnotationMode(mode); + this.annotationFSM.send(AnnotationInput.CLICK, mode); } /** @@ -1257,9 +1253,21 @@ class BaseViewer extends EventEmitter { // we remain in create mode if (status === 'success') { this.annotator.emit('annotations_active_set', id); + + if (this.annotationControls) { + this.annotationControls.setMode(this.annotationFSM.send(AnnotationInput.SUCCESS, AnnotationMode.NONE)); + } } } + handleAnnotationStagedChangeEvent({ status, type }) { + if (!this.annotationControls) { + return; + } + + this.annotationControls.setMode(this.annotationFSM.send(status, type)); + } + /** * Creates combined options to give to the annotator * diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index 3f1bd3474..a2d93981d 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -1108,8 +1108,7 @@ class DocBaseViewer extends BaseViewer { this.annotationControls.init({ fileId: this.options.file.id, onEscape: this.handleAnnotationControlsEscape, - onHighlightClick: this.handleHighlightClick, - onRegionClick: this.handleRegionClick, + onClick: this.handleAnnotationControlsClick, showHighlightText: this.options.showAnnotationsHighlightText, }); } From d6f6f989c44b57269767b0d87e2f6ca612fed054 Mon Sep 17 00:00:00 2001 From: Mingze Xiao Date: Wed, 9 Sep 2020 09:18:17 -0700 Subject: [PATCH 2/7] feat(annotations): AnnotationControls dumber, FSM smarter --- src/lib/AnnotationControls.ts | 21 ++----------------- ...otationFSM.ts => AnnotationControlsFSM.ts} | 9 ++++---- src/lib/viewers/BaseViewer.js | 15 +++++++------ 3 files changed, 15 insertions(+), 30 deletions(-) rename src/lib/{AnnotationFSM.ts => AnnotationControlsFSM.ts} (85%) diff --git a/src/lib/AnnotationControls.ts b/src/lib/AnnotationControls.ts index 8010eaba6..0faff5dfd 100644 --- a/src/lib/AnnotationControls.ts +++ b/src/lib/AnnotationControls.ts @@ -91,8 +91,6 @@ export default class AnnotationControls { this.hasInit = false; } - public getMode = (): AnnotationMode => this.currentMode; - /** * Deactivate current control button */ @@ -156,21 +154,6 @@ export default class AnnotationControls { this.updateButton(mode); } - /** - * Annotation control button click handler - */ - private handleClick = (onClick: ClickHandler, mode: AnnotationMode) => (event: MouseEvent): void => { - const prevMode = this.currentMode; - this.resetControls(); - - if (prevMode !== mode) { - this.currentMode = mode as AnnotationMode; - this.updateButton(mode); - } - - onClick({ event, mode: this.currentMode }); - }; - /** * Escape key handler, reset all control buttons, * and stop propagation to prevent preview modal from exiting @@ -187,7 +170,7 @@ export default class AnnotationControls { event.stopPropagation(); }; - private addButton = (mode: AnnotationMode, handler: ClickHandler, parent: HTMLElement, fileId: string): void => { + private addButton = (mode: AnnotationMode, onClick: ClickHandler, parent: HTMLElement, fileId: string): void => { const buttonProps = buttonPropsMap[mode]; if (!buttonProps) { @@ -196,7 +179,7 @@ export default class AnnotationControls { const buttonElement = this.controls.add( buttonProps.text, - this.handleClick(handler, mode), + (event: MouseEvent) => onClick({ event, mode }), buttonProps.classname, buttonProps.icon, 'button', diff --git a/src/lib/AnnotationFSM.ts b/src/lib/AnnotationControlsFSM.ts similarity index 85% rename from src/lib/AnnotationFSM.ts rename to src/lib/AnnotationControlsFSM.ts index 07fc9fc24..0b1fa9b19 100644 --- a/src/lib/AnnotationFSM.ts +++ b/src/lib/AnnotationControlsFSM.ts @@ -30,13 +30,13 @@ export const stateModeMap = { [AnnotationState.REGION_TEMP]: AnnotationMode.REGION, }; -export default class AnnotationFSM { +export default class AnnotationControlsFSM { private currentState = AnnotationState.NONE; - public send = (input: AnnotationInput, mode: AnnotationMode): AnnotationMode => { + public transition = (input: AnnotationInput, mode: AnnotationMode): AnnotationMode => { if (input === AnnotationInput.CLICK) { - this.currentState = modeStateMap[mode]; - return mode; + this.currentState = mode === stateModeMap[this.currentState] ? AnnotationState.NONE : modeStateMap[mode]; + return stateModeMap[this.currentState]; } switch (this.currentState) { @@ -46,7 +46,6 @@ export default class AnnotationFSM { mode === AnnotationMode.HIGHLIGHT ? AnnotationState.HIGHLIGHT_TEMP : AnnotationState.REGION_TEMP; - return mode; } break; case AnnotationState.HIGHLIGHT_TEMP: diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index a1a63c5f3..85826c31d 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -35,7 +35,7 @@ import { EXCLUDED_EXTENSIONS } from '../extensions'; import { getIconFromExtension, getIconFromName } from '../icons/icons'; import { VIEWER_EVENT, ERROR_CODE, LOAD_METRIC, DOWNLOAD_REACHABILITY_METRICS } from '../events'; import { AnnotationMode } from '../AnnotationControls'; -import AnnotationFSM, { AnnotationInput } from '../AnnotationFSM'; +import AnnotationControlsFSM, { AnnotationInput } from '../AnnotationControlsFSM'; import PreviewError from '../PreviewError'; import Timer from '../Timer'; @@ -141,7 +141,7 @@ class BaseViewer extends EventEmitter { this.emittedMetrics = {}; - this.annotationFSM = new AnnotationFSM(); + this.annotationControlsFSM = new AnnotationControlsFSM(); // Bind context for callbacks this.resetLoadTimeout = this.resetLoadTimeout.bind(this); @@ -1080,8 +1080,9 @@ class BaseViewer extends EventEmitter { * @return {void} */ handleAnnotationControlsClick({ mode }) { - this.annotator.toggleAnnotationMode(mode); - this.annotationFSM.send(AnnotationInput.CLICK, mode); + const nextMode = this.annotationControlsFSM.transition(AnnotationInput.CLICK, mode); + this.annotator.toggleAnnotationMode(nextMode); + this.annotationControls.setMode(nextMode); } /** @@ -1255,7 +1256,9 @@ class BaseViewer extends EventEmitter { this.annotator.emit('annotations_active_set', id); if (this.annotationControls) { - this.annotationControls.setMode(this.annotationFSM.send(AnnotationInput.SUCCESS, AnnotationMode.NONE)); + this.annotationControls.setMode( + this.annotationControlsFSM.transition(AnnotationInput.SUCCESS, AnnotationMode.NONE), + ); } } } @@ -1265,7 +1268,7 @@ class BaseViewer extends EventEmitter { return; } - this.annotationControls.setMode(this.annotationFSM.send(status, type)); + this.annotationControls.setMode(this.annotationControlsFSM.transition(status, type)); } /** From 61187930c57039e9f84dcc621b541d4d209f88d2 Mon Sep 17 00:00:00 2001 From: Mingze Xiao Date: Wed, 9 Sep 2020 10:04:51 -0700 Subject: [PATCH 3/7] feat(annotations): Address comments --- src/lib/AnnotationControlsFSM.ts | 13 ++++++++----- src/lib/viewers/BaseViewer.js | 4 +--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/lib/AnnotationControlsFSM.ts b/src/lib/AnnotationControlsFSM.ts index 0b1fa9b19..d22d9174f 100644 --- a/src/lib/AnnotationControlsFSM.ts +++ b/src/lib/AnnotationControlsFSM.ts @@ -22,6 +22,12 @@ export const modeStateMap = { [AnnotationMode.REGION]: AnnotationState.REGION, }; +export const modeTempStateMap = { + [AnnotationMode.HIGHLIGHT]: AnnotationState.HIGHLIGHT_TEMP, + [AnnotationMode.NONE]: AnnotationState.NONE, + [AnnotationMode.REGION]: AnnotationState.REGION_TEMP, +}; + export const stateModeMap = { [AnnotationState.HIGHLIGHT]: AnnotationMode.HIGHLIGHT, [AnnotationState.HIGHLIGHT_TEMP]: AnnotationMode.HIGHLIGHT, @@ -33,7 +39,7 @@ export const stateModeMap = { export default class AnnotationControlsFSM { private currentState = AnnotationState.NONE; - public transition = (input: AnnotationInput, mode: AnnotationMode): AnnotationMode => { + public transition = (input: AnnotationInput, mode: AnnotationMode = AnnotationMode.NONE): AnnotationMode => { if (input === AnnotationInput.CLICK) { this.currentState = mode === stateModeMap[this.currentState] ? AnnotationState.NONE : modeStateMap[mode]; return stateModeMap[this.currentState]; @@ -42,10 +48,7 @@ export default class AnnotationControlsFSM { switch (this.currentState) { case AnnotationState.NONE: if (input === AnnotationInput.CREATE) { - this.currentState = - mode === AnnotationMode.HIGHLIGHT - ? AnnotationState.HIGHLIGHT_TEMP - : AnnotationState.REGION_TEMP; + this.currentState = modeTempStateMap[mode] || AnnotationState.NONE; } break; case AnnotationState.HIGHLIGHT_TEMP: diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index 85826c31d..fcac0921e 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -1256,9 +1256,7 @@ class BaseViewer extends EventEmitter { this.annotator.emit('annotations_active_set', id); if (this.annotationControls) { - this.annotationControls.setMode( - this.annotationControlsFSM.transition(AnnotationInput.SUCCESS, AnnotationMode.NONE), - ); + this.annotationControls.setMode(this.annotationControlsFSM.transition(AnnotationInput.SUCCESS)); } } } From fa98114ca4db76ae0a4181d2fc8506da98a1f252 Mon Sep 17 00:00:00 2001 From: Mingze Xiao Date: Wed, 9 Sep 2020 11:03:17 -0700 Subject: [PATCH 4/7] feat(annotations): Add FSM unit tests --- src/lib/AnnotationControlsFSM.ts | 6 ++ .../__tests__/AnnotationControlsFSM-test.js | 90 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/lib/__tests__/AnnotationControlsFSM-test.js diff --git a/src/lib/AnnotationControlsFSM.ts b/src/lib/AnnotationControlsFSM.ts index d22d9174f..1445cec4b 100644 --- a/src/lib/AnnotationControlsFSM.ts +++ b/src/lib/AnnotationControlsFSM.ts @@ -39,6 +39,12 @@ export const stateModeMap = { export default class AnnotationControlsFSM { private currentState = AnnotationState.NONE; + public getState = (): AnnotationState => this.currentState; + + public setState = (state: AnnotationState): void => { + this.currentState = state; + }; + public transition = (input: AnnotationInput, mode: AnnotationMode = AnnotationMode.NONE): AnnotationMode => { if (input === AnnotationInput.CLICK) { this.currentState = mode === stateModeMap[this.currentState] ? AnnotationState.NONE : modeStateMap[mode]; diff --git a/src/lib/__tests__/AnnotationControlsFSM-test.js b/src/lib/__tests__/AnnotationControlsFSM-test.js new file mode 100644 index 000000000..491e3a877 --- /dev/null +++ b/src/lib/__tests__/AnnotationControlsFSM-test.js @@ -0,0 +1,90 @@ +import AnnotationControlsFSM, { AnnotationInput, AnnotationState } from '../AnnotationControlsFSM'; +import { AnnotationMode } from '../AnnotationControls'; + +let annotationControlsFSM; + +describe('lib/AnnotationControlsFSM', () => { + beforeEach(() => { + annotationControlsFSM = new AnnotationControlsFSM(); + }); + + [ + { + state: AnnotationState.NONE, + input: AnnotationInput.CLICK, + mode: AnnotationMode.HIGHLIGHT, + nextState: AnnotationState.HIGHLIGHT, + output: AnnotationMode.HIGHLIGHT, + }, + { + state: AnnotationState.NONE, + input: AnnotationInput.CLICK, + mode: AnnotationMode.REGION, + nextState: AnnotationState.REGION, + output: AnnotationMode.REGION, + }, + { + state: AnnotationState.NONE, + input: AnnotationInput.CREATE, + mode: AnnotationMode.HIGHLIGHT, + nextState: AnnotationState.HIGHLIGHT_TEMP, + output: AnnotationMode.HIGHLIGHT, + }, + { + state: AnnotationState.NONE, + input: AnnotationInput.CREATE, + mode: AnnotationMode.REGION, + nextState: AnnotationState.REGION_TEMP, + output: AnnotationMode.REGION, + }, + { + state: AnnotationState.HIGHLIGHT, + input: AnnotationInput.CANCEL, + mode: AnnotationMode.HIGHLIGHT, + nextState: AnnotationState.HIGHLIGHT, + output: AnnotationMode.HIGHLIGHT, + }, + { + state: AnnotationState.HIGHLIGHT, + input: AnnotationInput.SUCCESS, + mode: undefined, + nextState: AnnotationState.HIGHLIGHT, + output: AnnotationMode.HIGHLIGHT, + }, + { + state: AnnotationState.HIGHLIGHT_TEMP, + input: AnnotationInput.CLICK, + mode: AnnotationMode.HIGHLIGHT, + nextState: AnnotationState.NONE, + output: AnnotationMode.NONE, + }, + { + state: AnnotationState.HIGHLIGHT_TEMP, + input: AnnotationInput.CLICK, + mode: AnnotationMode.REGION, + nextState: AnnotationState.REGION, + output: AnnotationMode.REGION, + }, + { + state: AnnotationState.HIGHLIGHT_TEMP, + input: AnnotationInput.CANCEL, + mode: AnnotationMode.HIGHLIGHT, + nextState: AnnotationState.NONE, + output: AnnotationMode.NONE, + }, + { + state: AnnotationState.HIGHLIGHT_TEMP, + input: AnnotationInput.SUCCESS, + mode: undefined, + nextState: AnnotationState.NONE, + output: AnnotationMode.NONE, + }, + ].forEach(({ state, input, mode, nextState, output }) => { + it(`should go to state ${nextState} and output ${output}`, () => { + annotationControlsFSM.setState(state); + + expect(annotationControlsFSM.transition(input, mode)).to.equal(output); + expect(annotationControlsFSM.getState()).to.equal(nextState); + }); + }); +}); From d31e134af4072b0b172f4380c248cafa8b127b3c Mon Sep 17 00:00:00 2001 From: Mingze Xiao Date: Wed, 9 Sep 2020 14:14:33 -0700 Subject: [PATCH 5/7] feat(annotations): More unit tests --- src/lib/AnnotationControlsFSM.ts | 10 ++--- src/lib/__tests__/AnnotationControls-test.js | 42 +++---------------- .../__tests__/AnnotationControlsFSM-test.js | 6 +-- src/lib/viewers/__tests__/BaseViewer-test.js | 33 ++++++++++++++- src/lib/viewers/doc/DocBaseViewer.js | 2 +- .../doc/__tests__/DocBaseViewer-test.js | 3 +- src/lib/viewers/image/ImageViewer.js | 2 +- .../image/__tests__/ImageViewer-test.js | 2 +- 8 files changed, 47 insertions(+), 53 deletions(-) diff --git a/src/lib/AnnotationControlsFSM.ts b/src/lib/AnnotationControlsFSM.ts index 1445cec4b..d2657b349 100644 --- a/src/lib/AnnotationControlsFSM.ts +++ b/src/lib/AnnotationControlsFSM.ts @@ -37,13 +37,13 @@ export const stateModeMap = { }; export default class AnnotationControlsFSM { - private currentState = AnnotationState.NONE; + private currentState: AnnotationState; - public getState = (): AnnotationState => this.currentState; + constructor(initialState = AnnotationState.NONE) { + this.currentState = initialState; + } - public setState = (state: AnnotationState): void => { - this.currentState = state; - }; + public getState = (): AnnotationState => this.currentState; public transition = (input: AnnotationInput, mode: AnnotationMode = AnnotationMode.NONE): AnnotationMode => { if (input === AnnotationInput.CLICK) { diff --git a/src/lib/__tests__/AnnotationControls-test.js b/src/lib/__tests__/AnnotationControls-test.js index dc04e4524..69374c3b0 100644 --- a/src/lib/__tests__/AnnotationControls-test.js +++ b/src/lib/__tests__/AnnotationControls-test.js @@ -4,7 +4,6 @@ import AnnotationControls, { AnnotationMode, CLASS_ANNOTATIONS_BUTTON, CLASS_ANNOTATIONS_GROUP, - CLASS_BUTTON_ACTIVE, CLASS_GROUP_HIDE, CLASS_REGION_BUTTON, } from '../AnnotationControls'; @@ -24,8 +23,7 @@ describe('lib/AnnotationControls', () => { fixture.load('__tests__/AnnotationControls-test.html'); stubs.classListAdd = sandbox.stub(); stubs.classListRemove = sandbox.stub(); - stubs.onHighlightClick = sandbox.stub(); - stubs.onRegionClick = sandbox.stub(); + stubs.onClick = sandbox.stub(); stubs.querySelector = sandbox.stub().returns({ classList: { add: stubs.classListAdd, @@ -84,22 +82,22 @@ describe('lib/AnnotationControls', () => { }); it('should only add region button', () => { - annotationControls.init({ fileId: '0', onRegionClick: stubs.onRegionClick }); + annotationControls.init({ fileId: '0', onClick: stubs.onClick }); expect(annotationControls.addButton).to.be.calledOnceWith( AnnotationMode.REGION, - stubs.onRegionClick, + stubs.onClick, sinon.match.any, '0', ); }); it('should add highlight button', () => { - annotationControls.init({ fileId: '0', onHighlightClick: stubs.onHighlightClick, showHighlightText: true }); + annotationControls.init({ fileId: '0', onClick: stubs.onClick, showHighlightText: true }); expect(annotationControls.addButton).to.be.calledWith( AnnotationMode.HIGHLIGHT, - stubs.onHighlightClick, + stubs.onClick, sinon.match.any, '0', ); @@ -166,32 +164,6 @@ describe('lib/AnnotationControls', () => { }); }); - describe('handleClick()', () => { - beforeEach(() => { - stubs.event = sandbox.stub({}); - }); - - it('should activate region button then deactivate', () => { - expect(annotationControls.currentMode).to.equal(AnnotationMode.NONE); - - annotationControls.handleClick(stubs.onRegionClick, AnnotationMode.REGION)(stubs.event); - expect(annotationControls.currentMode).to.equal(AnnotationMode.REGION); - expect(stubs.classListAdd).to.be.calledWith(CLASS_BUTTON_ACTIVE); - - annotationControls.handleClick(stubs.onRegionClick, AnnotationMode.REGION)(stubs.event); - expect(annotationControls.currentMode).to.equal(AnnotationMode.NONE); - expect(stubs.classListRemove).to.be.calledWith(CLASS_BUTTON_ACTIVE); - }); - - it('should call onRegionClick', () => { - annotationControls.handleClick(stubs.onRegionClick, AnnotationMode.REGION)(stubs.event); - - expect(stubs.onRegionClick).to.be.calledWith({ - event: stubs.event, - }); - }); - }); - describe('resetControls()', () => { beforeEach(() => { sandbox.stub(annotationControls, 'updateButton'); @@ -233,9 +205,7 @@ describe('lib/AnnotationControls', () => { stubs.buttonElement = { setAttribute: sandbox.stub(), }; - stubs.clickHandler = sandbox.stub(); - sandbox.stub(annotationControls, 'handleClick').returns(stubs.clickHandler); sandbox.stub(annotationControls.controls, 'add').returns(stubs.buttonElement); }); @@ -250,7 +220,7 @@ describe('lib/AnnotationControls', () => { expect(annotationControls.controls.add).to.be.calledWith( __('region_comment'), - stubs.clickHandler, + sinon.match.func, `${CLASS_ANNOTATIONS_BUTTON} ${CLASS_REGION_BUTTON}`, ICON_REGION_COMMENT, 'button', diff --git a/src/lib/__tests__/AnnotationControlsFSM-test.js b/src/lib/__tests__/AnnotationControlsFSM-test.js index 491e3a877..d123935f6 100644 --- a/src/lib/__tests__/AnnotationControlsFSM-test.js +++ b/src/lib/__tests__/AnnotationControlsFSM-test.js @@ -4,10 +4,6 @@ import { AnnotationMode } from '../AnnotationControls'; let annotationControlsFSM; describe('lib/AnnotationControlsFSM', () => { - beforeEach(() => { - annotationControlsFSM = new AnnotationControlsFSM(); - }); - [ { state: AnnotationState.NONE, @@ -81,7 +77,7 @@ describe('lib/AnnotationControlsFSM', () => { }, ].forEach(({ state, input, mode, nextState, output }) => { it(`should go to state ${nextState} and output ${output}`, () => { - annotationControlsFSM.setState(state); + annotationControlsFSM = new AnnotationControlsFSM(state); expect(annotationControlsFSM.transition(input, mode)).to.equal(output); expect(annotationControlsFSM.getState()).to.equal(nextState); diff --git a/src/lib/viewers/__tests__/BaseViewer-test.js b/src/lib/viewers/__tests__/BaseViewer-test.js index 4ef68d72f..2f3c02128 100644 --- a/src/lib/viewers/__tests__/BaseViewer-test.js +++ b/src/lib/viewers/__tests__/BaseViewer-test.js @@ -1255,6 +1255,10 @@ describe('lib/viewers/BaseViewer', () => { 'annotations_initialized', base.handleAnnotationsInitialized, ); + expect(base.annotator.addListener).to.be.calledWith( + 'creator_staged_change', + base.handleAnnotationStagedChangeEvent, + ); expect(base.emit).to.be.calledWith('annotator', base.annotator); }); @@ -1773,6 +1777,10 @@ describe('lib/viewers/BaseViewer', () => { base.annotator = { emit: sandbox.stub(), }; + base.annotationControls = { + destroy: sandbox.stub(), + setMode: sandbox.stub(), + }; }); const createEvent = status => ({ @@ -1796,6 +1804,19 @@ describe('lib/viewers/BaseViewer', () => { base.handleAnnotationCreateEvent(event); expect(base.annotator.emit).to.be.calledWith('annotations_active_set', '123'); + expect(base.annotationControls.setMode).to.be.calledWith('none'); + }); + }); + + describe('handleAnnotationStagedChangeEvent()', () => { + it('should set mode', () => { + base.annotationControls = { + destroy: sandbox.stub(), + setMode: sandbox.stub(), + }; + base.handleAnnotationStagedChangeEvent({ status: 'create', type: 'highlight' }); + + expect(base.annotationControls.setMode).to.be.calledWith('highlight'); }); }); @@ -1811,15 +1832,23 @@ describe('lib/viewers/BaseViewer', () => { }); }); - describe('handleRegionClick', () => { + describe('handleAnnotationControlsClick', () => { + beforeEach(() => { + base.annotationControls = { + destroy: sandbox.stub(), + setMode: sandbox.stub(), + }; + }); + it('should call toggleAnnotationMode', () => { base.annotator = { toggleAnnotationMode: sandbox.stub(), }; - base.handleRegionClick(); + base.handleAnnotationControlsClick({ mode: 'region' }); expect(base.annotator.toggleAnnotationMode).to.be.calledWith('region'); + expect(base.annotationControls.setMode).to.be.calledWith('region'); }); }); }); diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index a2d93981d..d677a6521 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -1107,8 +1107,8 @@ class DocBaseViewer extends BaseViewer { if (this.areNewAnnotationsEnabled() && this.hasAnnotationCreatePermission()) { this.annotationControls.init({ fileId: this.options.file.id, - onEscape: this.handleAnnotationControlsEscape, onClick: this.handleAnnotationControlsClick, + onEscape: this.handleAnnotationControlsEscape, showHighlightText: this.options.showAnnotationsHighlightText, }); } diff --git a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js index 2f89f861a..46ce17c85 100644 --- a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js +++ b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js @@ -2346,9 +2346,8 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { ); expect(docBase.annotationControls.init).to.be.calledWith({ fileId: docBase.options.file.id, + onClick: docBase.handleAnnotationControlsClick, onEscape: docBase.handleAnnotationControlsEscape, - onHighlightClick: docBase.handleHighlightClick, - onRegionClick: docBase.handleRegionClick, showHighlightText: true, }); }); diff --git a/src/lib/viewers/image/ImageViewer.js b/src/lib/viewers/image/ImageViewer.js index be71c9ec3..6bdf68052 100644 --- a/src/lib/viewers/image/ImageViewer.js +++ b/src/lib/viewers/image/ImageViewer.js @@ -297,8 +297,8 @@ class ImageViewer extends ImageBaseViewer { this.annotationControls = new AnnotationControls(this.controls); this.annotationControls.init({ fileId: this.options.file.id, + onClick: this.handleAnnotationControlsClick, onEscape: this.handleAnnotationControlsEscape, - onRegionClick: this.handleRegionClick, }); } } diff --git a/src/lib/viewers/image/__tests__/ImageViewer-test.js b/src/lib/viewers/image/__tests__/ImageViewer-test.js index 9624e76a0..3adf9a6a2 100644 --- a/src/lib/viewers/image/__tests__/ImageViewer-test.js +++ b/src/lib/viewers/image/__tests__/ImageViewer-test.js @@ -372,8 +372,8 @@ describe('lib/viewers/image/ImageViewer', () => { expect(AnnotationControls.prototype.init).to.be.calledWith({ fileId: image.options.file.id, + onClick: image.handleAnnotationControlsClick, onEscape: image.handleAnnotationControlsEscape, - onRegionClick: image.handleRegionClick, }); }); }); From b786d0517dbbc9e96c8603f514046c2e24d6b4e5 Mon Sep 17 00:00:00 2001 From: Mingze Xiao Date: Wed, 9 Sep 2020 15:17:53 -0700 Subject: [PATCH 6/7] feat(annotations): Add tests for every possible inputs for every state --- .../__tests__/AnnotationControlsFSM-test.js | 204 +++++++++++------- 1 file changed, 126 insertions(+), 78 deletions(-) diff --git a/src/lib/__tests__/AnnotationControlsFSM-test.js b/src/lib/__tests__/AnnotationControlsFSM-test.js index d123935f6..29fa06831 100644 --- a/src/lib/__tests__/AnnotationControlsFSM-test.js +++ b/src/lib/__tests__/AnnotationControlsFSM-test.js @@ -1,86 +1,134 @@ import AnnotationControlsFSM, { AnnotationInput, AnnotationState } from '../AnnotationControlsFSM'; import { AnnotationMode } from '../AnnotationControls'; -let annotationControlsFSM; - describe('lib/AnnotationControlsFSM', () => { - [ - { - state: AnnotationState.NONE, - input: AnnotationInput.CLICK, - mode: AnnotationMode.HIGHLIGHT, - nextState: AnnotationState.HIGHLIGHT, - output: AnnotationMode.HIGHLIGHT, - }, - { - state: AnnotationState.NONE, - input: AnnotationInput.CLICK, - mode: AnnotationMode.REGION, - nextState: AnnotationState.REGION, - output: AnnotationMode.REGION, - }, - { - state: AnnotationState.NONE, - input: AnnotationInput.CREATE, - mode: AnnotationMode.HIGHLIGHT, - nextState: AnnotationState.HIGHLIGHT_TEMP, - output: AnnotationMode.HIGHLIGHT, - }, - { - state: AnnotationState.NONE, - input: AnnotationInput.CREATE, - mode: AnnotationMode.REGION, - nextState: AnnotationState.REGION_TEMP, - output: AnnotationMode.REGION, - }, - { - state: AnnotationState.HIGHLIGHT, - input: AnnotationInput.CANCEL, - mode: AnnotationMode.HIGHLIGHT, - nextState: AnnotationState.HIGHLIGHT, - output: AnnotationMode.HIGHLIGHT, - }, - { - state: AnnotationState.HIGHLIGHT, - input: AnnotationInput.SUCCESS, - mode: undefined, - nextState: AnnotationState.HIGHLIGHT, - output: AnnotationMode.HIGHLIGHT, - }, - { - state: AnnotationState.HIGHLIGHT_TEMP, - input: AnnotationInput.CLICK, - mode: AnnotationMode.HIGHLIGHT, - nextState: AnnotationState.NONE, - output: AnnotationMode.NONE, - }, - { - state: AnnotationState.HIGHLIGHT_TEMP, - input: AnnotationInput.CLICK, - mode: AnnotationMode.REGION, - nextState: AnnotationState.REGION, - output: AnnotationMode.REGION, - }, - { - state: AnnotationState.HIGHLIGHT_TEMP, - input: AnnotationInput.CANCEL, - mode: AnnotationMode.HIGHLIGHT, - nextState: AnnotationState.NONE, - output: AnnotationMode.NONE, - }, - { - state: AnnotationState.HIGHLIGHT_TEMP, - input: AnnotationInput.SUCCESS, - mode: undefined, - nextState: AnnotationState.NONE, - output: AnnotationMode.NONE, - }, - ].forEach(({ state, input, mode, nextState, output }) => { - it(`should go to state ${nextState} and output ${output}`, () => { - annotationControlsFSM = new AnnotationControlsFSM(state); + describe('AnnotationState.NONE', () => { + [ + { + input: AnnotationInput.CLICK, + mode: AnnotationMode.HIGHLIGHT, + nextState: AnnotationState.HIGHLIGHT, + output: AnnotationMode.HIGHLIGHT, + }, + { + input: AnnotationInput.CLICK, + mode: AnnotationMode.REGION, + nextState: AnnotationState.REGION, + output: AnnotationMode.REGION, + }, + { + input: AnnotationInput.CREATE, + mode: AnnotationMode.HIGHLIGHT, + nextState: AnnotationState.HIGHLIGHT_TEMP, + output: AnnotationMode.HIGHLIGHT, + }, + { + input: AnnotationInput.CREATE, + mode: AnnotationMode.REGION, + nextState: AnnotationState.REGION_TEMP, + output: AnnotationMode.REGION, + }, + ].forEach(({ input, mode, nextState, output }) => { + it(`should go to state ${nextState} and output ${output}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(); + + expect(annotationControlsFSM.transition(input, mode)).to.equal(output); + expect(annotationControlsFSM.getState()).to.equal(nextState); + }); + }); + + [AnnotationInput.CANCEL, AnnotationInput.SUCCESS, AnnotationInput.UPDATE].forEach(input => { + it(`should stay in state none if input is ${input}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(); + + expect(annotationControlsFSM.transition(input)).to.equal(AnnotationMode.NONE); + expect(annotationControlsFSM.getState()).to.equal(AnnotationState.NONE); + }); + }); + }); + + describe('AnnotationState.HIGHLIGHT/REGION', () => { + [AnnotationState.HIGHLIGHT, AnnotationState.REGION].forEach(state => { + // non-Click input + [AnnotationInput.CANCEL, AnnotationInput.CREATE, AnnotationInput.SUCCESS, AnnotationInput.SUCCESS].forEach( + input => { + it(`should stay in state ${state}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(state); + + expect(annotationControlsFSM.transition(input)).to.equal(state); + expect(annotationControlsFSM.getState()).to.equal(state); + }); + }, + ); + + // Click input + [AnnotationMode.HIGHLIGHT, AnnotationMode.REGION].forEach(mode => { + it(`should go to state none/${mode} if input is click and mode is ${mode}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(state); + + const output = state === mode ? AnnotationMode.NONE : mode; + + expect(annotationControlsFSM.transition(AnnotationInput.CLICK, mode)).to.equal(output); + expect(annotationControlsFSM.getState()).to.equal(output); + }); + }); + }); + }); + + describe('AnnotationState.HIGHLIGHT_TEMP/REGION_TEMP', () => { + [ + { + state: AnnotationState.HIGHLIGHT_TEMP, + stateMode: AnnotationMode.HIGHLIGHT, + }, + { + state: AnnotationState.REGION_TEMP, + stateMode: AnnotationMode.REGION, + }, + ].forEach(({ state, stateMode }) => { + [ + { + input: AnnotationInput.CLICK, + mode: AnnotationMode.HIGHLIGHT, + nextState: + stateMode === AnnotationMode.HIGHLIGHT ? AnnotationState.NONE : AnnotationState.HIGHLIGHT, + output: stateMode === AnnotationMode.HIGHLIGHT ? AnnotationMode.NONE : AnnotationMode.HIGHLIGHT, + }, + { + input: AnnotationInput.CLICK, + mode: AnnotationMode.REGION, + nextState: stateMode === AnnotationMode.REGION ? AnnotationState.NONE : AnnotationState.REGION, + output: stateMode === AnnotationMode.REGION ? AnnotationMode.NONE : AnnotationMode.REGION, + }, + { + input: AnnotationInput.CANCEL, + mode: AnnotationMode.HIGHLIGHT, + nextState: AnnotationState.NONE, + output: AnnotationMode.NONE, + }, + { + input: AnnotationInput.SUCCESS, + mode: undefined, + nextState: AnnotationState.NONE, + output: AnnotationMode.NONE, + }, + ].forEach(({ input, mode, nextState, output }) => { + it(`should go to state ${nextState} and output ${output}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(state); + + expect(annotationControlsFSM.transition(input, mode)).to.equal(output); + expect(annotationControlsFSM.getState()).to.equal(nextState); + }); + }); + + [AnnotationInput.CREATE, AnnotationInput.UPDATE].forEach(input => { + it(`should stay in state ${state} if input is ${input}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(state); - expect(annotationControlsFSM.transition(input, mode)).to.equal(output); - expect(annotationControlsFSM.getState()).to.equal(nextState); + expect(annotationControlsFSM.transition(input)).to.equal(stateMode); + expect(annotationControlsFSM.getState()).to.equal(state); + }); + }); }); }); }); From 4828358a2498e46c2d1b3f049c94ebfdfb1f51d4 Mon Sep 17 00:00:00 2001 From: Mingze Xiao Date: Wed, 9 Sep 2020 16:03:01 -0700 Subject: [PATCH 7/7] feat(annotations): Address comments --- .../__tests__/AnnotationControlsFSM-test.js | 112 ++++++++++++++---- 1 file changed, 86 insertions(+), 26 deletions(-) diff --git a/src/lib/__tests__/AnnotationControlsFSM-test.js b/src/lib/__tests__/AnnotationControlsFSM-test.js index 29fa06831..523561566 100644 --- a/src/lib/__tests__/AnnotationControlsFSM-test.js +++ b/src/lib/__tests__/AnnotationControlsFSM-test.js @@ -3,6 +3,7 @@ import { AnnotationMode } from '../AnnotationControls'; describe('lib/AnnotationControlsFSM', () => { describe('AnnotationState.NONE', () => { + // Go to different states [ { input: AnnotationInput.CLICK, @@ -29,7 +30,7 @@ describe('lib/AnnotationControlsFSM', () => { output: AnnotationMode.REGION, }, ].forEach(({ input, mode, nextState, output }) => { - it(`should go to state ${nextState} and output ${output}`, () => { + it(`should go to state ${nextState} and output ${output} if input is ${input} and mode is ${mode}`, () => { const annotationControlsFSM = new AnnotationControlsFSM(); expect(annotationControlsFSM.transition(input, mode)).to.equal(output); @@ -37,6 +38,7 @@ describe('lib/AnnotationControlsFSM', () => { }); }); + // Stay in the same state [AnnotationInput.CANCEL, AnnotationInput.SUCCESS, AnnotationInput.UPDATE].forEach(input => { it(`should stay in state none if input is ${input}`, () => { const annotationControlsFSM = new AnnotationControlsFSM(); @@ -48,11 +50,11 @@ describe('lib/AnnotationControlsFSM', () => { }); describe('AnnotationState.HIGHLIGHT/REGION', () => { + // Stay in the same state [AnnotationState.HIGHLIGHT, AnnotationState.REGION].forEach(state => { - // non-Click input [AnnotationInput.CANCEL, AnnotationInput.CREATE, AnnotationInput.SUCCESS, AnnotationInput.SUCCESS].forEach( input => { - it(`should stay in state ${state}`, () => { + it(`should stay in state ${state} if input is ${input}`, () => { const annotationControlsFSM = new AnnotationControlsFSM(state); expect(annotationControlsFSM.transition(input)).to.equal(state); @@ -60,13 +62,43 @@ describe('lib/AnnotationControlsFSM', () => { }); }, ); + }); - // Click input - [AnnotationMode.HIGHLIGHT, AnnotationMode.REGION].forEach(mode => { - it(`should go to state none/${mode} if input is click and mode is ${mode}`, () => { - const annotationControlsFSM = new AnnotationControlsFSM(state); + // Go to different states + describe('AnnotationState.HIGHLIGHT', () => { + [ + { + mode: AnnotationMode.HIGHLIGHT, + output: AnnotationMode.NONE, + }, + { + mode: AnnotationMode.REGION, + output: AnnotationMode.REGION, + }, + ].forEach(({ mode, output }) => { + it(`should output ${output} if input is click and mode is ${mode}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(AnnotationState.HIGHLIGHT); - const output = state === mode ? AnnotationMode.NONE : mode; + expect(annotationControlsFSM.transition(AnnotationInput.CLICK, mode)).to.equal(output); + expect(annotationControlsFSM.getState()).to.equal(output); + }); + }); + }); + + // Go to different states + describe('AnnotationState.REGION', () => { + [ + { + mode: AnnotationMode.REGION, + output: AnnotationMode.NONE, + }, + { + mode: AnnotationMode.HIGHLIGHT, + output: AnnotationMode.HIGHLIGHT, + }, + ].forEach(({ mode, output }) => { + it(`should output ${output} if input is click and mode is ${mode}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(AnnotationState.REGION); expect(annotationControlsFSM.transition(AnnotationInput.CLICK, mode)).to.equal(output); expect(annotationControlsFSM.getState()).to.equal(output); @@ -76,6 +108,7 @@ describe('lib/AnnotationControlsFSM', () => { }); describe('AnnotationState.HIGHLIGHT_TEMP/REGION_TEMP', () => { + // Go to none state [ { state: AnnotationState.HIGHLIGHT_TEMP, @@ -87,36 +120,21 @@ describe('lib/AnnotationControlsFSM', () => { }, ].forEach(({ state, stateMode }) => { [ - { - input: AnnotationInput.CLICK, - mode: AnnotationMode.HIGHLIGHT, - nextState: - stateMode === AnnotationMode.HIGHLIGHT ? AnnotationState.NONE : AnnotationState.HIGHLIGHT, - output: stateMode === AnnotationMode.HIGHLIGHT ? AnnotationMode.NONE : AnnotationMode.HIGHLIGHT, - }, - { - input: AnnotationInput.CLICK, - mode: AnnotationMode.REGION, - nextState: stateMode === AnnotationMode.REGION ? AnnotationState.NONE : AnnotationState.REGION, - output: stateMode === AnnotationMode.REGION ? AnnotationMode.NONE : AnnotationMode.REGION, - }, { input: AnnotationInput.CANCEL, - mode: AnnotationMode.HIGHLIGHT, nextState: AnnotationState.NONE, output: AnnotationMode.NONE, }, { input: AnnotationInput.SUCCESS, - mode: undefined, nextState: AnnotationState.NONE, output: AnnotationMode.NONE, }, - ].forEach(({ input, mode, nextState, output }) => { - it(`should go to state ${nextState} and output ${output}`, () => { + ].forEach(({ input, nextState, output }) => { + it(`should go to state ${nextState} and output ${output} if input is ${input}`, () => { const annotationControlsFSM = new AnnotationControlsFSM(state); - expect(annotationControlsFSM.transition(input, mode)).to.equal(output); + expect(annotationControlsFSM.transition(input)).to.equal(output); expect(annotationControlsFSM.getState()).to.equal(nextState); }); }); @@ -130,5 +148,47 @@ describe('lib/AnnotationControlsFSM', () => { }); }); }); + + // Go to different states + describe('AnnotationState.HIGHLIGHT_TEMP', () => { + [ + { + mode: AnnotationMode.HIGHLIGHT, + output: AnnotationMode.NONE, + }, + { + mode: AnnotationMode.REGION, + output: AnnotationMode.REGION, + }, + ].forEach(({ mode, output }) => { + it(`should output ${output} if input is click and mode is ${mode}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(AnnotationState.HIGHLIGHT_TEMP); + + expect(annotationControlsFSM.transition(AnnotationInput.CLICK, mode)).to.equal(output); + expect(annotationControlsFSM.getState()).to.equal(output); + }); + }); + }); + + // Go to different states + describe('AnnotationState.REGION_TEMP', () => { + [ + { + mode: AnnotationMode.REGION, + output: AnnotationMode.NONE, + }, + { + mode: AnnotationMode.HIGHLIGHT, + output: AnnotationMode.HIGHLIGHT, + }, + ].forEach(({ mode, output }) => { + it(`should output ${output} if input is click and mode is ${mode}`, () => { + const annotationControlsFSM = new AnnotationControlsFSM(AnnotationState.REGION_TEMP); + + expect(annotationControlsFSM.transition(AnnotationInput.CLICK, mode)).to.equal(output); + expect(annotationControlsFSM.getState()).to.equal(output); + }); + }); + }); }); });