write-e2e-tests

bởi tldraw

Viết bài kiểm tra Playwright E2E cho tldraw. Sử dụng khi tạo kiểm tra trình duyệt, kiểm tra tương tác UI, hoặc thêm phạm vi E2E trong apps/examples/e2e hoặc…

npx skills add https://github.com/tldraw/tldraw --skill write-e2e-tests

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

Thêm skills từ tldraw

write-example
tldraw
Viết các ví dụ cho ứng dụng ví dụ của tldraw SDK. Sử dụng khi tạo ví dụ mới, thêm minh họa SDK, hoặc viết mã ví dụ trong apps/examples.
official
write-issue
tldraw
Các tiêu chuẩn tham khảo để viết và duy trì các issue GitHub trong kho lưu trữ tldraw. Sử dụng như hướng dẫn hỗ trợ khi một kỹ năng hoặc quy trình làm việc khác cần issue…
official
write-pr
tldraw
Các tiêu chuẩn tham khảo để viết tiêu đề và mô tả pull request trong kho lưu trữ tldraw. Sử dụng như hướng dẫn hỗ trợ khi một kỹ năng hoặc quy trình làm việc khác cần…
official
write-release-notes
tldraw
Viết bài ghi chú phát hành cho các bản phát hành SDK tldraw. Sử dụng khi tạo tài liệu phát hành mới, soạn thảo ghi chú phát hành từ đầu, hoặc xem xét phát hành…
official
write-tbp
tldraw
Viết bài blog kỹ thuật về các tính năng và chi tiết triển khai của tldraw. Sử dụng khi tạo nội dung blog về cách tldraw giải quyết các vấn đề thú vị.
official
write-unit-tests
tldraw
Viết unit test và integration test cho tldraw SDK. Sử dụng khi tạo test mới, thêm phạm vi kiểm thử, hoặc sửa test lỗi trong packages/editor hoặc…
official
clean-copy
tldraw
Triển khai lại nhánh hiện tại trên một nhánh mới với lịch sử commit git sạch sẽ, có chất lượng tường thuật. Sử dụng khi được yêu cầu tạo một nhánh sao chép sạch, dọn dẹp commit…
official
commit-changes
tldraw
Tạo một git commit cho các thay đổi hiện tại. Sử dụng khi được yêu cầu commit các thay đổi, tạo commit, tạo thông điệp commit, hoặc commit worktree hiện tại với…
official