# 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 pagesdk - OpenCode SDK client for API callsgotoSession(sessionID?) - Navigate to sessionActions (actions.ts):
openPalette(page) - Open command paletteopenSettings(page) - Open settings dialogcloseDialog(page, dialog) - Close any dialogopenSidebar(page) / closeSidebar(page) - Toggle sidebarwithSession(sdk, title, callback) - Create temp sessionclickListItem(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:
test("test with cleanup", async ({ page, sdk, gotoSession }) => {
await withSession(sdk, "test session", async (session) => {
await gotoSession(session.id)
// Test code...
}) // Auto-deletes session
})
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)
})
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
../fixtures../actions and ../selectorsFor UI debugging, use:
bun test:e2e:ui
This opens Playwright's interactive UI for step-through debugging.