UNPKG

tldraw

Version:

A tiny little drawing editor.

342 lines (277 loc) • 10.1 kB
import { createShapeId } from '@tldraw/editor' import { vi } from 'vitest' import { TestEditor } from './TestEditor' let editor: TestEditor beforeEach(() => { editor = new TestEditor({ options: { maxShapesPerPage: 5 }, // Set a low limit for testing }) }) describe('Maximum shapes behavior', () => { describe('when maximum shapes limit is reached', () => { beforeEach(() => { // Create shapes up to the limit (5 shapes) const shapesToCreate = Array.from({ length: 5 }, (_, i) => ({ id: createShapeId(`shape-${i}`), type: 'geo' as const, x: i * 50, y: 0, props: { w: 40, h: 40 }, })) editor.createShapes(shapesToCreate) // Verify we're at the limit expect(editor.getCurrentPageShapeIds().size).toBe(5) expect(editor.canCreateShapes([{ type: 'geo' }])).toBe(false) }) describe('note shape creation', () => { it('should handle click creation gracefully without crashing', () => { // Set up the note tool editor.setCurrentTool('note') editor.expectToBeIn('note.idle') // Simulate clicking to create a note shape - when max shapes is reached, // the note tool immediately cancels and stays in idle expect(() => { editor.pointerDown(300, 100) }).not.toThrow() // The note tool should immediately cancel when shape creation fails editor.expectToBeIn('note.idle') // Complete the pointer interaction expect(() => { editor.pointerUp(300, 100) }).not.toThrow() // The tool should remain in idle state editor.expectToBeIn('note.idle') // Verify no new shapes were created expect(editor.getCurrentPageShapeIds().size).toBe(5) }) it('should handle drag creation gracefully without crashing', () => { // Set up the note tool editor.setCurrentTool('note') editor.expectToBeIn('note.idle') // Simulate starting to create a note shape - when max shapes is reached, // the note tool immediately cancels and stays in idle expect(() => { editor.pointerDown(300, 100) }).not.toThrow() // The note tool should immediately cancel when shape creation fails editor.expectToBeIn('note.idle') // Move and complete the interaction expect(() => { editor.pointerMove(350, 150) editor.pointerUp(350, 150) }).not.toThrow() // The tool should remain in idle state editor.expectToBeIn('note.idle') // Verify no new shapes were created expect(editor.getCurrentPageShapeIds().size).toBe(5) }) it('should emit max-shapes event when limit is reached', () => { const maxShapesHandler = vi.fn() editor.addListener('max-shapes', maxShapesHandler) // Set up the note tool editor.setCurrentTool('note') // Try to create a shape that would exceed the limit editor.pointerDown(300, 100).pointerUp(300, 100) // The max-shapes event should have been emitted expect(maxShapesHandler).toHaveBeenCalledWith({ name: expect.any(String), pageId: editor.getCurrentPageId(), count: 5, }) }) it('should properly handle cancellation during pointing state', () => { // With the fix, the note tool immediately cancels when max shapes reached, // so we test cancellation when the tool is in idle state editor.setCurrentTool('note') editor.expectToBeIn('note.idle') // Cancel should work without throwing even when already in idle expect(() => { editor.cancel() }).not.toThrow() // Should transition to select tool when cancelled from idle editor.expectToBeIn('select.idle') }) it('should properly handle interruption during pointing state', () => { // With the fix, the note tool immediately cancels when max shapes reached, // so we test interruption when the tool is in idle state editor.setCurrentTool('note') editor.expectToBeIn('note.idle') // Interrupt should work without throwing even when already in idle expect(() => { editor.interrupt() }).not.toThrow() editor.expectToBeIn('note.idle') }) }) describe('geo shape creation', () => { it('should handle geo shape creation gracefully when limit is reached', () => { // Set up the geo tool editor.setCurrentTool('geo') editor.expectToBeIn('geo.idle') // Simulate clicking to create a geo shape editor.pointerDown(300, 100) editor.expectToBeIn('geo.pointing') // Complete the click without dragging expect(() => { editor.pointerUp(300, 100) }).not.toThrow() // The tool should handle the failure gracefully editor.expectToBeIn('geo.idle') // Verify no new shapes were created expect(editor.getCurrentPageShapeIds().size).toBe(5) }) it('should handle geo shape drag creation gracefully when limit is reached', () => { // Set up the geo tool editor.setCurrentTool('geo') editor.expectToBeIn('geo.idle') // Simulate dragging to create a geo shape editor.pointerDown(300, 100) editor.expectToBeIn('geo.pointing') // Move to trigger drag (which attempts to create a shape) expect(() => { editor.pointerMove(350, 150) }).not.toThrow() // Complete the interaction expect(() => { editor.pointerUp(350, 150) }).not.toThrow() // Verify no new shapes were created expect(editor.getCurrentPageShapeIds().size).toBe(5) }) }) describe('frame shape creation', () => { it('should handle frame shape creation gracefully when limit is reached', () => { // Set up the frame tool editor.setCurrentTool('frame') editor.expectToBeIn('frame.idle') // Simulate clicking to create a frame shape editor.pointerDown(300, 100) editor.expectToBeIn('frame.pointing') // Complete the click without dragging expect(() => { editor.pointerUp(300, 100) }).not.toThrow() // The tool should handle the failure gracefully editor.expectToBeIn('frame.idle') // Verify no new shapes were created expect(editor.getCurrentPageShapeIds().size).toBe(5) }) }) }) describe('when shapes can still be created', () => { beforeEach(() => { // Create only 3 shapes, leaving room for 2 more const shapesToCreate = Array.from({ length: 3 }, (_, i) => ({ id: createShapeId(`shape-${i}`), type: 'geo' as const, x: i * 50, y: 0, props: { w: 40, h: 40 }, })) editor.createShapes(shapesToCreate) // Verify we can still create shapes expect(editor.getCurrentPageShapeIds().size).toBe(3) expect(editor.canCreateShapes([{ type: 'geo' }])).toBe(true) }) it('should create note shapes normally when under the limit', () => { // Set up the note tool editor.setCurrentTool('note') editor.expectToBeIn('note.idle') // Simulate creating a note shape editor.pointerDown(300, 100) editor.expectToBeIn('note.pointing') editor.pointerUp(300, 100) // Should have successfully created a shape and entered editing mode expect(editor.getCurrentPageShapeIds().size).toBe(4) editor.expectToBeIn('select.editing_shape') }) it('should create geo shapes normally when under the limit', () => { // Set up the geo tool editor.setCurrentTool('geo') editor.expectToBeIn('geo.idle') // Simulate creating a geo shape editor.pointerDown(300, 100) editor.expectToBeIn('geo.pointing') editor.pointerUp(300, 100) // Should have successfully created a shape expect(editor.getCurrentPageShapeIds().size).toBe(4) // After creating a shape, tool transitions to select.idle editor.expectToBeIn('select.idle') }) it('should create shapes normally with drag when under the limit', () => { // Set up the geo tool editor.setCurrentTool('geo') editor.expectToBeIn('geo.idle') // Simulate creating a shape with drag editor.pointerDown(300, 100) editor.expectToBeIn('geo.pointing') editor.pointerMove(350, 150) editor.expectToBeIn('select.resizing') // Enters resizing mode during drag creation editor.pointerUp(350, 150) // Should have successfully created a shape expect(editor.getCurrentPageShapeIds().size).toBe(4) }) }) describe('direct createShapes API behavior', () => { it('should return early and not create shapes when limit is reached', () => { // Fill to capacity const shapesToCreate = Array.from({ length: 5 }, (_, i) => ({ id: createShapeId(`shape-${i}`), type: 'geo' as const, x: i * 50, y: 0, props: { w: 40, h: 40 }, })) editor.createShapes(shapesToCreate) expect(editor.getCurrentPageShapeIds().size).toBe(5) // Try to create one more shape const maxShapesHandler = vi.fn() editor.addListener('max-shapes', maxShapesHandler) const extraShapeId = createShapeId('extra-shape') editor.createShapes([ { id: extraShapeId, type: 'geo', x: 300, y: 100, props: { w: 40, h: 40 }, }, ]) // Should not have created the extra shape expect(editor.getCurrentPageShapeIds().size).toBe(5) expect(editor.getShape(extraShapeId)).toBeUndefined() // Should have emitted the max-shapes event expect(maxShapesHandler).toHaveBeenCalledWith({ name: expect.any(String), pageId: editor.getCurrentPageId(), count: 5, }) }) it('should create shapes normally when under the limit', () => { // Create 3 shapes const shapesToCreate = Array.from({ length: 3 }, (_, i) => ({ id: createShapeId(`shape-${i}`), type: 'geo' as const, x: i * 50, y: 0, props: { w: 40, h: 40 }, })) editor.createShapes(shapesToCreate) expect(editor.getCurrentPageShapeIds().size).toBe(3) // Should be able to create 2 more const moreShapes = Array.from({ length: 2 }, (_, i) => ({ id: createShapeId(`more-shape-${i}`), type: 'geo' as const, x: (i + 3) * 50, y: 0, props: { w: 40, h: 40 }, })) editor.createShapes(moreShapes) // Should have created all shapes expect(editor.getCurrentPageShapeIds().size).toBe(5) moreShapes.forEach((shape) => { expect(editor.getShape(shape.id)).toBeDefined() }) }) }) })