import { createBindingId, createShapeId } 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'), } vi.useFakeTimers() beforeEach(() => { editor = new TestEditor() editor .selectAll() .deleteShapes(editor.getSelectedShapeIds()) .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.box3, type: 'geo', x: 100, y: 500, props: { w: 100, h: 100 } }, { id: ids.box4, type: 'geo', x: 100, y: 800, props: { w: 100, h: 100 } }, { id: ids.arrow1, type: 'arrow', x: 150, y: 150, props: { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, }, }, ]) .createBindings([ { id: createBindingId(), fromId: ids.arrow1, toId: ids.box1, type: 'arrow', props: { terminal: 'start', isExact: false, normalizedAnchor: { x: 0.5, y: 0.5 }, isPrecise: false, }, }, { id: createBindingId(), fromId: ids.arrow1, toId: ids.box2, type: 'arrow', props: { terminal: 'end', isExact: false, normalizedAnchor: { x: 0.5, y: 0.5 }, isPrecise: false, }, }, ]) }) describe('Editor.deleteShapes', () => { it('Deletes a shape', () => { editor.select(ids.box3, ids.box4) editor.markHistoryStoppingPoint('before deleting') editor.deleteShapes(editor.getSelectedShapeIds()) // delete the selected shapes expect(editor.getShape(ids.box3)).toBeUndefined() expect(editor.getShape(ids.box4)).toBeUndefined() expect(editor.getSelectedShapeIds()).toMatchObject([]) editor.undo() expect(editor.getShape(ids.box3)).not.toBeUndefined() expect(editor.getShape(ids.box4)).not.toBeUndefined() expect(editor.getSelectedShapeIds()).toMatchObject([ids.box3, ids.box4]) editor.redo() expect(editor.getShape(ids.box3)).toBeUndefined() expect(editor.getShape(ids.box4)).toBeUndefined() expect(editor.getSelectedShapeIds()).toMatchObject([]) }) it('Does nothing on an empty ids array', () => { editor.selectNone() const before = editor.store.serialize() editor.deleteShapes(editor.getSelectedShapeIds()) // should be a noop, nothing to delete expect(editor.store.serialize()).toStrictEqual(before) }) it('Deletes descendants', () => { editor.reparentShapes([ids.box4], ids.box3) editor.select(ids.box3) editor.markHistoryStoppingPoint('before deleting') editor.deleteShapes(editor.getSelectedShapeIds()) // should be a noop, nothing to delete expect(editor.getShape(ids.box3)).toBeUndefined() expect(editor.getShape(ids.box4)).toBeUndefined() editor.undo() expect(editor.getShape(ids.box3)).not.toBeUndefined() expect(editor.getShape(ids.box4)).not.toBeUndefined() editor.redo() expect(editor.getShape(ids.box3)).toBeUndefined() expect(editor.getShape(ids.box4)).toBeUndefined() }) it('cleans selected ids from page state once when deleting many selected shapes', () => { const shapeIds = Array.from({ length: 32 }, (_, i) => createShapeId(`bulk-${i}`)) editor.createShapes( shapeIds.map((id, i) => ({ id, type: 'geo', x: i * 10, y: i * 10, props: { w: 100, h: 100 }, })) ) editor.setSelectedShapes(shapeIds) editor.markHistoryStoppingPoint('before deleting') const putSpy = vi.spyOn(editor.store, 'put') editor.deleteShapes(editor.getSelectedShapeIds()) expect(editor.getSelectedShapeIds()).toEqual([]) expect(shapeIds.every((id) => !editor.getShape(id))).toBe(true) const pageStatePutCalls = putSpy.mock.calls.filter(([records]) => records.some((record) => record.typeName === 'instance_page_state') ) expect(pageStatePutCalls).toHaveLength(1) expect(pageStatePutCalls[0][0]).toMatchObject([ { typeName: 'instance_page_state', selectedShapeIds: [] }, ]) putSpy.mockRestore() editor.undo() expect(shapeIds.every((id) => editor.getShape(id))).toBe(true) expect(editor.getSelectedShapeIds()).toEqual(shapeIds) }) it('does not crash when deleting a bent arrow and two adjacent bound shapes', () => { const leftId = createShapeId('left') const rightId = createShapeId('right') const bentArrowId = createShapeId('bent-arrow') editor.createShapes([ { id: leftId, type: 'geo', x: 500, y: 500, props: { w: 100, h: 100 } }, { id: rightId, type: 'geo', x: 600, y: 500, props: { w: 100, h: 100 } }, { id: bentArrowId, type: 'arrow', x: 550, y: 550, props: { start: { x: 0, y: 0 }, end: { x: 100, y: 0 }, bend: -120, }, }, ]) editor.createBindings([ { id: createBindingId(), fromId: bentArrowId, toId: leftId, type: 'arrow', props: { terminal: 'start', isExact: false, normalizedAnchor: { x: 1, y: 0.5 }, isPrecise: false, }, }, { id: createBindingId(), fromId: bentArrowId, toId: rightId, type: 'arrow', props: { terminal: 'end', isExact: false, normalizedAnchor: { x: 0, y: 0.5 }, isPrecise: false, }, }, ]) expect(() => editor.deleteShapes([leftId, bentArrowId, rightId])).not.toThrow() }) }) describe('When deleting arrows', () => { function bindings() { return getArrowBindings(editor, editor.getShape(ids.arrow1)!) } it('Restores any bindings on undo', () => { editor.select(ids.arrow1) editor.markHistoryStoppingPoint('before deleting') expect(bindings().start).toBeDefined() expect(bindings().end).toBeDefined() editor.deleteShapes(editor.getSelectedShapeIds()) // delete the selected shapes expect(editor.store.query.records('binding').get()).toHaveLength(0) editor.undo() expect(bindings().start).toBeDefined() expect(bindings().end).toBeDefined() }) })