summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-03-12 17:35:26 +1000
committerGitHub <[email protected]>2026-03-12 17:35:26 +1000
commit328c6de80d51704c09bdd962df2ddf5b9d7c82ea (patch)
treebf6426b3afeb824feaae73aa22cfc20e6e0d438d /packages/app/e2e
parentc9c0318e0e5c2fcd80fc1c32a1ccfe360f182f90 (diff)
downloadopencode-328c6de80d51704c09bdd962df2ddf5b9d7c82ea.tar.gz
opencode-328c6de80d51704c09bdd962df2ddf5b9d7c82ea.zip
Fix terminal e2e flakiness with a real terminal driver (#17144)
Diffstat (limited to 'packages/app/e2e')
-rw-r--r--packages/app/e2e/AGENTS.md9
-rw-r--r--packages/app/e2e/actions.ts53
-rw-r--r--packages/app/e2e/fixtures.ts9
-rw-r--r--packages/app/e2e/prompt/prompt-slash-terminal.spec.ts3
-rw-r--r--packages/app/e2e/settings/settings-keybinds.spec.ts4
-rw-r--r--packages/app/e2e/terminal/terminal-init.spec.ts6
-rw-r--r--packages/app/e2e/terminal/terminal-tabs.spec.ts21
-rw-r--r--packages/app/e2e/terminal/terminal.spec.ts3
8 files changed, 87 insertions, 21 deletions
diff --git a/packages/app/e2e/AGENTS.md b/packages/app/e2e/AGENTS.md
index 8bfbd111b..cb8080fb2 100644
--- a/packages/app/e2e/AGENTS.md
+++ b/packages/app/e2e/AGENTS.md
@@ -70,6 +70,8 @@ test("test description", async ({ page, sdk, gotoSession }) => {
- `openSettings(page)` - Open settings dialog
- `closeDialog(page, dialog)` - Close any dialog
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
+- `waitTerminalReady(page, { term? })` - Wait for a mounted terminal to connect and finish rendering output
+- `runTerminal(page, { cmd, token, term?, timeout? })` - Type into the terminal via the browser and wait for rendered output
- `withSession(sdk, title, callback)` - Create temp session
- `withProject(...)` - Create temp project/workspace
- `sessionIDFromUrl(url)` - Read session ID from URL
@@ -167,6 +169,13 @@ 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? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`.
+- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
+- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
+
## Writing New Tests
1. Choose appropriate folder or create new one
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index 86147dc65..f721e0a7c 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -3,6 +3,7 @@ import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { execSync } from "node:child_process"
+import { terminalAttr, type E2EWindow } from "../src/testing/terminal"
import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
import {
dropdownMenuTriggerSelector,
@@ -15,6 +16,7 @@ import {
listItemSelector,
listItemKeySelector,
listItemKeyStartsWithSelector,
+ terminalSelector,
workspaceItemSelector,
workspaceMenuTriggerSelector,
} from "./selectors"
@@ -28,6 +30,57 @@ export async function defocus(page: Page) {
.catch(() => undefined)
}
+async function terminalID(term: Locator) {
+ const id = await term.getAttribute(terminalAttr)
+ if (id) return id
+ throw new Error(`Active terminal missing ${terminalAttr}`)
+}
+
+async function terminalReady(page: Page, term?: Locator) {
+ const next = term ?? page.locator(terminalSelector).first()
+ const id = await terminalID(next)
+ return page.evaluate((id) => {
+ const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]
+ return !!state?.connected && (state.settled ?? 0) > 0
+ }, id)
+}
+
+async function terminalHas(page: Page, input: { term?: Locator; token: string }) {
+ const next = input.term ?? page.locator(terminalSelector).first()
+ const id = await terminalID(next)
+ return page.evaluate(
+ (input) => {
+ const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[input.id]
+ return state?.rendered.includes(input.token) ?? false
+ },
+ { id, token: input.token },
+ )
+}
+
+export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) {
+ const term = input?.term ?? page.locator(terminalSelector).first()
+ const timeout = input?.timeout ?? 10_000
+ await expect(term).toBeVisible()
+ await expect(term.locator("textarea")).toHaveCount(1)
+ await expect
+ .poll(() => terminalReady(page, term), { timeout })
+ .toBe(true)
+}
+
+export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) {
+ const term = input.term ?? page.locator(terminalSelector).first()
+ const timeout = input.timeout ?? 10_000
+ await waitTerminalReady(page, { term, timeout })
+ const textarea = term.locator("textarea")
+ await term.click()
+ await expect(textarea).toBeFocused()
+ await page.keyboard.type(input.cmd)
+ await page.keyboard.press("Enter")
+ await expect
+ .poll(() => terminalHas(page, { term, token: input.token }), { timeout })
+ .toBe(true)
+}
+
export async function openPalette(page: Page) {
await defocus(page)
await page.keyboard.press(`${modKey}+P`)
diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts
index 6a35c6901..cf59eeb47 100644
--- a/packages/app/e2e/fixtures.ts
+++ b/packages/app/e2e/fixtures.ts
@@ -1,4 +1,5 @@
import { test as base, expect, type Page } from "@playwright/test"
+import type { E2EWindow } from "../src/testing/terminal"
import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions"
import { promptSelector } from "./selectors"
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
@@ -91,6 +92,14 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
await seedProjects(page, input)
await page.addInitScript(() => {
+ const win = window as E2EWindow
+ win.__opencode_e2e = {
+ ...win.__opencode_e2e,
+ terminal: {
+ enabled: true,
+ terminals: {},
+ },
+ }
localStorage.setItem(
"opencode.global.dat:model",
JSON.stringify({
diff --git a/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts
index fa884b752..100d1878a 100644
--- a/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts
+++ b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts
@@ -1,4 +1,5 @@
import { test, expect } from "../fixtures"
+import { waitTerminalReady } from "../actions"
import { promptSelector, terminalSelector } from "../selectors"
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
@@ -13,7 +14,7 @@ test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
await prompt.fill("/terminal")
await expect(slash).toBeVisible()
await page.keyboard.press("Enter")
- await expect(terminal).toBeVisible()
+ await waitTerminalReady(page, { term: terminal })
// Terminal panel retries focus (immediate, RAF, 120ms, 240ms) after opening,
// which can steal focus from the prompt and prevent fill() from triggering
diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts
index e0d590b31..9fc2a50ad 100644
--- a/packages/app/e2e/settings/settings-keybinds.spec.ts
+++ b/packages/app/e2e/settings/settings-keybinds.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
-import { openSettings, closeDialog, withSession } from "../actions"
+import { openSettings, closeDialog, waitTerminalReady, withSession } from "../actions"
import { keybindButtonSelector, terminalSelector } from "../selectors"
import { modKey } from "../utils"
@@ -302,7 +302,7 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) =>
await expect(terminal).not.toBeVisible()
await page.keyboard.press(`${modKey}+Y`)
- await expect(terminal).toBeVisible()
+ await waitTerminalReady(page, { term: terminal })
await page.keyboard.press(`${modKey}+Y`)
await expect(terminal).not.toBeVisible()
diff --git a/packages/app/e2e/terminal/terminal-init.spec.ts b/packages/app/e2e/terminal/terminal-init.spec.ts
index 18991bf76..d9bbfa2be 100644
--- a/packages/app/e2e/terminal/terminal-init.spec.ts
+++ b/packages/app/e2e/terminal/terminal-init.spec.ts
@@ -1,4 +1,5 @@
import { test, expect } from "../fixtures"
+import { waitTerminalReady } from "../actions"
import { promptSelector, terminalSelector } from "../selectors"
import { terminalToggleKey } from "../utils"
@@ -13,8 +14,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
await page.keyboard.press(terminalToggleKey)
}
- await expect(terminals.first()).toBeVisible()
- await expect(terminals.first().locator("textarea")).toHaveCount(1)
+ await waitTerminalReady(page, { term: terminals.first() })
await expect(terminals).toHaveCount(1)
// Ghostty captures a lot of keybinds when focused; move focus back
@@ -24,5 +24,5 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
await expect(tabs).toHaveCount(2)
await expect(terminals).toHaveCount(1)
- await expect(terminals.first().locator("textarea")).toHaveCount(1)
+ await waitTerminalReady(page, { term: terminals.first() })
})
diff --git a/packages/app/e2e/terminal/terminal-tabs.spec.ts b/packages/app/e2e/terminal/terminal-tabs.spec.ts
index afa6254cd..ca1f7eee8 100644
--- a/packages/app/e2e/terminal/terminal-tabs.spec.ts
+++ b/packages/app/e2e/terminal/terminal-tabs.spec.ts
@@ -1,4 +1,5 @@
import type { Page } from "@playwright/test"
+import { runTerminal, waitTerminalReady } from "../actions"
import { test, expect } from "../fixtures"
import { terminalSelector } from "../selectors"
import { terminalToggleKey, workspacePersistKey } from "../utils"
@@ -17,16 +18,7 @@ async function open(page: Page) {
const terminal = page.locator(terminalSelector)
const visible = await terminal.isVisible().catch(() => false)
if (!visible) await page.keyboard.press(terminalToggleKey)
- await expect(terminal).toBeVisible()
- await expect(terminal.locator("textarea")).toHaveCount(1)
-}
-
-async function run(page: Page, cmd: string) {
- const terminal = page.locator(terminalSelector)
- await expect(terminal).toBeVisible()
- await terminal.click()
- await page.keyboard.type(cmd)
- await page.keyboard.press("Enter")
+ await waitTerminalReady(page, { term: terminal })
}
async function store(page: Page, key: string) {
@@ -56,15 +48,16 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
await gotoSession()
await open(page)
- await run(page, `echo ${one}`)
+ await runTerminal(page, { cmd: `echo ${one}`, token: one })
await page.getByRole("button", { name: /new terminal/i }).click()
await expect(tabs).toHaveCount(2)
- await run(page, `echo ${two}`)
+ await runTerminal(page, { cmd: `echo ${two}`, token: two })
await first.click()
await expect(first).toHaveAttribute("aria-selected", "true")
+
await expect
.poll(
async () => {
@@ -76,7 +69,7 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
second: second.includes(two),
}
},
- { timeout: 30_000 },
+ { timeout: 5_000 },
)
.toEqual({ first: false, second: true })
@@ -93,7 +86,7 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
second: second.includes(two),
}
},
- { timeout: 30_000 },
+ { timeout: 5_000 },
)
.toEqual({ first: true, second: false })
})
diff --git a/packages/app/e2e/terminal/terminal.spec.ts b/packages/app/e2e/terminal/terminal.spec.ts
index ef88aa34e..768f7c182 100644
--- a/packages/app/e2e/terminal/terminal.spec.ts
+++ b/packages/app/e2e/terminal/terminal.spec.ts
@@ -1,4 +1,5 @@
import { test, expect } from "../fixtures"
+import { waitTerminalReady } from "../actions"
import { terminalSelector } from "../selectors"
import { terminalToggleKey } from "../utils"
@@ -13,5 +14,5 @@ test("terminal panel can be toggled", async ({ page, gotoSession }) => {
}
await page.keyboard.press(terminalToggleKey)
- await expect(terminal).toBeVisible()
+ await waitTerminalReady(page, { term: terminal })
})