diff options
| author | Luke Parker <[email protected]> | 2026-03-12 17:35:26 +1000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-12 17:35:26 +1000 |
| commit | 328c6de80d51704c09bdd962df2ddf5b9d7c82ea (patch) | |
| tree | bf6426b3afeb824feaae73aa22cfc20e6e0d438d /packages/app/e2e | |
| parent | c9c0318e0e5c2fcd80fc1c32a1ccfe360f182f90 (diff) | |
| download | opencode-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.md | 9 | ||||
| -rw-r--r-- | packages/app/e2e/actions.ts | 53 | ||||
| -rw-r--r-- | packages/app/e2e/fixtures.ts | 9 | ||||
| -rw-r--r-- | packages/app/e2e/prompt/prompt-slash-terminal.spec.ts | 3 | ||||
| -rw-r--r-- | packages/app/e2e/settings/settings-keybinds.spec.ts | 4 | ||||
| -rw-r--r-- | packages/app/e2e/terminal/terminal-init.spec.ts | 6 | ||||
| -rw-r--r-- | packages/app/e2e/terminal/terminal-tabs.spec.ts | 21 | ||||
| -rw-r--r-- | packages/app/e2e/terminal/terminal.spec.ts | 3 |
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 }) }) |
