diff --git a/__tests__/commands/change-page.ts b/__tests__/commands/change-page.test.ts similarity index 100% rename from __tests__/commands/change-page.ts rename to __tests__/commands/change-page.test.ts diff --git a/__tests__/commands/delete-page.ts b/__tests__/commands/delete-page.test.ts similarity index 100% rename from __tests__/commands/delete-page.ts rename to __tests__/commands/delete-page.test.ts diff --git a/__tests__/commands/distribute.ts b/__tests__/commands/distribute.test.ts similarity index 100% rename from __tests__/commands/distribute.ts rename to __tests__/commands/distribute.test.ts diff --git a/__tests__/commands/draw.ts b/__tests__/commands/draw.test.ts similarity index 100% rename from __tests__/commands/draw.ts rename to __tests__/commands/draw.test.ts diff --git a/__tests__/commands/duplicate.ts b/__tests__/commands/duplicate.test.ts similarity index 100% rename from __tests__/commands/duplicate.ts rename to __tests__/commands/duplicate.test.ts diff --git a/__tests__/commands/edit.ts b/__tests__/commands/edit.test.ts similarity index 100% rename from __tests__/commands/edit.ts rename to __tests__/commands/edit.test.ts diff --git a/__tests__/commands/generate.ts b/__tests__/commands/generate.test.ts similarity index 100% rename from __tests__/commands/generate.ts rename to __tests__/commands/generate.test.ts diff --git a/__tests__/commands/group.ts b/__tests__/commands/group.test.ts similarity index 100% rename from __tests__/commands/group.ts rename to __tests__/commands/group.test.ts diff --git a/__tests__/commands/move-to-page.ts b/__tests__/commands/move-to-page.test.ts similarity index 100% rename from __tests__/commands/move-to-page.ts rename to __tests__/commands/move-to-page.test.ts diff --git a/__tests__/commands/toggle.test.ts b/__tests__/commands/toggle.test.ts new file mode 100644 index 000000000..a19dc7ce2 --- /dev/null +++ b/__tests__/commands/toggle.test.ts @@ -0,0 +1,237 @@ +import { ShapeType } from 'types' +import TestState from '../test-utils' + +describe('toggle command', () => { + const tt = new TestState() + tt.resetDocumentState() + .createShape( + { + type: ShapeType.Rectangle, + point: [0, 0], + size: [100, 100], + childIndex: 1, + isLocked: false, + isHidden: false, + isAspectRatioLocked: false, + }, + 'rect1' + ) + .createShape( + { + type: ShapeType.Rectangle, + point: [400, 0], + size: [100, 100], + childIndex: 2, + isHidden: false, + isLocked: false, + isAspectRatioLocked: false, + }, + 'rect2' + ) + .createShape( + { + type: ShapeType.Rectangle, + point: [800, 0], + size: [100, 100], + childIndex: 3, + isHidden: true, + isLocked: true, + isAspectRatioLocked: true, + }, + 'rect3' + ) + .createShape( + { + type: ShapeType.Rectangle, + point: [1000, 0], + size: [100, 100], + childIndex: 4, + isHidden: true, + isLocked: true, + isAspectRatioLocked: true, + }, + 'rect4' + ) + .save() + + describe('toggles properties on single shape', () => { + it('does command', () => { + tt.restore().clickShape('rect1') + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect1').isLocked).toBe(true) + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect1').isLocked).toBe(false) + }) + + it('undoes and redoes command', () => { + // Restore the saved data state, with the initial selection + tt.restore().clickShape('rect1') + + tt.send('TOGGLED_SHAPE_LOCK') + + tt.send('UNDO') + + expect(tt.getShape('rect1').isLocked).toBe(false) + + tt.send('REDO') + + expect(tt.getShape('rect1').isLocked).toBe(true) + }) + }) + + describe('toggles properties on shapes with matching properties, starting from false', () => { + it('does command', () => { + // Restore the saved data state, with the initial selection + tt.restore().clickShape('rect1').clickShape('rect2', { shiftKey: true }) + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect1').isLocked).toBe(true) + expect(tt.getShape('rect2').isLocked).toBe(true) + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect1').isLocked).toBe(false) + expect(tt.getShape('rect2').isLocked).toBe(false) + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect1').isLocked).toBe(true) + expect(tt.getShape('rect2').isLocked).toBe(true) + }) + + it('undoes and redoes command', () => { + // Restore the saved data state, with the initial selection + tt.restore().clickShape('rect1').clickShape('rect2', { shiftKey: true }) + + tt.send('TOGGLED_SHAPE_LOCK').undo() + + expect(tt.getShape('rect1').isLocked).toBe(false) + expect(tt.getShape('rect2').isLocked).toBe(false) + + tt.redo() + + expect(tt.getShape('rect1').isLocked).toBe(true) + expect(tt.getShape('rect2').isLocked).toBe(true) + }) + }) + + describe('toggles properties on shapes with matching properties, starting from true', () => { + it('does command', () => { + // Restore the saved data state, with the initial selection + tt.restore().clickShape('rect3').clickShape('rect4', { shiftKey: true }) + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect3').isLocked).toBe(false) + expect(tt.getShape('rect4').isLocked).toBe(false) + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect3').isLocked).toBe(true) + expect(tt.getShape('rect4').isLocked).toBe(true) + }) + + it('undoes and redoes command', () => { + // Restore the saved data state, with the initial selection + tt.restore().clickShape('rect3').clickShape('rect4', { shiftKey: true }) + + tt.send('TOGGLED_SHAPE_LOCK').undo() + + expect(tt.getShape('rect3').isLocked).toBe(true) + expect(tt.getShape('rect4').isLocked).toBe(true) + + tt.redo() + + expect(tt.getShape('rect3').isLocked).toBe(false) + expect(tt.getShape('rect4').isLocked).toBe(false) + }) + }) + + describe('toggles properties on shapes with different properties', () => { + it('does command', () => { + // Restore the saved data state, with the initial selection + tt.restore() + .clickShape('rect1') + .clickShape('rect2', { shiftKey: true }) + .clickShape('rect3', { shiftKey: true }) + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect1').isLocked).toBe(true) + expect(tt.getShape('rect2').isLocked).toBe(true) + expect(tt.getShape('rect3').isLocked).toBe(true) + + tt.send('TOGGLED_SHAPE_LOCK') + + expect(tt.getShape('rect1').isLocked).toBe(false) + expect(tt.getShape('rect2').isLocked).toBe(false) + expect(tt.getShape('rect3').isLocked).toBe(false) + }) + + it('undoes and redoes command', () => { + tt.restore() + .clickShape('rect1') + .clickShape('rect2', { shiftKey: true }) + .clickShape('rect3', { shiftKey: true }) + + tt.send('TOGGLED_SHAPE_LOCK').undo() + + expect(tt.getShape('rect1').isLocked).toBe(false) + expect(tt.getShape('rect2').isLocked).toBe(false) + expect(tt.getShape('rect3').isLocked).toBe(true) + + tt.redo() + + expect(tt.getShape('rect1').isLocked).toBe(true) + expect(tt.getShape('rect2').isLocked).toBe(true) + expect(tt.getShape('rect3').isLocked).toBe(true) + }) + }) + + describe('catch all for other toggle props', () => { + const eventPropertyPairs = { + TOGGLED_SHAPE_LOCK: 'isLocked', + TOGGLED_SHAPE_HIDE: 'isHidden', + TOGGLED_SHAPE_ASPECT_LOCK: 'isAspectRatioLocked', + } + + it('toggles all event property pairs', () => { + Object.entries(eventPropertyPairs).forEach(([eventName, propName]) => { + // Restore the saved data state, with the initial selection + tt.restore() + .clickShape('rect1') + .clickShape('rect2', { shiftKey: true }) + .clickShape('rect3', { shiftKey: true }) + + tt.send(eventName) + + expect(tt.getShape('rect1')[propName]).toBe(true) + expect(tt.getShape('rect2')[propName]).toBe(true) + expect(tt.getShape('rect3')[propName]).toBe(true) + + tt.undo() + + expect(tt.getShape('rect1')[propName]).toBe(false) + expect(tt.getShape('rect2')[propName]).toBe(false) + expect(tt.getShape('rect3')[propName]).toBe(true) + + tt.redo() + + expect(tt.getShape('rect1')[propName]).toBe(true) + expect(tt.getShape('rect2')[propName]).toBe(true) + expect(tt.getShape('rect3')[propName]).toBe(true) + + tt.send(eventName) + + expect(tt.getShape('rect1')[propName]).toBe(false) + expect(tt.getShape('rect2')[propName]).toBe(false) + expect(tt.getShape('rect3')[propName]).toBe(false) + }) + }) + }) +}) diff --git a/__tests__/test-utils.ts b/__tests__/test-utils.ts index 94daad003..f80b4104e 100644 --- a/__tests__/test-utils.ts +++ b/__tests__/test-utils.ts @@ -228,15 +228,18 @@ class TestState { *``` */ clickShape(id: string, options: PointerOptions = {}): TestState { - this.state.send( - 'POINTED_SHAPE', - inputs.pointerDown(TestState.point(options), id) - ) + const shape = tld.getShape(this.data, id) + const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0] - this.state.send( - 'STOPPED_POINTING', - inputs.pointerUp(TestState.point(options), id) - ) + this.state + .send( + 'POINTED_SHAPE', + inputs.pointerDown(TestState.point({ x, y, ...options }), id) + ) + .send( + 'STOPPED_POINTING', + inputs.pointerUp(TestState.point({ x, y, ...options }), id) + ) return this } @@ -251,9 +254,12 @@ class TestState { *``` */ startClick(id: string, options: PointerOptions = {}): TestState { + const shape = tld.getShape(this.data, id) + const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0] + this.state.send( 'POINTED_SHAPE', - inputs.pointerDown(TestState.point(options), id) + inputs.pointerDown(TestState.point({ x, y, ...options }), id) ) return this @@ -269,9 +275,12 @@ class TestState { *``` */ stopClick(id: string, options: PointerOptions = {}): TestState { + const shape = tld.getShape(this.data, id) + const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0] + this.state.send( 'STOPPED_POINTING', - inputs.pointerUp(TestState.point(options), id) + inputs.pointerUp(TestState.point({ x, y, ...options }), id) ) return this @@ -287,12 +296,18 @@ class TestState { *``` */ doubleClickShape(id: string, options: PointerOptions = {}): TestState { + const shape = tld.getShape(this.data, id) + const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0] + this.state .send( 'DOUBLE_POINTED_SHAPE', - inputs.pointerDown(TestState.point(options), id) + inputs.pointerDown(TestState.point({ x, y, ...options }), id) + ) + .send( + 'STOPPED_POINTING', + inputs.pointerUp(TestState.point({ x, y, ...options }), id) ) - .send('STOPPED_POINTING', inputs.pointerUp(TestState.point(options), id)) return this } diff --git a/state/commands/toggle.ts b/state/commands/toggle.ts index f6c113e13..47a97dad3 100644 --- a/state/commands/toggle.ts +++ b/state/commands/toggle.ts @@ -5,15 +5,22 @@ import tld from 'utils/tld' import { getShapeUtils } from 'state/shape-utils' import { PropsOfType } from 'types' +/** + * The toggle command toggles a boolean property on all selected shapes. + * If the value is true for all selected shapes, then the property is + * set to false; the value is false for one or more of the selected + * shapes, then the value for all shapes is set to true. + */ export default function toggleCommand( data: Data, prop: PropsOfType ): void { const selectedShapes = tld.getSelectedShapes(data) const isAllToggled = selectedShapes.every((shape) => shape[prop]) - const initialShapes = Object.fromEntries( - selectedShapes.map((shape) => [shape.id, shape[prop]]) - ) + const initialShapes = selectedShapes.map((shape) => ({ + id: shape.id, + value: shape[prop], + })) history.execute( data, @@ -23,22 +30,22 @@ export default function toggleCommand( do(data) { const { shapes } = tld.getPage(data) - for (const id in initialShapes) { + initialShapes.forEach(({ id }) => { const shape = shapes[id] getShapeUtils(shape).setProperty( shape, prop, isAllToggled ? false : true ) - } + }) }, undo(data) { const { shapes } = tld.getPage(data) - for (const id in initialShapes) { + initialShapes.forEach(({ id, value }) => { const shape = shapes[id] - getShapeUtils(shape).setProperty(shape, prop, initialShapes[id]) - } + getShapeUtils(shape).setProperty(shape, prop, value) + }) }, }) )