snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
288
skills/write-e2e-tests/SKILL.md
Normal file
288
skills/write-e2e-tests/SKILL.md
Normal file
@@ -0,0 +1,288 @@
|
||||
---
|
||||
name: write-e2e-tests
|
||||
description: 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.
|
||||
---
|
||||
|
||||
# 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:
|
||||
|
||||
```typescript
|
||||
import { Editor } from 'tldraw'
|
||||
|
||||
declare const editor: Editor
|
||||
declare const __tldraw_ui_event: { name: string; data?: any }
|
||||
```
|
||||
|
||||
## Basic test structure
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
test.beforeEach(setupOrReset) // Smart: navigates first run, fast reset after
|
||||
```
|
||||
|
||||
### Shared page for performance
|
||||
|
||||
For tests that don't need full isolation:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
test('desktop only feature', async ({ isMobile }) => {
|
||||
if (isMobile) return
|
||||
// Desktop-specific test
|
||||
})
|
||||
```
|
||||
|
||||
## Helper functions
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
test.describe.skip('clipboard tests', () => {
|
||||
// Skipped because flaky in CI
|
||||
})
|
||||
|
||||
test.skip('known issue', async () => {})
|
||||
```
|
||||
|
||||
## Running E2E tests
|
||||
|
||||
```bash
|
||||
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 `setupOrReset` in `beforeEach` for test isolation
|
||||
- Declare `editor` and `__tldraw_ui_event` for `page.evaluate()`
|
||||
- Use `page.evaluate()` for fast editor manipulation (faster than keyboard)
|
||||
- Use `getByTestId()` with `tools.<name>` pattern for tool selection
|
||||
- Use `clickMenu()` / `withMenu()` for menu interactions
|
||||
- Handle platform differences with `isMac` and `isMobile` fixtures
|
||||
- Test against `localhost:5420/end-to-end` example
|
||||
Reference in New Issue
Block a user