import { createCustomShapeId } from '@tldraw/tlschema' import { TestApp } from '../TestApp' let app: TestApp const ids = { box1: createCustomShapeId('box1'), embed1: createCustomShapeId('embed1'), } jest.useFakeTimers() beforeEach(() => { app = new TestApp() app .selectAll() .deleteShapes() .createShapes([{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }]) }) describe('TLSelectTool.Translating', () => { it('Enters from pointing and exits to idle', () => { const shape = app.getShapeById(ids.box1) app.pointerDown(150, 150, { target: 'shape', shape }) app.expectToBeIn('select.pointing_shape') app.pointerMove(200, 200) app.expectToBeIn('select.translating') app.pointerUp() app.expectToBeIn('select.idle') }) it('Drags a shape', () => { const shape = app.getShapeById(ids.box1) app.pointerDown(150, 150, { target: 'shape', shape }) app.pointerMove(200, 200) app.pointerUp() app.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 }) }) it('Clones a shape, removes the clone, and re-creates the clone', () => { const shape = app.getShapeById(ids.box1) app.pointerDown(150, 150, { target: 'shape', shape }) app.pointerMove(200, 200) expect(app.shapesArray.length).toBe(1) app.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 }) const t1 = [...app.shapeIds.values()] app.keyDown('Alt') expect(app.shapesArray.length).toBe(2) app.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 }) // const t2 = [...app.shapeIds.values()] app.keyUp('Alt') // There's a timer here! We shouldn't end the clone until the timer is done expect(app.shapesArray.length).toBe(2) jest.advanceTimersByTime(250) // tick tock // Timer is done! We should have ended the clone. expect(app.shapesArray.length).toBe(1) app.expectToBeIn('select.translating') app.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 }) expect([...app.shapeIds.values()]).toMatchObject(t1) // todo: Should cloning again duplicate new shapes, or restore the last clone? // app.keyDown('Alt') // expect(app.shapesArray.length).toBe(2) // app.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 }) // expect([...app.shapeIds.values()]).toMatchObject(t2) }) it('Constrains when holding shift', () => { const shape = app.getShapeById(ids.box1) app.pointerDown(150, 150, { target: 'shape', shape }) app.pointerMove(200, 170) app.expectShapeToMatch({ id: ids.box1, x: 150, y: 120 }) app.keyDown('Shift') app.expectShapeToMatch({ id: ids.box1, x: 150, y: 100 }) }) it('Does not expand selection when holding shift and alt', () => { const shape = app.getShapeById(ids.box1) app.keyDown('Shift') // alt-drag to create a copy: app.keyDown('Alt') app.pointerDown(150, 150, { target: 'shape', shape }) app.pointerMove(150, 250) app.pointerUp() const box2Id = app.onlySelectedShape!.id expect(app.shapesArray.length).toStrictEqual(2) expect(ids.box1).not.toEqual(box2Id) // shift-alt-drag the original, we shouldn't duplicate the copy too: app.pointerDown(150, 150, { target: 'shape', shape }) expect(app.selectedIds).toStrictEqual([ids.box1]) app.pointerMove(250, 150) app.pointerUp() expect(app.shapesArray.length).toStrictEqual(3) }) }) describe('PointingHandle', () => { it('Enters from idle and exits to idle', () => { const shape = app.getShapeById(ids.box1) app.pointerDown(150, 150, { target: 'handle', shape, handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 }, }) app.expectToBeIn('select.pointing_handle') app.pointerUp() app.expectToBeIn('select.idle') }) it('Bails on escape', () => { const shape = app.getShapeById(ids.box1) app.pointerDown(150, 150, { target: 'handle', shape, handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 }, }) app.expectToBeIn('select.pointing_handle') app.cancel() app.expectToBeIn('select.idle') }) }) describe('DraggingHandle', () => { it('Enters from pointing_handle and exits to idle', () => { const shape = app.getShapeById(ids.box1) app.pointerDown(150, 150, { target: 'handle', shape, handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 }, }) app.pointerMove(100, 100) app.expectToBeIn('select.dragging_handle') app.pointerUp() app.expectToBeIn('select.idle') }) it('Bails on escape', () => { const shape = app.getShapeById(ids.box1) app.pointerDown(150, 150, { target: 'handle', shape, handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 }, }) app.pointerMove(100, 100) app.expectToBeIn('select.dragging_handle') app.cancel() app.expectToBeIn('select.idle') }) }) describe('When double clicking a shape', () => { it('begins editing a geo shapes label', () => { app .selectAll() .deleteShapes() .selectNone() .createShapes([{ id: app.createShapeId(), type: 'geo' }]) .doubleClick(50, 50, { target: 'shape', shape: app.shapesArray[0] }) .expectToBeIn('select.editing_shape') }) }) describe('When pressing enter on a selected shape', () => { it('begins editing a geo shapes label', () => { const id = app.createShapeId() app .selectAll() .deleteShapes() .selectNone() .createShapes([{ id, type: 'geo' }]) .select(id) .keyUp('Enter') .expectToBeIn('select.editing_shape') }) }) // it('selects the child of a group', () => { // const id1 = app.createShapeId() // const id2 = app.createShapeId() // app // .selectAll() // .deleteShapes() // .selectNone() // .createShapes([ // { id: id1, type: 'geo', x: 100, y: 100 }, // { id: id2, type: 'geo', x: 200, y: 200 }, // ]) // .selectAll() // .groupShapes() // .doubleClick(50, 50, { target: 'shape', shape: app.getShapeById(id1) }) // .expectToBeIn('select.editing_shape') // }) describe('When double clicking the selection edge', () => { it('Resets text scale when double clicking the edge of the text', () => { const id = app.createShapeId() app .selectAll() .deleteShapes() .selectNone() .createShapes([{ id, type: 'text', x: 100, y: 100, props: { scale: 2, text: 'hello' } }]) .select(id) .doubleClick(100, 100, { target: 'selection', handle: 'left' }) app.expectShapeToMatch({ id, props: { scale: 1 } }) }) it('Resets text autosize first when double clicking the edge of the text', () => { const id = app.createShapeId() app .selectAll() .deleteShapes() .selectNone() .createShapes([ { id, type: 'text', props: { scale: 2, autoSize: false, w: 200, text: 'hello' }, }, ]) .select(id) .doubleClick(100, 100, { target: 'selection', handle: 'left' }) app.expectShapeToMatch({ id, props: { scale: 2, autoSize: true } }) app.doubleClick(100, 100, { target: 'selection', handle: 'left' }) app.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } }) }) it('Begins editing the text if handler returns no change', () => { const id = app.createShapeId() app .selectAll() .deleteShapes() .selectNone() .createShapes([ { id, type: 'text', props: { scale: 2, autoSize: false, w: 200, text: 'hello' }, }, ]) .select(id) .doubleClick(100, 100, { target: 'selection', handle: 'left' }) .doubleClick(100, 100, { target: 'selection', handle: 'left' }) expect(app.editingId).toBe(null) app.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } }) app.doubleClick(100, 100, { target: 'selection', handle: 'left' }) expect(app.editingId).toBe(id) }) it('Selects a geo shape when double clicking on its edge', () => { const id = app.createShapeId() app .selectAll() .deleteShapes() .selectNone() .createShapes([ { id, type: 'geo', }, ]) .select(id) expect(app.editingId).toBe(null) app.doubleClick(100, 100, { target: 'selection', handle: 'left' }) expect(app.editingId).toBe(id) }) }) describe('When editing shapes', () => { let ids: any beforeEach(() => { ids = { geo1: app.createShapeId(), geo2: app.createShapeId(), text1: app.createShapeId(), text2: app.createShapeId(), } app.createShapes([ { id: ids.geo1, type: 'geo', props: { text: 'hello world ' } }, { id: ids.geo2, type: 'geo', props: { text: 'hello world ' } }, { id: ids.text1, type: 'text', props: { text: 'hello world ' } }, { id: ids.text2, type: 'text', props: { text: 'hello world ' } }, ]) }) it('Pointing a shape of a different type selects it and leaves editing', () => { expect(app.editingId).toBe(null) expect(app.selectedIds.length).toBe(0) // start editing the geo shape app.doubleClick(50, 50, { target: 'shape', shape: app.getShapeById(ids.geo1) }) expect(app.editingId).toBe(ids.geo1) expect(app.onlySelectedShape?.id).toBe(ids.geo1) // point the text shape app.pointerDown(50, 50, { target: 'shape', shape: app.getShapeById(ids.text1) }) expect(app.editingId).toBe(null) expect(app.onlySelectedShape?.id).toBe(ids.text1) }) it('Pointing a shape of a different type selects it and leaves editing', () => { expect(app.editingId).toBe(null) expect(app.selectedIds.length).toBe(0) // start editing the geo shape app.doubleClick(50, 50, { target: 'shape', shape: app.getShapeById(ids.geo1) }) expect(app.editingId).toBe(ids.geo1) expect(app.onlySelectedShape?.id).toBe(ids.geo1) // point the other geo shape app.pointerDown(50, 50, { target: 'shape', shape: app.getShapeById(ids.geo2) }) // that other shape should now be editing and selected! expect(app.editingId).toBe(ids.geo2) expect(app.onlySelectedShape?.id).toBe(ids.geo2) }) it('Works with text, too', () => { expect(app.editingId).toBe(null) expect(app.selectedIds.length).toBe(0) // start editing the geo shape app.doubleClick(50, 50, { target: 'shape', shape: app.getShapeById(ids.text1) }) app.pointerDown(50, 50, { target: 'shape', shape: app.getShapeById(ids.text2) }) // that other shape should now be editing and selected! expect(app.editingId).toBe(ids.text2) expect(app.onlySelectedShape?.id).toBe(ids.text2) }) it('Double clicking the canvas creates a new text shape', () => { expect(app.editingId).toBe(null) expect(app.selectedIds.length).toBe(0) expect(app.shapesArray.length).toBe(5) app.doubleClick(750, 750) expect(app.shapesArray.length).toBe(6) expect(app.shapesArray[5].type).toBe('text') }) it('It deletes an empty text shape when your click away', () => { expect(app.editingId).toBe(null) expect(app.selectedIds.length).toBe(0) expect(app.shapesArray.length).toBe(5) // Create a new shape by double clicking app.doubleClick(750, 750) expect(app.selectedIds.length).toBe(1) expect(app.shapesArray.length).toBe(6) const shapeId = app.selectedIds[0] // Click away app.click(1000, 1000) expect(app.selectedIds.length).toBe(0) expect(app.shapesArray.length).toBe(5) expect(app.getShapeById(shapeId)).toBe(undefined) }) it('It deletes an empty text shape when your click another text shape', () => { expect(app.editingId).toBe(null) expect(app.selectedIds.length).toBe(0) expect(app.shapesArray.length).toBe(5) // Create a new shape by double clicking app.doubleClick(750, 750) expect(app.selectedIds.length).toBe(1) expect(app.shapesArray.length).toBe(6) const shapeId = app.selectedIds[0] // Click another text shape app.click(50, 50, { target: 'shape', shape: app.getShapeById(ids.text1) }) expect(app.selectedIds.length).toBe(1) expect(app.shapesArray.length).toBe(5) expect(app.getShapeById(shapeId)).toBe(undefined) }) it.todo('restores selection after changing styles') }) describe('When in readonly mode', () => { beforeEach(() => { app.createShapes([ { id: ids.embed1, type: 'embed', x: 100, y: 100, props: { opacity: '1', w: 100, h: 100, url: '', doesResize: false }, }, ]) app.setReadOnly(true) app.setSelectedTool('select') }) it('Begins editing embed when double clicked', () => { expect(app.editingId).toBe(null) expect(app.selectedIds.length).toBe(0) expect(app.isReadOnly).toBe(true) const shape = app.getShapeById(ids.embed1) app.doubleClick(100, 100, { target: 'shape', shape }) expect(app.editingId).toBe(ids.embed1) }) it('Begins editing embed when pressing Enter on a selected embed', () => { expect(app.editingId).toBe(null) expect(app.selectedIds.length).toBe(0) expect(app.isReadOnly).toBe(true) app.setSelectedIds([ids.embed1]) expect(app.selectedIds.length).toBe(1) app.keyUp('Enter') expect(app.editingId).toBe(ids.embed1) }) })