Écrire des tests E2E
Les tests E2E utilisent Playwright. Situés dans apps/examples/e2e/ (exemples SDK) et apps/dotcom/client/e2e/ (tldraw.com).
Structure des fichiers de test
apps/examples/e2e/
├── fixtures/
│ ├── fixtures.ts # Fixtures de test (toolbar, menus, etc.)
│ └── menus/ # Page object models
├── tests/
│ └── test-*.spec.ts # Fichiers de test
└── shared-e2e.ts # Utilitaires partagés
Nommez les fichiers de test test-<feature>.spec.ts.
Déclarations requises
Lors de l'utilisation de page.evaluate() pour accéder à l'éditeur ou aux événements UI :
import { Editor } from 'tldraw'
declare const editor: Editor
declare const __tldraw_ui_event: { name: string; data?: any }
Structure de test basique
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 }) => {
// Implémentation du test
})
})
Patterns de setup
Setup standard (recommandé)
test.beforeEach(setupOrReset) // Smart: navigue au premier lancement, réinitialisation rapide après
Page partagée pour les performances
Pour les tests qui n'ont pas besoin d'une isolation complète :
let page: Page
test.describe('Feature', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test.beforeEach(async () => {
await hardResetEditor(page)
})
})
Setup avec formes
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)
})
Fixtures disponibles
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, // Méthodes tldrawApi
isMobile, // Vérification du viewport mobile
isMac, // Vérification de la plateforme Mac
}) => {})
Interagir avec l'éditeur
Via page.evaluate
// Exécuter du code dans le contexte du navigateur
await page.evaluate(() => {
editor.createShapes([{ type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }])
})
// Réinitialisation rapide (plus rapide que les raccourcis clavier)
await page.evaluate(() => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
editor.setCurrentTool('select')
})
// Récupérer des données de l'éditeur
const shape = await page.evaluate(() => editor.getOnlySelectedShape())
expect(shape).toMatchObject({ type: 'geo', x: 100, y: 100 })
Tester les événements UI
await page.keyboard.press('Control+a')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-all-shapes',
data: { source: 'kbd' },
})
Sélectionner les outils et éléments UI
Par test ID
await page.getByTestId('tools.rectangle').click()
await page.getByTestId('tools.more.cloud').click() // Dans popover
await expect(page.getByTestId('tools.select')).toHaveAttribute('aria-pressed', 'true')
Via la fixture toolbar
const { select, draw, arrow, rectangle } = toolbar.tools
await rectangle.click()
await toolbar.isSelected(rectangle)
await toolbar.isNotSelected(select)
// Popover plus d'outils
await toolbar.moreToolsButton.click()
await toolbar.popOverTools.popoverCloud.click()
Interactions avec les menus
import { clickMenu, withMenu } from '../shared-e2e'
// Cliquer sur un élément de menu
await clickMenu(page, 'main-menu.edit.copy')
await clickMenu(page, 'context-menu.copy-as.copy-as-png')
// Mettre le focus et interagir avec un élément de menu
await page.mouse.click(200, 200, { button: 'right' })
await withMenu(page, 'context-menu.arrange.distribute-horizontal', (item) => item.focus())
await page.keyboard.press('Enter')
Tests pilotés par les données
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)
// Réinitialiser pour l'itération suivante
await page.evaluate(() => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
})
}
})
Gestion spécifique aux plates-formes
Touches de modification
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)
})
Passer sur mobile
test('desktop only feature', async ({ isMobile }) => {
if (isMobile) return
// Test spécifique au bureau
})
Fonctions d'aide
import { getAllShapeTypes, getAllShapeLabels, sleep, sleepFrames } from '../shared-e2e'
// Obtenir les types de formes sur le canvas
const shapes = await getAllShapeTypes(page)
expect(shapes).toEqual(['geo', 'arrow'])
// Attendre les opérations asynchrones
await sleep(100)
await sleepFrames(2) // Attendre les frames d'animation
Assertions
// Assertions sur les formes
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
type: 'geo',
props: { w: 100, h: 100 },
})
// Assertions sur les attributs
await expect(page.getByTestId('tools.select')).toHaveAttribute('aria-pressed', 'true')
// Assertions CSS (pour l'état de sélection)
await expect(tool).toHaveCSS('color', 'rgb(255, 255, 255)')
// Visibilité
await expect(toolbar.moreToolsPopover).toBeVisible()
await expect(toolbar.toolLock).toBeHidden()
Ignorer les tests instables
test.describe.skip('clipboard tests', () => {
// Ignoré car instable en CI
})
test.skip('known issue', async () => {})
Exécuter les tests E2E
yarn e2e # E2E des exemples
yarn e2e-dotcom # E2E Dotcom
yarn e2e-ui # Avec Playwright UI
yarn e2e -- --grep "toolbar" # Filtrer par motif
Résumé des patterns clés
- Utilisez
setupOrResetdansbeforeEachpour l'isolation des tests - Déclarez
editoret__tldraw_ui_eventpourpage.evaluate() - Utilisez
page.evaluate()pour la manipulation rapide de l'éditeur (plus rapide que le clavier) - Utilisez
getByTestId()avec le motiftools.<name>pour la sélection d'outils - Utilisez
clickMenu()/withMenu()pour les interactions de menu - Gérez les différences de plate-forme avec les fixtures
isMacetisMobile - Testez contre l'exemple
localhost:5420/end-to-end