56 results available
Sign in
Skills directory

write-e2e-tests

tldraw

·

Last changed
Hardened

This skill has been hardened by Chainguard

Last changed

This version passed the hardening gate. Hardening applied 6 targeted changes and preserved all other text. Classified findings before hardening: 0. Classified findings closed: 0. Classified findings remaining: 0. Unclassified scanner findings remaining: 0. Review findings closed: 7. Review findings remaining: 1.


Open before hardening

None

Open after hardening

None


Expand
File
Group
Subtype
Severity
Status

No findings available

0 findings


SKILL.md

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

Some snippets below leave out imports for brevity. Import the Page type from @playwright/test, and import the setup, reset and shape helpers (such as setupPage, hardResetEditor and getAllShapeTypes) from ../shared-e2e. Import only the helpers a test actually uses.

test.beforeEach(setupOrReset) // Smart: navigates first run, fast reset after

Shared page for performance

For tests that don't need full isolation:

This pattern shares browser storage and editor state across tests, so use it only when the tests tolerate that shared state, and close the shared page once all tests in the group finish. Keep setupOrReset as the default for test 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 } 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()
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
})

An early return reports the test as passed on mobile even though nothing ran. Prefer the conditional form of the test skip call so mobile runs are reported as skipped.

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

Prefer Playwright web-first assertions, which wait for the expected state, over fixed sleeps. Reserve frame waits for animation timing, and use a fixed sleep only when nothing observable can be awaited.

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

Fix the cause of flakiness first, for example by waiting on assertions or editor state. Treat skipping as a last resort: record the reason and a tracking issue next to the skip, and follow up to fix and re-enable the test. Keep failing tests running when a change breaks them, and fix the change instead.

test.describe.skip('clipboard tests', () => {
	// Skipped because flaky in CI
})

test.skip('known issue', async () => {})

Running E2E tests

pnpm e2e                    # Examples E2E
pnpm e2e-dotcom            # Dotcom E2E
pnpm e2e-ui                # With Playwright UI
pnpm 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

Install

Install with chainctl
chainctl skill install public/tldraw/write-e2e-tests:latest
Get started with chainctl
View source

Harden your own skills

Bring Chainguard's hardening pipeline to your first-party skills — custom rulesets, automated remediation, full audit trail.

Provenance

Upstream sourcehttps://github.com/tldraw/tldraw
Upstream commitbe38cf1f060050be8599108869fa31aae0639032
OCI reference

skills.cgr.dev/public/github.com/tldraw/skills/write-e2e-tests:latest

License

MIT

Files

HARDENING.md

LICENSE

SKILL.md


The trusted source for open source

Talk to an expert
PrivacyTerms

Product

Chainguard ContainersChainguard LibrariesChainguard VMsChainguard OS PackagesChainguard ActionsChainguard Agent SkillsIntegrationsPricing
© 2026 Chainguard, Inc. All Rights Reserved.
Chainguard® and the Chainguard logo are registered trademarks of Chainguard, Inc. in the United States and/or other countries.
The other respective trademarks mentioned on this page are owned by the respective companies and use of them does not imply any affiliation or endorsement.