UNPKG

tldraw

Version:

A tiny little drawing editor.

484 lines (391 loc) • 15 kB
import { createShapeId, TLArrowShape } from '@tldraw/editor' import { vi } from 'vitest' import { getArrowBindings } from '../../lib/shapes/arrow/shared' import { TestEditor } from '../TestEditor' let editor: TestEditor const ids = { box1: createShapeId('box1'), box2: createShapeId('box2'), box3: createShapeId('box3'), box4: createShapeId('box4'), arrow1: createShapeId('arrow1'), } beforeEach(() => { editor = new TestEditor() editor.selectAll().deleteShapes(editor.getSelectedShapeIds()) }) afterEach(() => { editor.selectAll().deleteShapes(editor.getSelectedShapeIds()) }) const doMockClipboard = () => { const context: { current: any } = { current: undefined } Object.assign(window.navigator, { clipboard: { write: vi.fn((content: any) => { context.current = content }), }, }) globalThis.ClipboardItem = vi.fn((payload: any) => payload) as any return context } const assertClipboardOfCorrectShape = async (_clipboardContent: any) => { return true // expect(clipboardContent.length).toBe(1) // expect(clipboardContent[0]['text/html']).toBeDefined() // expect(clipboardContent[0]['text/plain']).toBeDefined() // expect(clipboardContent.length).toBe(1) // expect(await readAsText(clipboardContent[0]['text/html'])).toMatch(/^<tldraw>(.*)<\/tldraw>$/) } describe('When copying and pasting', () => { it('does nothing when copying with nothing is selected', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ { id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, ]) editor.copy() expect(mockClipboard.current).toBeUndefined() }) it('copies the selected shapes and pastes when ALL shapes still in the viewport', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ { id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, ]) const shapesBefore = editor.getCurrentPageShapes() editor.selectAll().copy() await assertClipboardOfCorrectShape(mockClipboard.current) const testOffsetX = 100 const testOffsetY = 100 editor.setCamera({ x: editor.getCamera().x - testOffsetX, y: editor.getCamera().y - testOffsetY, z: editor.getZoomLevel(), }) editor.paste() const shapesAfter = editor.getCurrentPageShapes() // We should not have changed the original shapes expect(shapesBefore[0]).toMatchObject(shapesAfter[0]) expect(shapesBefore[1]).toMatchObject(shapesAfter[1]) const box1a = shapesAfter[0] const box2a = shapesAfter[1] const box1b = shapesAfter[2] const box2b = shapesAfter[3] // The new shapes should match the old shapes, except for their id expect(shapesAfter.length).toBe(shapesBefore.length * 2) expect(box1b).toMatchObject({ ...box1a, id: box1b.id, index: 'a3' }) expect(box2b).toMatchObject({ ...box2a, id: box2b.id, index: 'a4' }) }) it.todo('pastes at the correct child index (top of the current focus layer list)') it.todo( 'does not move shapes that are outside of the viewport when pasting into the children of an existing / non-copied shape' ) it('copies the selected shapes and pastes when SOME shapes still in the viewport', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ { id: ids.box1, type: 'geo', x: -2000, y: -100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 1900, y: 0, props: { w: 100, h: 100 } }, ]) const shapesBefore = editor.getCurrentPageShapes() editor.selectAll().copy() await assertClipboardOfCorrectShape(mockClipboard.current) const testOffsetX = 1800 const testOffsetY = 0 editor.setCamera({ x: editor.getCamera().x - testOffsetX, y: editor.getCamera().y - testOffsetY, z: editor.getZoomLevel(), }) editor.paste() const shapesAfter = editor.getCurrentPageShapes() // We should not have changed the original shapes expect(shapesBefore[0]).toMatchObject(shapesAfter[0]) expect(shapesBefore[1]).toMatchObject(shapesAfter[1]) const box1a = shapesAfter[0] const box2a = shapesAfter[1] const box1b = shapesAfter[2] const box2b = shapesAfter[3] // The new shapes should match the old shapes, except for their id expect(shapesAfter.length).toBe(shapesBefore.length * 2) expect(box1b).toMatchObject({ ...box1a, id: box1b.id, index: 'a3' }) expect(box2b).toMatchObject({ ...box2a, id: box2b.id, index: 'a4' }) }) it('copies the selected shapes and pastes when NO shapes still in the viewport', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ // NOTE: These shapes are centered around [0,0] to make calcs in assertions easier. { id: ids.box1, type: 'geo', x: -100, y: -100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } }, ]) const shapesBefore = editor.getCurrentPageShapes() editor.selectAll().copy() await assertClipboardOfCorrectShape(mockClipboard.current) const testOffsetX = 2000 const testOffsetY = 3000 const { w: screenWidth, h: screenHeight } = editor.getViewportScreenBounds() editor.setCamera({ x: editor.getCamera().x - testOffsetX, y: editor.getCamera().y - testOffsetY, z: editor.getZoomLevel(), }) editor.paste() const shapesAfter = editor.getCurrentPageShapes() // We should not have changed the original shapes expect(shapesBefore[0]).toMatchObject(shapesAfter[0]) expect(shapesBefore[1]).toMatchObject(shapesAfter[1]) const box1a = shapesAfter[0] const box2a = shapesAfter[1] const box1b = shapesAfter[2] const box2b = shapesAfter[3] // The new shapes should match the old shapes, except for the should be positioned on the new viewport center. expect(shapesAfter.length).toBe(shapesBefore.length * 2) expect(box1b).toMatchObject({ ...box1a, id: box1b.id, index: 'a3', x: testOffsetX + screenWidth / 2 + box1a.x, y: testOffsetY + screenHeight / 2 + box1a.y, }) expect(box2b).toMatchObject({ ...box2a, id: box2b.id, index: 'a4', x: testOffsetX + screenWidth / 2 + box2a.x, y: testOffsetY + screenHeight / 2 + box2a.y, }) }) it('creates new bindings for arrows when pasting', async () => { const mockClipboard = doMockClipboard() editor .createShapes([ { id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, { id: ids.arrow1, type: 'arrow', x: 150, y: 150 }, ]) .createBindings([ { fromId: ids.arrow1, toId: ids.box1, type: 'arrow', props: { terminal: 'start', normalizedAnchor: { x: 0.5, y: 0.5 }, isExact: false, isPrecise: false, }, }, { fromId: ids.arrow1, toId: ids.box2, type: 'arrow', props: { terminal: 'end', normalizedAnchor: { x: 0.5, y: 0.5 }, isExact: false, isPrecise: false, }, }, ]) const shapesBefore = editor.getCurrentPageShapes() editor.selectAll().copy() // Test the shape of the clipboard data. await assertClipboardOfCorrectShape(mockClipboard.current) editor.paste() const shapesAfter = editor.getCurrentPageShapes() // We should not have changed the original shapes expect(shapesBefore[0]).toMatchObject(shapesAfter[0]) expect(shapesBefore[1]).toMatchObject(shapesAfter[1]) expect(shapesBefore[2]).toMatchObject(shapesAfter[2]) const box1a = shapesAfter[0] const box2a = shapesAfter[1] const arrow1a = shapesAfter[2] as TLArrowShape const box1b = shapesAfter[3] const box2b = shapesAfter[4] const arrow1b = shapesAfter[5] // The new shapes should match the old shapes, except for their id and the arrow's bindings! expect(shapesAfter.length).toBe(shapesBefore.length * 2) expect(box1b).toMatchObject({ ...box1a, id: box1b.id, index: 'a4' }) expect(box2b).toMatchObject({ ...box2a, id: box2b.id, index: 'a5' }) expect(arrow1b).toMatchObject({ ...arrow1a, id: arrow1b.id, index: 'a6', }) expect(getArrowBindings(editor, arrow1b as TLArrowShape)).toMatchObject({ start: { toId: box1b.id }, end: { toId: box2b.id }, }) }) it('does nothing when cutting with nothing is selected', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ { id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, ]) editor.cut() expect(mockClipboard.current).toBeUndefined() }) it('cuts the selected shapes and pastes when ALL shapes still in the viewport', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ { id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, ]) const shapesBefore = editor.getCurrentPageShapes() editor.selectAll().cut() await assertClipboardOfCorrectShape(mockClipboard.current) const testOffsetX = 100 const testOffsetY = 100 editor.setCamera({ x: editor.getCamera().x - testOffsetX, y: editor.getCamera().y - testOffsetY, z: editor.getZoomLevel(), }) editor.paste() const shapesAfter = editor.getCurrentPageShapes() // The new shapes should match the old shapes, except for their id expect(shapesAfter.length).toBe(shapesBefore.length) expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id }) expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id }) }) it('cuts the selected shapes and pastes when SOME shapes still in the viewport', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ { id: ids.box1, type: 'geo', x: -2000, y: -100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 1900, y: 0, props: { w: 100, h: 100 } }, ]) const shapesBefore = editor.getCurrentPageShapes() editor.selectAll().cut() await assertClipboardOfCorrectShape(mockClipboard.current) const testOffsetX = 1800 const testOffsetY = 0 editor.setCamera({ x: editor.getCamera().x - testOffsetX, y: editor.getCamera().y - testOffsetY, z: editor.getZoomLevel(), }) editor.paste() const shapesAfter = editor.getCurrentPageShapes() // The new shapes should match the old shapes, except for their id expect(shapesAfter.length).toBe(shapesBefore.length) expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id }) expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id }) }) it('cuts the selected shapes and pastes when NO shapes still in the viewport', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ // NOTE: These shapes are centered around [0,0] to make calcs in assertions easier. { id: ids.box1, type: 'geo', x: -100, y: -100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } }, ]) const shapesBefore = editor.getCurrentPageShapes() editor.selectAll().cut() await assertClipboardOfCorrectShape(mockClipboard.current) const testOffsetX = 2000 const testOffsetY = 3000 const { w: screenWidth, h: screenHeight } = editor.getViewportScreenBounds() editor.setCamera({ x: editor.getCamera().x - testOffsetX, y: editor.getCamera().y - testOffsetY, z: editor.getZoomLevel(), }) editor.paste() const shapesAfter = editor.getCurrentPageShapes() // The new shapes should match the old shapes, except for the should be positioned on the new viewport center. expect(shapesAfter.length).toBe(shapesBefore.length) expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id, x: testOffsetX + screenWidth / 2 + shapesBefore[0].x, y: testOffsetY + screenHeight / 2 + shapesBefore[0].y, }) expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id, x: testOffsetX + screenWidth / 2 + shapesBefore[1].x, y: testOffsetY + screenHeight / 2 + shapesBefore[1].y, }) }) it('creates new bindings for arrows when pasting', async () => { const mockClipboard = doMockClipboard() editor .createShapes([ { id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, { id: ids.arrow1, type: 'arrow', x: 150, y: 150 }, ]) .createBindings([ { fromId: ids.arrow1, toId: ids.box1, type: 'arrow', props: { terminal: 'start', normalizedAnchor: { x: 0.5, y: 0.5 }, isExact: false, isPrecise: false, }, }, { fromId: ids.arrow1, toId: ids.box2, type: 'arrow', props: { terminal: 'end', normalizedAnchor: { x: 0.5, y: 0.5 }, isExact: false, isPrecise: false, }, }, ]) const shapesBefore = editor.getCurrentPageShapesSorted() editor.selectAll().cut() // Test the shape of the clipboard data. await assertClipboardOfCorrectShape(mockClipboard.current) editor.paste() const shapesAfter = editor.getCurrentPageShapesSorted() // The new shapes should match the old shapes, except for their id and the arrow's bindings! expect(shapesAfter.length).toBe(shapesBefore.length) expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id }) expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id }) expect(shapesAfter[2]).toMatchObject({ ...shapesBefore[2], id: shapesAfter[2].id }) expect(getArrowBindings(editor, shapesAfter[2] as TLArrowShape)).toMatchObject({ start: { toId: shapesAfter[0].id }, end: { toId: shapesAfter[1].id }, }) }) it('pastes in the right position after copying from within a group.', async () => { const mockClipboard = doMockClipboard() editor.createShapes([ { id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } }, { id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, ]) editor // Create group .selectAll() .groupShapes(editor.getSelectedShapeIds()) // Move the group .updateShapes([ { id: editor.getCurrentPageShapes()[2].id, type: 'group', x: 400, y: 400, }, ]) // Copy a shape from within the group .selectNone() .setSelectedShapes([ids.box1]) .copy() await assertClipboardOfCorrectShape(mockClipboard.current) // Paste the shape expect(editor.getCurrentPageShapes().length).toEqual(3) editor.paste() expect(editor.getCurrentPageShapes().length).toEqual(4) // Check if the position is correct const pastedShape = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1] const pastedPoint = { x: pastedShape.x, y: pastedShape.y } expect(pastedPoint).toMatchObject({ x: 150, y: 150 }) // center of group }) })