# 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
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
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()
})
page - Playwright pagellm - Mock LLM server for queuing responses (text, tool, toolMatch, textMatch, etc.)project - Golden-path project fixture (call project.open() first, then use project.sdk, project.prompt(...), project.gotoSession(...), project.trackSession(...))sdk - OpenCode SDK client for API calls (worker-scoped, shared directory)gotoSession(sessionID?) - Navigate to session (worker-scoped, shared directory)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 sessionsessionIDFromUrl(url) - Read session ID from URLslugFromUrl(url) - Read workspace slug from URLwaitSlug(page, skip?) - Wait for resolved workspace slugclickListItem(container, filter) - Click list item by key/textSelectors (selectors.ts):
promptSelector - Prompt inputterminalSelector - Terminal panelsessionItemSelector(id) - Session in sidebarlistItemSelector - Generic list itemsUtils (utils.ts):
modKey - Meta (Mac) or Control (Linux/Win)serverUrl - Backend server URLsessionPath(dir, id?) - Build session URLAlways import from ../fixtures, not @playwright/test:
// ✅ Good
import { test, expect } from "../fixtures"
// ❌ Bad
import { test, expect } from "@playwright/test"
feature-name.spec.ts"sidebar can be toggled"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
})
project fixture for tests that need a dedicated project with LLM mocking — call project.open() then use project.prompt(...), project.trackSession(...), etc.withSession(sdk, title, callback) for lightweight temp sessions on the shared worker directoryproject.trackSession(sessionID, directory?) and project.trackDirectory(directory) for any resources created outside the fixture so teardown can clean them upsdk.session.delete(...) directlyDefault: 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)
})
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()
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
waitTerminalReady(page, { term? }) and runTerminal(page, { cmd, token, term?, timeout? }) from actions.ts.waitTerminalFocusIdle(...) before the next keyboard action when prompt focus or keyboard routing matters.waitForTimeout and custom DOM or data-* readiness checks.page.waitForTimeout(...) to make a test passexpect(...), expect.poll(...), or existing helperstoBeVisible(), toHaveCount(0), and toHaveAttribute(...) for normal UI state, and reserve expect.poll(...) for probe, mock, or backend state--repeat-each and multiple workers when practicalpackages/app/src/testing/terminal.ts../fixtures../actions and ../selectors../actions. Workspace URL slugs can be canonicalized on Windows, so assert against canonical or resolved workspace slugs.For UI debugging, use:
bun test:e2e:ui
This opens Playwright's interactive UI for step-through debugging.