write-e2e-tests
作者: tldraw
为tldraw编写Playwright端到端测试。在创建浏览器测试、测试UI交互或在apps/examples/e2e等目录中添加端到端测试覆盖时使用。
npx skills add https://github.com/tldraw/tldraw --skill write-e2e-testsWriting 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
来自 tldraw 的更多技能
write-example
tldraw
为tldraw SDK示例应用编写示例。在创建新示例、添加SDK演示或在apps/examples中编写示例代码时使用。
official
write-issue
tldraw
在tldraw仓库中编写和维护GitHub问题的参考标准。当其他技能或工作流需要处理问题时,可作为辅助指南。
official
write-pr
tldraw
在 tldraw 仓库中编写拉取请求标题和描述的参考标准。当其他技能或工作流需要时,作为辅助指南使用…
official
write-release-notes
tldraw
为 tldraw SDK 版本编写发布说明文章。在创建新的发布文档、从头起草发布说明或审阅发布…时使用。
official
write-tbp
tldraw
撰写关于tldraw功能及实现细节的技术博客文章。适用于创作tldraw如何解决有趣问题的博客内容。
official
write-unit-tests
tldraw
为tldraw SDK编写单元测试和集成测试。在创建新测试、增加测试覆盖率或修复packages/editor或…中的失败测试时使用。
official
clean-copy
tldraw
在当前分支上重新实现一个新分支,并生成清晰、叙事性强的Git提交历史。当被要求创建干净的分支副本、清理提交记录时使用。
official
commit-changes
tldraw
为当前更改创建git提交。当被要求提交更改、进行提交、生成提交信息或提交当前工作树时使用…
official