write-e2e-tests

Par tldraw · tldraw

Rédaction de tests E2E Playwright pour tldraw. À utiliser lors de la création de tests navigateur, du test d'interactions UI, ou de l'ajout de couverture E2E dans `apps/examples/e2e` ou `apps/dotcom/client/e2e`.

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

É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 setupOrReset dans beforeEach pour l'isolation des tests
  • Déclarez editor et __tldraw_ui_event pour page.evaluate()
  • Utilisez page.evaluate() pour la manipulation rapide de l'éditeur (plus rapide que le clavier)
  • Utilisez getByTestId() avec le motif tools.<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 isMac et isMobile
  • Testez contre l'exemple localhost:5420/end-to-end

Skills similaires