Playwright E2E testing for tldraw.
Install
mkdir -p .claude/skills/write-e2e-tests && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/985" && unzip -o skill.zip -d .claude/skills/write-e2e-tests && rm skill.zipInstalls to .claude/skills/write-e2e-tests
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Writing Playwright E2E tests for tldraw. Use when creating browser tests, testing UI interactions, or adding E2E coverage in apps/examples/e2e or apps/dotcom/client/e2e.Key capabilities
- →Write Playwright E2E tests for tldraw applications
- →Structure test files within `apps/examples/e2e/` or `apps/dotcom/client/e2e/`
- →Utilize shared fixtures for toolbar, menus, and other UI elements
- →Interact with the editor via `page.evaluate()` for manipulation and data retrieval
- →Handle platform-specific modifier keys and mobile-only test skipping
How it works
The skill provides patterns and helper functions for writing Playwright tests, including setup, UI interaction, and assertions, within the tldraw repository structure.
Inputs & outputs
When to use write-e2e-tests
- →Create browser tests
- →Test UI interactions
- →Add E2E coverage
About this skill
Writing E2E tests
E2E tests use Playwright. Located in apps/examples/e2e/ (SDK examples) and apps/dotcom/client/e2e/ (tldraw.com).
Test file structure
apps/examples/e2e/
├── fixtures/
│ ├── fixtures.ts # Test fixtures (toolbar, menus, etc.)
│ └── menus/ # Page object models
├── tests/
│ └── test-*.spec.ts # Test files
└── shared-e2e.ts # Shared utilities
Name test files test-<feature>.spec.ts.
Required declarations
When using page.evaluate() to access the editor or UI events:
import { Editor } from 'tldraw'
declare const editor: Editor
declare const __tldraw_ui_event: { name: string; data?: any }
Basic test structure
import { expect } from '@playwright/test'
import test from '../fixtures/fixtures'
import { setupOrReset } from '../shared-e2e'
test.describe('Feature name', () => {
test.beforeEach(setupOrReset)
test('does something', async ({ page, toolbar }) => {
// Test implementation
})
})
Setup patterns
Standard setup (recommended)
test.beforeEach(setupOrReset) // Smart: navigates first run, fast reset after
Shared page for performance
For tests that don't need full isolation:
let page: Page
test.describe('Feature', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test.beforeEach(async () => {
await hardResetEditor(page)
})
})
Setup with shapes
import { setupPageWithShapes, hardResetWithShapes } from '../shared-e2e'
test.beforeEach(async ({ browser }) => {
if (!page) {
page = await browser.newPage()
await setupPage(page)
} else {
await hardResetEditor(page)
}
await setupPageWithShapes(page)
})
Available fixtures
test('example', async ({
page, // Playwright page
toolbar, // Toolbar page object
stylePanel, // Style panel
actionsMenu, // Actions menu
mainMenu, // Main menu
pageMenu, // Page menu
navigationPanel, // Navigation panel
richTextToolbar, // Rich text toolbar
api, // tldrawApi methods
isMobile, // Mobile viewport check
isMac, // Mac platform check
}) => {})
Interacting with the editor
Via page.evaluate
// Execute code in browser context
await page.evaluate(() => {
editor.createShapes([{ type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }])
})
// Fast reset (faster than keyboard shortcuts)
await page.evaluate(() => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
editor.setCurrentTool('select')
})
// Get data from editor
const shape = await page.evaluate(() => editor.getOnlySelectedShape())
expect(shape).toMatchObject({ type: 'geo', x: 100, y: 100 })
Testing UI events
await page.keyboard.press('Control+a')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-all-shapes',
data: { source: 'kbd' },
})
Selecting tools and UI elements
By test ID
await page.getByTestId('tools.rectangle').click()
await page.getByTestId('tools.more.cloud').click() // In popover
await expect(page.getByTestId('tools.select')).toHaveAttribute('aria-pressed', 'true')
Via toolbar fixture
const { select, draw, arrow, rectangle } = toolbar.tools
await rectangle.click()
await toolbar.isSelected(rectangle)
await toolbar.isNotSelected(select)
// More tools popover
await toolbar.moreToolsButton.click()
await toolbar.popOverTools.popoverCloud.click()
Menu interactions
import { clickMenu, withMenu } from '../shared-e2e'
// Click a menu item
await clickMenu(page, 'main-menu.edit.copy')
await clickMenu(page, 'context-menu.copy-as.copy-as-png')
// Focus and interact with menu item
await page.mouse.click(200, 200, { button: 'right' })
await withMenu(page, 'context-menu.arrange.distribute-horizontal', (item) => item.focus())
await page.keyboard.press('Enter')
Data-driven tests
const tools = [
{ tool: 'rectangle', shape: 'geo' },
{ tool: 'arrow', shape: 'arrow' },
{ tool: 'draw', shape: 'draw' },
]
test('creates shapes with tools', async ({ page, toolbar }) => {
for (const { tool, shape } of tools) {
await page.getByTestId(`tools.${tool}`).click()
await page.mouse.click(200, 200)
expect(await getAllShapeTypes(page)).toContain(shape)
// Reset for next iteration
await page.evaluate(() => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
})
}
})
Platform-specific handling
Modifier keys
test('copy paste', async ({ page, isMac }) => {
const modifier = isMac ? 'Meta' : 'Control'
await page.keyboard.down(modifier)
await page.keyboard.press('KeyC')
await page.keyboard.press('KeyV')
await page.keyboard.up(modifier)
})
Skip on mobile
test('desktop only feature', async ({ isMobile }) => {
if (isMobile) return
// Desktop-specific test
})
Helper functions
import { getAllShapeTypes, getAllShapeLabels, sleep, sleepFrames } from '../shared-e2e'
// Get shape types on canvas
const shapes = await getAllShapeTypes(page)
expect(shapes).toEqual(['geo', 'arrow'])
// Wait for async operations
await sleep(100)
await sleepFrames(2) // Wait for animation frames
Assertions
// Shape assertions
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
type: 'geo',
props: { w: 100, h: 100 },
})
// Attribute assertions
await expect(page.getByTestId('tools.select')).toHaveAttribute('aria-pressed', 'true')
// CSS assertions (for selection state)
await expect(tool).toHaveCSS('color', 'rgb(255, 255, 255)')
// Visibility
await expect(toolbar.moreToolsPopover).toBeVisible()
await expect(toolbar.toolLock).toBeHidden()
Skipping flaky tests
test.describe.skip('clipboard tests', () => {
// Skipped because flaky in CI
})
test.skip('known issue', async () => {})
Running E2E tests
yarn e2e # Examples E2E
yarn e2e-dotcom # Dotcom E2E
yarn e2e-ui # With Playwright UI
yarn e2e -- --grep "toolbar" # Filter by pattern
Key patterns summary
- Use
setupOrResetinbeforeEachfor test isolation - Declare
editorand__tldraw_ui_eventforpage.evaluate() - Use
page.evaluate()for fast editor manipulation (faster than keyboard) - Use
getByTestId()withtools.<name>pattern for tool selection - Use
clickMenu()/withMenu()for menu interactions - Handle platform differences with
isMacandisMobilefixtures - Test against
localhost:5420/end-to-endexample
How it compares
This skill offers specific patterns and fixtures for tldraw's Playwright E2E tests, providing a structured approach compared to general Playwright test creation.
Compared to similar skills
write-e2e-tests side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| write-e2e-tests (this skill) | 3 | 3mo | Review | Intermediate |
| dependency-upgrade | 26 | 5mo | Review | Intermediate |
| angular-best-practices | 21 | 3mo | No flags | Advanced |
| validate-typescript | 5 | 9mo | Review | Beginner |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by tldraw
View all by tldraw →You might also like
dependency-upgrade
wshobson
Manage major dependency version upgrades with compatibility analysis, staged rollout, and comprehensive testing. Use when upgrading framework versions, updating major dependencies, or managing breaking changes in libraries.
angular-best-practices
sickn33
Angular performance optimization and best practices guide. Use when writing, reviewing, or refactoring Angular code for optimal performance, bundle size, and rendering efficiency.
validate-typescript
BerryKuipers
Run TypeScript compiler type-checking (tsc --noEmit) to validate type safety and catch type errors. Works with any TypeScript project. Returns structured output with error counts, categories (type/syntax/import errors), and affected files. Used for quality gates and pre-commit validation.
ts-testing
johnlindquist
Design, implement, and maintain high‑value TypeScript test suites using popular JS/TS testing libraries. Use this skill whenever the user is adding tests, debugging failing tests, or refactoring code that should be covered by tests.
react-best-practices
redpanda-data
Client-side React performance optimization patterns.
feature-flags
Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.