mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 13:54:01 +00:00
7.8 KiB
7.8 KiB
E2E Testing Guide
Build/Lint/Test Commands
# Run all e2e tests
bun test:e2e
# Run specific test file
bun test:e2e -- app/home.spec.ts
# Run single test by title
bun test:e2e -- -g "home renders and shows core entrypoints"
# Run tests with UI mode (for debugging)
bun test:e2e:ui
# Run tests locally with full server setup
bun test:e2e:local
# View test report
bun test:e2e:report
# Typecheck
bun typecheck
Test Structure
All tests live in packages/app/e2e/:
e2e/
├── fixtures.ts # Test fixtures (test, expect, gotoSession, sdk)
├── actions.ts # Reusable action helpers
├── selectors.ts # DOM selectors
├── utils.ts # Utilities (serverUrl, modKey, path helpers)
└── [feature]/
└── *.spec.ts # Test files
Test Patterns
Basic Test Structure
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { withSession } from "../actions"
test("test description", async ({ page, sdk, gotoSession }) => {
await gotoSession() // or gotoSession(sessionID)
// Your test code
await expect(page.locator(promptSelector)).toBeVisible()
})
Using Fixtures
page- Playwright pagesdk- OpenCode SDK client for API callsgotoSession(sessionID?)- Navigate to session
Helper Functions
Actions (actions.ts):
openPalette(page)- Open command paletteopenSettings(page)- Open settings dialogcloseDialog(page, dialog)- Close any dialogopenSidebar(page)/closeSidebar(page)- Toggle sidebarwaitTerminalReady(page, { term? })- Wait for a mounted terminal to connect and finish rendering outputrunTerminal(page, { cmd, token, term?, timeout? })- Type into the terminal via the browser and wait for rendered outputwithSession(sdk, title, callback)- Create temp sessionwithProject(...)- Create temp project/workspacesessionIDFromUrl(url)- Read session ID from URLslugFromUrl(url)- Read workspace slug from URLwaitSlug(page, skip?)- Wait for resolved workspace slugtrackSession(sessionID, directory?)- Register session for fixture cleanuptrackDirectory(directory)- Register directory for fixture cleanupclickListItem(container, filter)- Click list item by key/text
Selectors (selectors.ts):
promptSelector- Prompt inputterminalSelector- Terminal panelsessionItemSelector(id)- Session in sidebarlistItemSelector- Generic list items
Utils (utils.ts):
modKey- Meta (Mac) or Control (Linux/Win)serverUrl- Backend server URLsessionPath(dir, id?)- Build session URL
Code Style Guidelines
Imports
Always import from ../fixtures, not @playwright/test:
// ✅ Good
import { test, expect } from "../fixtures"
// ❌ Bad
import { test, expect } from "@playwright/test"
Naming Conventions
- Test files:
feature-name.spec.ts - Test names: lowercase, descriptive:
"sidebar can be toggled" - Variables: camelCase
- Constants: SCREAMING_SNAKE_CASE
Error Handling
Tests should clean up after themselves. Prefer fixture-managed cleanup:
test("test with cleanup", async ({ page, sdk, gotoSession }) => {
await withSession(sdk, "test session", async (session) => {
await gotoSession(session.id)
// Test code...
}) // Auto-deletes session
})
- Prefer
withSession(...)for temp sessions - In
withProject(...)tests that create sessions or extra workspaces, calltrackSession(sessionID, directory?)andtrackDirectory(directory) - This lets fixture teardown abort, wait for idle, and clean up safely under CI concurrency
- Avoid calling
sdk.session.delete(...)directly
Timeouts
Default: 60s per test, 10s per assertion. Override when needed:
test.setTimeout(120_000) // For long LLM operations
test("slow test", async () => {
await expect.poll(() => check(), { timeout: 90_000 }).toBe(true)
})
Selectors
Use data-component, data-action, or semantic roles:
// ✅ Good
await page.locator('[data-component="prompt-input"]').click()
await page.getByRole("button", { name: "Open settings" }).click()
// ❌ Bad
await page.locator(".css-class-name").click()
await page.locator("#id-name").click()
Keyboard Shortcuts
Use modKey for cross-platform compatibility:
import { modKey } from "../utils"
await page.keyboard.press(`${modKey}+B`) // Toggle sidebar
await page.keyboard.press(`${modKey}+Comma`) // Open settings
Terminal Tests
- In terminal tests, type through the browser. Do not write to the PTY through the SDK.
- Use
waitTerminalReady(page, { term? })andrunTerminal(page, { cmd, token, term?, timeout? })fromactions.ts. - These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
- After opening the terminal, use
waitTerminalFocusIdle(...)before the next keyboard action when prompt focus or keyboard routing matters. - This avoids racing terminal mount, focus handoff, and prompt readiness when the next step types or sends shortcuts.
- Avoid
waitForTimeoutand custom DOM ordata-*readiness checks.
Wait on state
- Never use wall-clock waits like
page.waitForTimeout(...)to make a test pass - Avoid race-prone flows that assume work is finished after an action
- Wait or poll on observable state with
expect(...),expect.poll(...), or existing helpers - Prefer locator assertions like
toBeVisible(),toHaveCount(0), andtoHaveAttribute(...)for normal UI state, and reserveexpect.poll(...)for probe, mock, or backend state - Prefer semantic app state over transient DOM visibility when behavior depends on active selection, focus ownership, or async retry loops
- Do not treat a visible element as proof that the app will route the next action to it
- When fixing a flake, validate with
--repeat-eachand multiple workers when practical
Add hooks
- If required state is not observable from the UI, add a small test-only driver or probe in app code instead of sleeps or fragile DOM checks
- Keep these hooks minimal and purpose-built, following the style of
packages/app/src/testing/terminal.ts - Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony
- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI
- Add minimal test-only probes for semantic state like the active list item or selected command when DOM intermediates are unstable
- Prefer probing committed app state over asserting on transient highlight, visibility, or animation states
Prefer helpers
- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise
- Use direct locators when the interaction is simple and a helper would not add clarity
- Prefer helpers that both perform an action and verify the app consumed it
- Avoid composing helpers redundantly when one already includes the other or already waits for the resulting state
- If a helper already covers the required wait or verification, use it directly instead of layering extra clicks, keypresses, or assertions
Writing New Tests
- Choose appropriate folder or create new one
- Import from
../fixtures - Use helper functions from
../actionsand../selectors - When validating routing, use shared helpers from
../actions. Workspace URL slugs can be canonicalized on Windows, so assert against canonical or resolved workspace slugs. - Clean up any created resources
- Use specific selectors (avoid CSS classes)
- Test one feature per test file
Local Development
For UI debugging, use:
bun test:e2e:ui
This opens Playwright's interactive UI for step-through debugging.