summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e/actions.ts
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-03-20 14:12:06 +1000
committerGitHub <[email protected]>2026-03-20 00:12:06 -0400
commitd460614cd7ad9e047a2792139ea67e16caa82ea7 (patch)
treeff415e8719c7b6edd73bc824da379308e4e62589 /packages/app/e2e/actions.ts
parent7866dbcfcc36a60d22ad466eddf54c54b21fabe3 (diff)
downloadopencode-d460614cd7ad9e047a2792139ea67e16caa82ea7.tar.gz
opencode-d460614cd7ad9e047a2792139ea67e16caa82ea7.zip
fix: lots of desktop stability, better e2e error logging (#18300)
Diffstat (limited to 'packages/app/e2e/actions.ts')
-rw-r--r--packages/app/e2e/actions.ts152
1 files changed, 145 insertions, 7 deletions
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index 88d71f94c..aced0756c 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -9,6 +9,7 @@ import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
import {
dropdownMenuTriggerSelector,
dropdownMenuContentSelector,
+ projectSwitchSelector,
projectMenuTriggerSelector,
projectCloseMenuSelector,
projectWorkspacesToggleSelector,
@@ -23,6 +24,16 @@ import {
workspaceMenuTriggerSelector,
} from "./selectors"
+const phase = new WeakMap<Page, "test" | "cleanup">()
+
+export function setHealthPhase(page: Page, value: "test" | "cleanup") {
+ phase.set(page, value)
+}
+
+export function healthPhase(page: Page) {
+ return phase.get(page) ?? "test"
+}
+
export async function defocus(page: Page) {
await page
.evaluate(() => {
@@ -196,11 +207,51 @@ export async function closeDialog(page: Page, dialog: Locator) {
}
export async function isSidebarClosed(page: Page) {
- const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
- await expect(button).toBeVisible()
+ const button = await waitSidebarButton(page, "isSidebarClosed")
return (await button.getAttribute("aria-expanded")) !== "true"
}
+async function errorBoundaryText(page: Page) {
+ const title = page.getByRole("heading", { name: /something went wrong/i }).first()
+ if (!(await title.isVisible().catch(() => false))) return
+
+ const description = await page
+ .getByText(/an error occurred while loading the application\./i)
+ .first()
+ .textContent()
+ .catch(() => "")
+ const detail = await page
+ .getByRole("textbox", { name: /error details/i })
+ .first()
+ .inputValue()
+ .catch(async () =>
+ (
+ (await page
+ .getByRole("textbox", { name: /error details/i })
+ .first()
+ .textContent()
+ .catch(() => "")) ?? ""
+ ).trim(),
+ )
+
+ return [title ? "Error boundary" : "", description ?? "", detail ?? ""].filter(Boolean).join("\n")
+}
+
+export async function assertHealthy(page: Page, context: string) {
+ const text = await errorBoundaryText(page)
+ if (!text) return
+ console.log(`[e2e:error-boundary][${context}]\n${text}`)
+ throw new Error(`Error boundary during ${context}\n${text}`)
+}
+
+async function waitSidebarButton(page: Page, context: string) {
+ const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
+ const boundary = page.getByRole("heading", { name: /something went wrong/i }).first()
+ await button.or(boundary).first().waitFor({ state: "visible", timeout: 10_000 })
+ await assertHealthy(page, context)
+ return button
+}
+
export async function toggleSidebar(page: Page) {
await defocus(page)
await page.keyboard.press(`${modKey}+B`)
@@ -209,7 +260,7 @@ export async function toggleSidebar(page: Page) {
export async function openSidebar(page: Page) {
if (!(await isSidebarClosed(page))) return
- const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
+ const button = await waitSidebarButton(page, "openSidebar")
await button.click()
const opened = await expect(button)
@@ -226,7 +277,7 @@ export async function openSidebar(page: Page) {
export async function closeSidebar(page: Page) {
if (await isSidebarClosed(page)) return
- const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
+ const button = await waitSidebarButton(page, "closeSidebar")
await button.click()
const closed = await expect(button)
@@ -241,6 +292,7 @@ export async function closeSidebar(page: Page) {
}
export async function openSettings(page: Page) {
+ await assertHealthy(page, "openSettings")
await defocus(page)
const dialog = page.getByRole("dialog")
@@ -253,6 +305,8 @@ export async function openSettings(page: Page) {
if (opened) return dialog
+ await assertHealthy(page, "openSettings")
+
await page.getByRole("button", { name: "Settings" }).first().click()
await expect(dialog).toBeVisible()
return dialog
@@ -314,10 +368,12 @@ export async function seedProjects(page: Page, input: { directory: string; extra
export async function createTestProject() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
+ const id = `e2e-${path.basename(root)}`
- await fs.writeFile(path.join(root, "README.md"), "# e2e\n")
+ await fs.writeFile(path.join(root, "README.md"), `# e2e\n\n${id}\n`)
execSync("git init", { cwd: root, stdio: "ignore" })
+ await fs.writeFile(path.join(root, ".git", "opencode"), id)
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
execSync("git add -A", { cwd: root, stdio: "ignore" })
execSync('git -c user.name="e2e" -c user.email="[email protected]" commit -m "init" --allow-empty', {
@@ -339,12 +395,24 @@ export function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
}
+async function probeSession(page: Page) {
+ return page
+ .evaluate(() => {
+ const win = window as E2EWindow
+ const current = win.__opencode_e2e?.model?.current
+ if (!current) return null
+ return { dir: current.dir, sessionID: current.sessionID }
+ })
+ .catch(() => null as { dir?: string; sessionID?: string } | null)
+}
+
export async function waitSlug(page: Page, skip: string[] = []) {
let prev = ""
let next = ""
await expect
.poll(
- () => {
+ async () => {
+ await assertHealthy(page, "waitSlug")
const slug = slugFromUrl(page.url())
if (!slug) return ""
if (skip.includes(slug)) return ""
@@ -374,6 +442,7 @@ export async function waitDir(page: Page, directory: string) {
await expect
.poll(
async () => {
+ await assertHealthy(page, "waitDir")
const slug = slugFromUrl(page.url())
if (!slug) return ""
return resolveSlug(slug)
@@ -386,6 +455,69 @@ export async function waitDir(page: Page, directory: string) {
return { directory: target, slug: base64Encode(target) }
}
+export async function waitSession(page: Page, input: { directory: string; sessionID?: string }) {
+ const target = await resolveDirectory(input.directory)
+ await expect
+ .poll(
+ async () => {
+ await assertHealthy(page, "waitSession")
+ const slug = slugFromUrl(page.url())
+ if (!slug) return false
+ const resolved = await resolveSlug(slug).catch(() => undefined)
+ if (!resolved || resolved.directory !== target) return false
+ if (input.sessionID && sessionIDFromUrl(page.url()) !== input.sessionID) return false
+
+ const state = await probeSession(page)
+ if (input.sessionID && (!state || state.sessionID !== input.sessionID)) return false
+ if (state?.dir) {
+ const dir = await resolveDirectory(state.dir).catch(() => state.dir ?? "")
+ if (dir !== target) return false
+ }
+
+ return page
+ .locator(promptSelector)
+ .first()
+ .isVisible()
+ .catch(() => false)
+ },
+ { timeout: 45_000 },
+ )
+ .toBe(true)
+ return { directory: target, slug: base64Encode(target) }
+}
+
+export async function waitSessionSaved(directory: string, sessionID: string, timeout = 30_000) {
+ const sdk = createSdk(directory)
+ const target = await resolveDirectory(directory)
+
+ await expect
+ .poll(
+ async () => {
+ const data = await sdk.session
+ .get({ sessionID })
+ .then((x) => x.data)
+ .catch(() => undefined)
+ if (!data?.directory) return ""
+ return resolveDirectory(data.directory).catch(() => data.directory)
+ },
+ { timeout },
+ )
+ .toBe(target)
+
+ await expect
+ .poll(
+ async () => {
+ const items = await sdk.session
+ .messages({ sessionID, limit: 20 })
+ .then((x) => x.data ?? [])
+ .catch(() => [])
+ return items.some((item) => item.info.role === "user")
+ },
+ { timeout },
+ )
+ .toBe(true)
+}
+
export function sessionIDFromUrl(url: string) {
const match = /\/session\/([^/?#]+)/.exec(url)
return match?.[1]
@@ -797,8 +929,14 @@ export async function openStatusPopover(page: Page) {
}
export async function openProjectMenu(page: Page, projectSlug: string) {
+ await openSidebar(page)
+ const item = page.locator(projectSwitchSelector(projectSlug)).first()
+ await expect(item).toBeVisible()
+ await item.hover()
+
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
await expect(trigger).toHaveCount(1)
+ await expect(trigger).toBeVisible()
const menu = page
.locator(dropdownMenuContentSelector)
@@ -807,7 +945,7 @@ export async function openProjectMenu(page: Page, projectSlug: string) {
const close = menu.locator(projectCloseMenuSelector(projectSlug)).first()
const clicked = await trigger
- .click({ timeout: 1500 })
+ .click({ force: true, timeout: 1500 })
.then(() => true)
.catch(() => false)