summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-03-07 17:48:30 +1000
committerGitHub <[email protected]>2026-03-07 07:48:30 +0000
commitb7e208b4f1e6641a1cbb1e13f59789c7b7f4c60a (patch)
tree44a08ed1524957a5d59bf5a03ce46fe61b53f5b7
parentbe9b4d1bcde39341c7813b66c5c19150a01bb8c2 (diff)
downloadopencode-b7e208b4f1e6641a1cbb1e13f59789c7b7f4c60a.tar.gz
opencode-b7e208b4f1e6641a1cbb1e13f59789c7b7f4c60a.zip
test(app): share workspace slug wait helper across e2e specs (#16446)
-rw-r--r--packages/app/e2e/AGENTS.md10
-rw-r--r--packages/app/e2e/actions.ts27
-rw-r--r--packages/app/e2e/projects/projects-switch.spec.ts53
-rw-r--r--packages/app/e2e/projects/workspace-new-session.spec.ts26
-rw-r--r--packages/app/e2e/projects/workspaces.spec.ts38
5 files changed, 59 insertions, 95 deletions
diff --git a/packages/app/e2e/AGENTS.md b/packages/app/e2e/AGENTS.md
index f97838978..8bfbd111b 100644
--- a/packages/app/e2e/AGENTS.md
+++ b/packages/app/e2e/AGENTS.md
@@ -72,6 +72,9 @@ test("test description", async ({ page, sdk, gotoSession }) => {
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
- `withSession(sdk, title, callback)` - Create temp session
- `withProject(...)` - Create temp project/workspace
+- `sessionIDFromUrl(url)` - Read session ID from URL
+- `slugFromUrl(url)` - Read workspace slug from URL
+- `waitSlug(page, skip?)` - Wait for resolved workspace slug
- `trackSession(sessionID, directory?)` - Register session for fixture cleanup
- `trackDirectory(directory)` - Register directory for fixture cleanup
- `clickListItem(container, filter)` - Click list item by key/text
@@ -169,9 +172,10 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
1. Choose appropriate folder or create new one
2. Import from `../fixtures`
3. Use helper functions from `../actions` and `../selectors`
-4. Clean up any created resources
-5. Use specific selectors (avoid CSS classes)
-6. Test one feature per test file
+4. When validating routing, use shared helpers from `../actions`. Workspace URL slugs can be canonicalized on Windows, so assert against canonical or resolved workspace slugs.
+5. Clean up any created resources
+6. Use specific selectors (avoid CSS classes)
+7. Test one feature per test file
## Local Development
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index 2354b88e8..86147dc65 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -199,6 +199,33 @@ export async function cleanupTestProject(directory: string) {
await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined)
}
+export function slugFromUrl(url: string) {
+ return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
+}
+
+export async function waitSlug(page: Page, skip: string[] = []) {
+ let prev = ""
+ let next = ""
+ await expect
+ .poll(
+ () => {
+ const slug = slugFromUrl(page.url())
+ if (!slug) return ""
+ if (skip.includes(slug)) return ""
+ if (slug !== prev) {
+ prev = slug
+ next = ""
+ return ""
+ }
+ next = slug
+ return slug
+ },
+ { timeout: 45_000 },
+ )
+ .not.toBe("")
+ return next
+}
+
export function sessionIDFromUrl(url: string) {
const match = /\/session\/([^/?#]+)/.exec(url)
return match?.[1]
diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts
index a942f29e0..6ad64f592 100644
--- a/packages/app/e2e/projects/projects-switch.spec.ts
+++ b/packages/app/e2e/projects/projects-switch.spec.ts
@@ -1,13 +1,9 @@
import { base64Decode } from "@opencode-ai/util/encode"
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
-import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl } from "../actions"
+import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions"
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
-import { dirSlug } from "../utils"
-
-function slugFromUrl(url: string) {
- return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
-}
+import { dirSlug, resolveDirectory } from "../utils"
async function workspaces(page: Page, directory: string, enabled: boolean) {
await page.evaluate(
@@ -76,7 +72,6 @@ test("switching back to a project opens the latest workspace session", async ({
const other = await createTestProject()
const otherSlug = dirSlug(other)
- let workspaceDir: string | undefined
try {
await withProject(
async ({ directory, slug, trackSession, trackDirectory }) => {
@@ -89,33 +84,27 @@ test("switching back to a project opens the latest workspace session", async ({
await page.getByRole("button", { name: "New workspace" }).first().click()
- await expect
- .poll(
- () => {
- const next = slugFromUrl(page.url())
- if (!next) return ""
- if (next === slug) return ""
- return next
- },
- { timeout: 45_000 },
- )
- .not.toBe("")
-
- const workspaceSlug = slugFromUrl(page.url())
- workspaceDir = base64Decode(workspaceSlug)
- if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`)
- trackDirectory(workspaceDir)
+ const raw = await waitSlug(page, [slug])
+ const dir = base64Decode(raw)
+ if (!dir) throw new Error(`Failed to decode workspace slug: ${raw}`)
+ const space = await resolveDirectory(dir)
+ const next = dirSlug(space)
+ trackDirectory(space)
await openSidebar(page)
- const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
- await expect(workspace).toBeVisible()
- await workspace.hover()
+ const item = page.locator(`${workspaceItemSelector(next)}, ${workspaceItemSelector(raw)}`).first()
+ await expect(item).toBeVisible()
+ await item.hover()
- const newSession = page.locator(workspaceNewSessionSelector(workspaceSlug)).first()
- await expect(newSession).toBeVisible()
- await newSession.click({ force: true })
+ const btn = page.locator(`${workspaceNewSessionSelector(next)}, ${workspaceNewSessionSelector(raw)}`).first()
+ await expect(btn).toBeVisible()
+ await btn.click({ force: true })
- await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))
+ // A new workspace can be discovered via a transient slug before the route and sidebar
+ // settle to the canonical workspace path on Windows, so interact with either and assert
+ // against the resolved workspace slug.
+ await waitSlug(page)
+ await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
// Create a session by sending a prompt
const prompt = page.locator(promptSelector)
@@ -128,9 +117,9 @@ test("switching back to a project opens the latest workspace session", async ({
const created = sessionIDFromUrl(page.url())
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
- trackSession(created, workspaceDir)
+ trackSession(created, space)
- await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
+ await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
await openSidebar(page)
diff --git a/packages/app/e2e/projects/workspace-new-session.spec.ts b/packages/app/e2e/projects/workspace-new-session.spec.ts
index 621ba0f3a..18fa46d32 100644
--- a/packages/app/e2e/projects/workspace-new-session.spec.ts
+++ b/packages/app/e2e/projects/workspace-new-session.spec.ts
@@ -1,34 +1,10 @@
import { base64Decode } from "@opencode-ai/util/encode"
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
-import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions"
+import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions"
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
import { createSdk } from "../utils"
-function slugFromUrl(url: string) {
- return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
-}
-
-async function waitSlug(page: Page, skip: string[] = []) {
- let prev = ""
- await expect
- .poll(
- () => {
- const slug = slugFromUrl(page.url())
- if (!slug) return ""
- if (skip.includes(slug)) return ""
- if (slug !== prev) {
- prev = slug
- return ""
- }
- return slug
- },
- { timeout: 45_000 },
- )
- .not.toBe("")
- return slugFromUrl(page.url())
-}
-
async function waitWorkspaceReady(page: Page, slug: string) {
await openSidebar(page)
await expect
diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts
index 805b45e98..aeeccb9bb 100644
--- a/packages/app/e2e/projects/workspaces.spec.ts
+++ b/packages/app/e2e/projects/workspaces.spec.ts
@@ -14,34 +14,12 @@ import {
openSidebar,
openWorkspaceMenu,
setWorkspacesEnabled,
+ slugFromUrl,
+ waitSlug,
} from "../actions"
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
import { createSdk, dirSlug } from "../utils"
-function slugFromUrl(url: string) {
- return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
-}
-
-async function waitSlug(page: Page, skip: string[] = []) {
- let prev = ""
- await expect
- .poll(
- () => {
- const slug = slugFromUrl(page.url())
- if (!slug) return ""
- if (skip.includes(slug)) return ""
- if (slug !== prev) {
- prev = slug
- return ""
- }
- return slug
- },
- { timeout: 45_000 },
- )
- .not.toBe("")
- return slugFromUrl(page.url())
-}
-
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
const rootSlug = project.slug
await openSidebar(page)
@@ -353,17 +331,7 @@ test("can reorder workspaces by drag and drop", async ({ page, withProject }) =>
for (const _ of [0, 1]) {
const prev = slugFromUrl(page.url())
await page.getByRole("button", { name: "New workspace" }).first().click()
- await expect
- .poll(
- () => {
- const slug = slugFromUrl(page.url())
- return slug.length > 0 && slug !== rootSlug && slug !== prev
- },
- { timeout: 45_000 },
- )
- .toBe(true)
-
- const slug = slugFromUrl(page.url())
+ const slug = await waitSlug(page, [rootSlug, prev])
const dir = base64Decode(slug)
workspaces.push({ slug, directory: dir })