🎯

write-unit-tests

🎯Skill

from eyh0602/skillshub

VibeIndex|
What it does

Generates comprehensive unit and integration tests for tldraw SDK using Vitest, covering editor functionality, shapes, tools, and test file organization.

πŸ“¦

Part of

eyh0602/skillshub(14 items)

write-unit-tests

Installation

CargoRun with Cargo (Rust)
cargo install skillshub
git cloneClone repository
git clone https://github.com/EYH0602/skillshub
CargoRun with Cargo (Rust)
cargo install --path .
PythonRun Python server
python -m pip install --user pre-commit
πŸ“– Extracted from docs: eyh0602/skillshub
2Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Writing unit and integration tests for the tldraw SDK. Use when creating new tests, adding test coverage, or fixing failing tests in packages/editor or packages/tldraw. Covers Vitest patterns, TestEditor usage, and test file organization.

Overview

# Writing tests

Unit and integration tests use Vitest. Tests run from workspace directories, not the repo root.

Test file locations

Unit tests - alongside source files:

```

packages/editor/src/lib/primitives/Vec.ts

packages/editor/src/lib/primitives/Vec.test.ts # Same directory

```

Integration tests - in src/test/ directory:

```

packages/tldraw/src/test/SelectTool.test.ts

packages/tldraw/src/test/commands/createShape.test.ts

```

Shape/tool tests - alongside the implementation:

```

packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts

packages/tldraw/src/lib/shapes/arrow/ArrowShapeTool.test.ts

```

Which workspace to test in

  • packages/editor: Core primitives, geometry, managers, base editor functionality
  • packages/tldraw: Anything needing default shapes/tools (most integration tests)

```bash

cd packages/tldraw && yarn test run

cd packages/tldraw && yarn test run --grep "SelectTool"

```

TestEditor vs Editor

Use TestEditor for integration tests (includes default shapes/tools):

```typescript

import { createShapeId } from '@tldraw/editor'

import { TestEditor } from './TestEditor'

let editor: TestEditor

beforeEach(() => {

editor = new TestEditor()

editor.selectAll().deleteShapes(editor.getSelectedShapeIds())

})

afterEach(() => {

editor?.dispose()

})

```

Use raw Editor when testing editor setup or custom configurations:

```typescript

import { Editor, createTLStore } from '@tldraw/editor'

beforeEach(() => {

editor = new Editor({

shapeUtils: [CustomShape],

bindingUtils: [],

tools: [CustomTool],

store: createTLStore({ shapeUtils: [CustomShape], bindingUtils: [] }),

getContainer: () => document.body,

})

})

```

Common TestEditor methods

```typescript

// Pointer simulation

editor.pointerDown(x, y, options?)

editor.pointerMove(x, y, options?)

editor.pointerUp(x, y, options?)

editor.click(x, y, shapeId?)

editor.doubleClick(x, y, shapeId?)

// Keyboard simulation

editor.keyDown(key, options?)

editor.keyUp(key, options?)

// State assertions

editor.expectToBeIn('select.idle')

editor.expectToBeIn('select.crop.idle')

// Shape assertions

editor.expectShapeToMatch({ id, x, y, props: { ... } })

// Shape operations

editor.createShapes([{ id, type, x, y, props }])

editor.updateShapes([{ id, type, props }])

editor.getShape(id)

editor.select(id1, id2)

editor.selectAll()

editor.selectNone()

editor.getSelectedShapeIds()

editor.getOnlySelectedShape()

// Tool operations

editor.setCurrentTool('arrow')

editor.getCurrentToolId()

// Undo/redo

editor.undo()

editor.redo()

```

Pointer event options

```typescript

editor.pointerDown(100, 100, {

target: 'shape', // 'canvas' | 'shape' | 'handle' | 'selection'

shape: editor.getShape(id),

})

editor.pointerDown(150, 300, {

target: 'selection',

handle: 'bottom', // 'top' | 'bottom' | 'left' | 'right' | corners

})

editor.doubleClick(550, 550, {

target: 'selection',

handle: 'bottom_right',

})

```

Setup patterns

Standard setup with shape IDs

```typescript

const ids = {

box1: createShapeId('box1'),

box2: createShapeId('box2'),

arrow1: createShapeId('arrow1'),

}

vi.useFakeTimers()

beforeEach(() => {

editor = new TestEditor()

editor.selectAll().deleteShapes(editor.getSelectedShapeIds())

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 } },

])

})

afterEach(() => {

editor?.dispose()

})

```

Reusable props

```typescript

const imageProps = {

assetId: null,

playing: true,

url: '',

w: 1200,

h: 800,

}

editor.createShapes([

{ id: ids.imageA, type: 'image', x: 100, y: 100, props: imageProps },

{ id: ids.imageB, type: 'image', x: 500, y: 500, props: { ...imageProps, w: 600, h: 400 } },

])

```

Helper functions

```typescript

function arrow(id = ids.arrow1) {

return editor.getShape(id) as TLArrowShape

}

function bindings(id = ids.arrow1) {

return getArrowBindings(editor, arrow(id))

}

```

Mocking with vi.spyOn

```typescript

// Mock return value

vi.spyOn(editor, 'getIsReadonly').mockReturnValue(true)

// Mock implementation

const isHiddenSpy = vi.spyOn(editor, 'isShapeHidden')

isHiddenSpy.mockImplementation((shape) => shape.id === ids.hiddenShape)

// Verify calls

const spy = vi.spyOn(editor, 'setSelectedShapes')

editor.selectAll()

expect(spy).toHaveBeenCalled()

expect(spy).not.toHaveBeenCalled()

// Always restore

isHiddenSpy.mockRestore()

```

Fake timers

```typescript

vi.useFakeTimers()

// Mock animation frame

window.requestAnimationFrame = (cb) => setTimeout(cb, 1000 / 60)

window.cancelAnimationFrame = (id) => clearTimeout(id)

it('handles animation', () => {

editor.alignShapes(editor.getSelectedShapeIds(), 'right')

vi.advanceTimersByTime(1000)

// Assert after animation completes

})

```

Assertions

Shape matching

```typescript

// Partial matching (most common)

expect(editor.getShape(id)).toMatchObject({

type: 'geo',

x: 100,

props: { w: 100 },

})

editor.expectShapeToMatch({

id: ids.box1,

x: 350,

y: 350,

})

// Floating point matching (custom matcher)

expect(result).toCloselyMatchObject({

props: { normalizedAnchor: { x: 0.5, y: 0.75 } },

})

```

Array assertions

```typescript

expect(editor.getSelectedShapeIds()).toMatchObject([ids.box1])

expect(Array.from(selectedIds).sort()).toEqual([id1, id2, id3].sort())

expect(shapes).toContain('geo')

expect(shapes).not.toContain(ids.lockedShape)

```

State assertions

```typescript

editor.expectToBeIn('select.idle')

editor.expectToBeIn('select.brushing')

editor.expectToBeIn('select.crop.idle')

```

Testing undo/redo

```typescript

it('handles undo/redo', () => {

editor.doubleClick(550, 550, ids.image)

editor.expectToBeIn('select.crop.idle')

editor.updateShape({ id: ids.image, type: 'image', props: { crop: newCrop } })

editor.undo()

editor.expectToBeIn('select.crop.idle')

expect(editor.getShape(ids.image)!.props.crop).toMatchObject(originalCrop)

editor.redo()

expect(editor.getShape(ids.image)!.props.crop).toMatchObject(newCrop)

})

```

Testing TypeScript types

```typescript

it('Uses typescript generics', () => {

expect(() => {

// @ts-expect-error - wrong props type

editor.createShape({ id, type: 'geo', props: { w: 'OH NO' } })

// @ts-expect-error - unknown prop

editor.createShape({ id, type: 'geo', props: { foo: 'bar' } })

// Valid

editor.createShape({ id, type: 'geo', props: { w: 100 } })

}).toThrow()

})

```

Testing custom shapes

```typescript

declare module '@tldraw/tlschema' {

export interface TLGlobalShapePropsMap {

'my-custom-shape': { w: number; h: number; text: string | undefined }

}

}

class CustomShape extends ShapeUtil {

static override type = 'my-custom-shape'

static override props: RecordProps = {

w: T.number,

h: T.number,

text: T.string.optional(),

}

getDefaultProps() {

return { w: 200, h: 200, text: '' }

}

getGeometry(shape) {

return new Rectangle2d({ width: shape.props.w, height: shape.props.h })

}

indicator() {}

component() {}

}

```

Testing side effects

```typescript

beforeEach(() => {

editor = new TestEditor()

editor.sideEffects.registerAfterChangeHandler('instance_page_state', (prev, next) => {

if (prev.croppingShapeId !== next.croppingShapeId) {

// Handle state change

}

})

})

```

Testing events

```typescript

it('emits wheel events', () => {

const handler = vi.fn()

editor.on('event', handler)

editor.dispatch({

type: 'wheel',

name: 'wheel',

delta: { x: 0, y: 10, z: 0 },

point: { x: 100, y: 100, z: 1 },

shiftKey: false,

// ... other modifiers

})

editor.emit('tick', 16) // Flush batched events

expect(handler).toHaveBeenCalledWith(expect.objectContaining({ name: 'wheel' }))

})

```

Method chaining

```typescript

editor

.expectToBeIn('select.idle')

.select(ids.imageA, ids.imageB)

.doubleClick(550, 550, { target: 'selection', handle: 'bottom_right' })

.expectToBeIn('select.idle')

editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(100, 100).pointerUp()

```

Running tests

```bash

cd packages/tldraw && yarn test run

cd packages/tldraw && yarn test run --grep "arrow"

cd packages/editor && yarn test run --grep "Vec"

# Watch mode

cd packages/tldraw && yarn test

```

Key patterns summary

  • Use createShapeId() for shape IDs
  • Use vi.useFakeTimers() for time-dependent behavior
  • Clear shapes in beforeEach, dispose in afterEach
  • Test in packages/tldraw for shapes/tools
  • Use expectToBeIn() for state machine assertions
  • Use toMatchObject() for partial matching
  • Use toCloselyMatchObject() for floating point values
  • Mock with vi.spyOn() and always mockRestore()

More from this repository10