UNPKG

tldraw

Version:

A tiny little drawing editor.

295 lines (252 loc) • 7.85 kB
import { TLNoteShape } from '@tldraw/editor' import { TestEditor } from '../../../test/TestEditor' import { NoteShapeTool } from './NoteShapeTool' let editor: TestEditor beforeEach(() => { editor = new TestEditor({ options: { adjacentShapeMargin: 20 } }) }) afterEach(() => { editor?.dispose() }) describe(NoteShapeTool, () => { it('Creates note shapes on click-and-drag, supports undo and redo', () => { expect(editor.getCurrentPageShapes().length).toBe(0) editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.pointerMove(100, 100) editor.pointerUp(100, 100) expect(editor.getCurrentPageShapes().length).toBe(1) expect(editor.getCurrentPageShapes()[0]?.type).toBe('note') expect(editor.getSelectedShapeIds()[0]).toBe(editor.getCurrentPageShapes()[0]?.id) editor.cancel() // leave edit mode editor.undo() expect(editor.getCurrentPageShapes().length).toBe(0) editor.redo() expect(editor.getCurrentPageShapes().length).toBe(1) }) it('Creates note shapes on click, supports undo and redo', () => { expect(editor.getCurrentPageShapes().length).toBe(0) editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.pointerUp(50, 50) expect(editor.getCurrentPageShapes().length).toBe(1) expect(editor.getCurrentPageShapes()[0]?.type).toBe('note') expect(editor.getSelectedShapeIds()[0]).toBe(editor.getCurrentPageShapes()[0]?.id) editor.undo() expect(editor.getCurrentPageShapes().length).toBe(0) editor.redo() expect(editor.getCurrentPageShapes().length).toBe(1) }) }) describe('When selecting the tool', () => { it('selects the tool and enters the idle state', () => { editor.setCurrentTool('note') editor.expectToBeIn('note.idle') }) }) describe('When in the idle state', () => { it('Enters pointing state on pointer down', () => { editor.setCurrentTool('note') editor.pointerDown(100, 100) editor.expectToBeIn('note.pointing') }) it('Switches back to select tool on cancel', () => { editor.setCurrentTool('note') editor.cancel() editor.expectToBeIn('select.idle') }) it('Does nothing on interrupt', () => { editor.setCurrentTool('note') editor.interrupt() editor.expectToBeIn('note.idle') }) }) describe('When in the pointing state', () => { it('Switches back to idle on cancel', () => { editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.expectToBeIn('note.pointing') editor.cancel() editor.expectToBeIn('note.idle') }) it('Enters the select.translating state on drag start', () => { editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.pointerMove(51, 51) // not far enough! editor.expectToBeIn('note.pointing') editor.pointerMove(55, 55) editor.expectToBeIn('select.translating') }) it('Returns to the note tool on cancel from translating', () => { editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.pointerMove(55, 55) editor.cancel() editor.expectToBeIn('note.idle') }) it('Returns to the note tool on complete from translating when tool lock is enabled', () => { editor.updateInstanceState({ isToolLocked: true }) editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.pointerMove(55, 55) editor.pointerUp() editor.expectToBeIn('note.idle') }) it('Returns to the idle state on interrupt', () => { editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.interrupt() editor.expectToBeIn('note.idle') }) it('Creates a note and begins editing on pointer up', () => { expect(editor.getCurrentPageShapes().length).toBe(0) editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.pointerUp(50, 50) editor.expectToBeIn('select.editing_shape') expect(editor.getCurrentPageShapes().length).toBe(1) }) it('Creates a note and returns to note.idle on pointer up if tool lock is enabled', () => { editor.updateInstanceState({ isToolLocked: true }) expect(editor.getCurrentPageShapes().length).toBe(0) editor.setCurrentTool('note') editor.pointerDown(50, 50) editor.pointerUp(50, 50) editor.expectToBeIn('note.idle') expect(editor.getCurrentPageShapes().length).toBe(1) }) }) describe('Adjacent note position helpers (sticky pits)', () => { it('Creates a new sticky note outside of a sticky pit', () => { editor.createShape({ type: 'note', x: 0, y: 0 }) for (const pit of [ { x: 100, y: -120 }, { x: 320, y: 100 }, { x: 100, y: 320 }, { x: -120, y: 100 }, ]) { const OFFSET_DISTANCE = 8 editor .setCurrentTool('note') .pointerMove(pit.x + OFFSET_DISTANCE, pit.y + OFFSET_DISTANCE) // too far from the pit .click() .expectShapeToMatch({ ...editor.getLastCreatedShape(), x: pit.x + OFFSET_DISTANCE - 100, y: pit.y + OFFSET_DISTANCE - 100, }) } }) it('Creates a new sticky note in a sticky pit', () => { editor.createShape({ type: 'note', x: 0, y: 0 }) for (const pit of [ { x: 100, y: -120 }, { x: 320, y: 100 }, { x: 100, y: 320 }, { x: -120, y: 100 }, ]) { const OFFSET_DISTANCE = 7 // close enough to the pit to fall into it editor .setCurrentTool('note') .pointerMove(pit.x + OFFSET_DISTANCE, pit.y + OFFSET_DISTANCE) .click() .expectShapeToMatch({ ...editor.getLastCreatedShape(), x: pit.x - 100, y: pit.y - 100, }) } }) it('Falls into a sticky pit when empty', () => { editor .createShape({ type: 'note', x: 0, y: 0 }) .setCurrentTool('note') .pointerMove(324, 104) .click() .expectShapeToMatch({ ...editor.getLastCreatedShape(), // in da pit x: 220, y: 0, }) }) it('Does not create a new sticky note in a sticky pit if a note is already there', () => { editor .createShape({ type: 'note', x: 0, y: 0 }) .createShape({ type: 'note', x: 330, y: 8 }) // make a shape kinda there already! .setCurrentTool('note') .pointerMove(300, 104) .click() .expectShapeToMatch({ ...editor.getLastCreatedShape(), // outta da pit x: 200, y: 4, }) }) it('Does not fall into pits around rotated notes', () => { editor.createShape({ type: 'note', x: 0, y: 0, rotation: 0.0000001 }) for (const pit of [ { x: 100, y: -120 }, { x: 320, y: 100 }, { x: 100, y: 320 }, { x: -120, y: 100 }, ]) { const OFFSET_DISTANCE = 7 // close enough to the pit to fall into it (if it weren't rotated) editor .setCurrentTool('note') .pointerMove(pit.x + OFFSET_DISTANCE, pit.y + OFFSET_DISTANCE) .click() .expectShapeToMatch({ ...editor.getLastCreatedShape(), x: pit.x + OFFSET_DISTANCE - 100, y: pit.y + OFFSET_DISTANCE - 100, }) } }) it('Falls into correct pit below notes with growY', () => { editor.createShape({ type: 'note', x: 0, y: 0 }).updateShape({ ...editor.getLastCreatedShape<TLNoteShape>(), props: { growY: 100 }, }) // Misses the pit below the note because the note has growY // instead of being at 100, 320, it's at 100, 320 + 100 = 320 editor .setCurrentTool('note') .pointerMove(100, 324) .click() .expectShapeToMatch({ ...editor.getLastCreatedShape(), x: 0, y: 224, }) .undo() // Let's get it in that pit editor .setCurrentTool('note') .pointerMove(100, 424) .click() .expectShapeToMatch({ ...editor.getLastCreatedShape(), x: 0, y: 320, }) .undo() }) it('Prefers snapping to adjacent note position over the grid', () => { editor.updateInstanceState({ isGridMode: true }) editor.createShape({ type: 'note', x: 2, y: 0 }) const pit = { x: 322, y: 100 } editor .setCurrentTool('note') .pointerDown(0, 0) .pointerMove(pit.x, pit.y) .pointerUp() .expectShapeToMatch({ ...editor.getLastCreatedShape(), x: pit.x - 100, y: pit.y - 100, }) expect(editor.getLastCreatedShape().x).not.toBe(220) }) })