8.9 KiB
App E2E Smoke Suite (CI)
Implement a small set of high-signal, low-flake Playwright tests to run in CI.
These tests are intended to catch regressions in the “core shell” of the app (navigation, dialogs, prompt UX, file viewer, terminal), without relying on model output.
Summary
Add 6 smoke tests to packages/app/e2e/:
- Settings dialog: open, switch tabs, close
- Prompt slash command:
/openopens the file picker dialog - Prompt @mention:
@<file>inserts a file pill token - Model picker: open model selection and choose a model
- File viewer: open a known file and assert contents render
- Terminal: open terminal, verify Ghostty mounts, create a second terminal
Progress
- 1. Settings dialog open / switch / close (
packages/app/e2e/settings.spec.ts) - 2. Prompt slash command path:
/openopens file picker (packages/app/e2e/prompt-slash-open.spec.ts) - 3. Prompt @mention inserts a file pill token (
packages/app/e2e/prompt-mention.spec.ts) - 4. Model selection UI works end-to-end (
packages/app/e2e/model-picker.spec.ts) - 5. File viewer renders real file content (
packages/app/e2e/file-viewer.spec.ts) - 8. Terminal init + create new terminal (
packages/app/e2e/terminal-init.spec.ts)
Goals
- Tests run reliably in CI using the existing local runner (
packages/app/script/e2e-local.ts). - Cover “wiring” regressions across UI + backend APIs:
- dialogs + command routing
- prompt contenteditable parsing
- file search + file read + code viewer render
- terminal open + pty creation + Ghostty mount
- Avoid assertions that depend on LLM output.
- Keep runtime low (these should be “smoke”, not full workflows).
Non-goals
- Verifying complex model behavior, streaming correctness, or tool call semantics.
- Testing provider auth flows (CI has no secrets).
- Testing share, MCP, or LSP download flows (disabled in the e2e runner).
Current State
Existing tests in packages/app/e2e/ already cover:
- Home renders + server picker opens
- Directory route redirects to
/session - Sidebar collapse/expand
- Command palette opens/closes
- Basic session open + prompt input + (optional) prompt/reply flow
- File open via palette (but shallow assertion: tab exists)
- Terminal panel toggles (but doesn’t assert Ghostty mounted)
- Context panel open
We want to add a focused smoke layer that increases coverage of the most regression-prone UI paths.
Proposed Tests
All tests should use the shared fixtures in:
packages/app/e2e/fixtures.ts(forsdk,directory,gotoSession)packages/app/e2e/utils.ts(formodKey,promptSelector,terminalToggleKey)
Prefer creating new spec files rather than overloading existing ones, so it’s easy to run these tests as a group via grep.
Suggested file layout:
packages/app/e2e/settings.spec.tspackages/app/e2e/prompt-slash-open.spec.tspackages/app/e2e/prompt-mention.spec.tspackages/app/e2e/model-picker.spec.tspackages/app/e2e/file-viewer.spec.tspackages/app/e2e/terminal-init.spec.ts
Name each test with a “smoke” prefix so CI can run only this suite if needed.
1) Settings dialog open / switch / close
Purpose: catch regressions in dialog infra, settings rendering, tabs.
Steps:
await gotoSession().- Open settings via keybind (preferred for stability):
await page.keyboard.press(${modKey}+Comma). - Assert dialog visible (
page.getByRole('dialog')). - Click the "Shortcuts" tab (role
tab, name "Shortcuts"). - Assert shortcuts view renders (e.g. the search field placeholder or reset button exists).
- Close with
Escapeand assert dialog removed.
Notes:
- If
Meta+Comma/Control+Commakey name is flaky, fall back to clicking the sidebar settings icon. - Favor role-based selectors over brittle class selectors.
- If
Escapedoesn’t dismiss reliably (tooltips can intercept), fall back to clicking the dialog overlay.
Implementation: packages/app/e2e/settings.spec.ts
Acceptance criteria:
- Settings dialog opens reliably.
- Switching to Shortcuts tab works.
- Escape closes the dialog.
2) Prompt slash command path: /open opens file picker
Purpose: validate contenteditable parsing + slash popover + builtin command dispatch (distinct from mod+p).
Steps:
await gotoSession().- Click prompt (
promptSelector). - Type
/open. - Press
Enter(while slash popover is active). - Assert a dialog appears and contains a textbox (the file picker search input).
- Close dialog with
Escape.
Acceptance criteria:
/opentriggersfile.openand opensDialogSelectFile.
3) Prompt @mention inserts a file pill token
Purpose: validate the most fragile prompt behavior: structured tokens inside contenteditable.
Steps:
await gotoSession().- Focus the prompt.
- Type
@packages/app/package.json. - Press
Tabto accept the active @mention suggestion. - Assert a pill element is inserted:
page.locator('[data-component="prompt-input"] [data-type="file"][data-path="packages/app/package.json"]')exists.
Acceptance criteria:
- A file pill is inserted and has the expected
data-*attributes. - Prompt editor remains interactable (e.g. typing a trailing space works).
4) Model selection UI works end-to-end
Purpose: validate model list rendering, selection wiring, and prompt footer updating.
Implementation approach:
- Use
/modelto open the model selection dialog (builtin command).
Steps:
await gotoSession().- Focus prompt, type
/model, pressEnter. - In the model dialog, pick a visible model that is not the current selection (if available).
- Use the search field to filter to that model (use its id from the list item's
data-keyto avoid time-based model visibility drift). - Select the filtered model.
- Assert dialog closed.
- Assert the prompt footer now shows the chosen model name.
Acceptance criteria:
- A model can be selected without requiring provider auth.
- The prompt footer reflects the new selection.
5) File viewer renders real file content
Purpose: ensure file search + open + file.read + code viewer render all work.
Steps:
await gotoSession().- Open file picker (either
mod+por/open). - Search for
packages/app/package.json. - Click the matching file result.
- Ensure the new file tab is active (click the
package.jsontab if needed so the viewer mounts). - Assert the code viewer contains a known substring:
"name": "@opencode-ai/app".
- Optionally assert the file tab is active and visible.
Acceptance criteria:
- Code view shows expected content (not just “tab exists”).
8) Terminal init + create new terminal
Purpose: ensure terminal isn’t only “visible”, but actually mounted and functional.
Steps:
await gotoSession().- Open terminal with
terminalToggleKey(currentlyControl+Backquote). - Assert terminal container exists and is visible:
[data-component="terminal"]. - Assert Ghostty textarea exists:
[data-component="terminal"] textarea. - Create a new terminal via keybind (
terminal.newisctrl+alt+t). - Assert terminal tab count increases to 2.
Acceptance criteria:
- Ghostty mounts (textarea present).
- Creating a new terminal results in a second tab.
CI Stability + Flake Avoidance
These tests run with fullyParallel: true in packages/app/playwright.config.ts. Keep them isolated and deterministic.
- Avoid ordering-based assertions: never assume a “first” session/project/file is stable unless you filtered by unique text.
- Prefer deterministic targets:
- use
packages/app/package.jsonrather than barepackage.json(multiple hits possible) - for models, avoid hardcoding a single model id; pick from the visible list and filter by its
data-keyinstead
- use
- Prefer robust selectors:
- role selectors:
getByRole('dialog'),getByRole('textbox'),getByRole('tab') - stable data attributes already present:
promptSelector,[data-component="terminal"]
- role selectors:
- Keep tests local and fast:
- do not submit prompts that require real model replies
- avoid
page.waitForTimeout; useexpect(...).toBeVisible()andexpect.pollwhen needed
- Watch for silent UI failures:
- capture
page.on('pageerror')and fail test if any are emitted - optionally capture console errors (
page.on('console', ...)) and fail ontype==='error'
- capture
- Cleanup:
- these tests should not need to create sessions
- if a test ever creates sessions or PTYs directly, clean up with SDK calls in
finally
Validation Plan
Run locally:
cd packages/appbun run test:e2e:local -- --grep smoke
Verify:
- all new tests pass consistently across multiple runs
- overall e2e suite time does not increase significantly
Open Questions
- Should we add a small helper in
packages/app/e2e/utils.tsfor “type into prompt contenteditable” to reduce duplication? - Do we want to gate these smoke tests with a dedicated
@smokenaming convention (ortest.describe('smoke', ...)) so CI can target them explicitly?