write-e2e-tests by tldraw/tldraw
npx skills add https://github.com/tldraw/tldraw --skill write-e2e-tests端到端测试使用 Playwright。位于 apps/examples/e2e/(SDK 示例)和 apps/dotcom/client/e2e/(tldraw.com)。
apps/examples/e2e/
├── fixtures/
│ ├── fixtures.ts # 测试夹具(工具栏、菜单等)
│ └── menus/ # 页面对象模型
├── tests/
│ └── test-*.spec.ts # 测试文件
└── shared-e2e.ts # 共享工具函数
测试文件命名为 test-<功能>.spec.ts。
当使用 page.evaluate() 访问编辑器或 UI 事件时:
import { Editor } from 'tldraw'
declare const editor: Editor
declare const __tldraw_ui_event: { name: string; data?: any }
import { expect } from '@playwright/test'
import test from '../fixtures/fixtures'
import { setupOrReset } from '../shared-e2e'
test.describe('功能名称', () => {
test.beforeEach(setupOrReset)
test('执行某些操作', async ({ page, toolbar }) => {
// 测试实现
})
})
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
test.beforeEach(setupOrReset) // 智能:首次运行导航,之后快速重置
对于不需要完全隔离的测试:
let page: Page
test.describe('功能', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test.beforeEach(async () => {
await hardResetEditor(page)
})
})
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)
})
test('示例', async ({
page, // Playwright 页面
toolbar, // 工具栏页面对象
stylePanel, // 样式面板
actionsMenu, // 操作菜单
mainMenu, // 主菜单
pageMenu, // 页面菜单
navigationPanel, // 导航面板
richTextToolbar, // 富文本工具栏
api, // tldrawApi 方法
isMobile, // 移动端视口检查
isMac, // Mac 平台检查
}) => {})
// 在浏览器上下文中执行代码
await page.evaluate(() => {
editor.createShapes([{ type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }])
})
// 快速重置(比键盘快捷键更快)
await page.evaluate(() => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
editor.setCurrentTool('select')
})
// 从编辑器获取数据
const shape = await page.evaluate(() => editor.getOnlySelectedShape())
expect(shape).toMatchObject({ type: 'geo', x: 100, y: 100 })
await page.keyboard.press('Control+a')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-all-shapes',
data: { source: 'kbd' },
})
await page.getByTestId('tools.rectangle').click()
await page.getByTestId('tools.more.cloud').click() // 在弹出层中
await expect(page.getByTestId('tools.select')).toHaveAttribute('aria-pressed', 'true')
const { select, draw, arrow, rectangle } = toolbar.tools
await rectangle.click()
await toolbar.isSelected(rectangle)
await toolbar.isNotSelected(select)
// 更多工具弹出层
await toolbar.moreToolsButton.click()
await toolbar.popOverTools.popoverCloud.click()
import { clickMenu, withMenu } from '../shared-e2e'
// 点击菜单项
await clickMenu(page, 'main-menu.edit.copy')
await clickMenu(page, 'context-menu.copy-as.copy-as-png')
// 聚焦并交互菜单项
await page.mouse.click(200, 200, { button: 'right' })
await withMenu(page, 'context-menu.arrange.distribute-horizontal', (item) => item.focus())
await page.keyboard.press('Enter')
const tools = [
{ tool: 'rectangle', shape: 'geo' },
{ tool: 'arrow', shape: 'arrow' },
{ tool: 'draw', shape: 'draw' },
]
test('使用工具创建形状', 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)
// 为下一次迭代重置
await page.evaluate(() => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
})
}
})
test('复制粘贴', 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)
})
test('仅限桌面的功能', async ({ isMobile }) => {
if (isMobile) return
// 桌面特定测试
})
import { getAllShapeTypes, getAllShapeLabels, sleep, sleepFrames } from '../shared-e2e'
// 获取画布上的形状类型
const shapes = await getAllShapeTypes(page)
expect(shapes).toEqual(['geo', 'arrow'])
// 等待异步操作
await sleep(100)
await sleepFrames(2) // 等待动画帧
// 形状断言
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
type: 'geo',
props: { w: 100, h: 100 },
})
// 属性断言
await expect(page.getByTestId('tools.select')).toHaveAttribute('aria-pressed', 'true')
// CSS 断言(用于选择状态)
await expect(tool).toHaveCSS('color', 'rgb(255, 255, 255)')
// 可见性
await expect(toolbar.moreToolsPopover).toBeVisible()
await expect(toolbar.toolLock).toBeHidden()
test.describe.skip('剪贴板测试', () => {
// 在 CI 中不稳定,已跳过
})
test.skip('已知问题', async () => {})
yarn e2e # 示例端到端测试
yarn e2e-dotcom # 网站端到端测试
yarn e2e-ui # 使用 Playwright UI
yarn e2e -- --grep "toolbar" # 按模式过滤
beforeEach 中使用 setupOrReset 进行测试隔离page.evaluate() 声明 editor 和 __tldraw_ui_eventpage.evaluate() 进行快速编辑器操作(比键盘更快)getByTestId() 和 tools.<名称> 模式选择工具clickMenu() / withMenu() 进行菜单交互isMac 和 isMobile 夹具处理平台差异localhost:5420/end-to-end 示例进行测试每周安装量
184
代码仓库
GitHub 星标
46.0K
首次出现
2026年1月22日
安全审计
安装于
gemini-cli148
opencode143
codex139
antigravity134
github-copilot134
cursor124
E2E tests use Playwright. Located in apps/examples/e2e/ (SDK examples) and apps/dotcom/client/e2e/ (tldraw.com).
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.
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 }
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
})
})
test.beforeEach(setupOrReset) // Smart: navigates first run, fast reset after
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)
})
})
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)
})
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
}) => {})
// 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 })
await page.keyboard.press('Control+a')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-all-shapes',
data: { source: 'kbd' },
})
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')
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()
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')
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())
})
}
})
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)
})
test('desktop only feature', async ({ isMobile }) => {
if (isMobile) return
// Desktop-specific test
})
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
// 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()
test.describe.skip('clipboard tests', () => {
// Skipped because flaky in CI
})
test.skip('known issue', async () => {})
yarn e2e # Examples E2E
yarn e2e-dotcom # Dotcom E2E
yarn e2e-ui # With Playwright UI
yarn e2e -- --grep "toolbar" # Filter by pattern
setupOrReset in beforeEach for test isolationeditor and __tldraw_ui_event for page.evaluate()page.evaluate() for fast editor manipulation (faster than keyboard)getByTestId() with tools.<name> pattern for tool selectionclickMenu() / withMenu() for menu interactionsWeekly Installs
184
Repository
GitHub Stars
46.0K
First Seen
Jan 22, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli148
opencode143
codex139
antigravity134
github-copilot134
cursor124
Skills CLI 使用指南:AI Agent 技能包管理器安装与管理教程
33,600 周安装
isMacisMobilelocalhost:5420/end-to-end example