diff options
| author | Luke Parker <[email protected]> | 2026-03-20 14:12:06 +1000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-20 00:12:06 -0400 |
| commit | d460614cd7ad9e047a2792139ea67e16caa82ea7 (patch) | |
| tree | ff415e8719c7b6edd73bc824da379308e4e62589 /packages/app/e2e/actions.ts | |
| parent | 7866dbcfcc36a60d22ad466eddf54c54b21fabe3 (diff) | |
| download | opencode-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.ts | 152 |
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) |
