Skip to content

Commit b7e208b

Browse files
authored
test(app): share workspace slug wait helper across e2e specs (anomalyco#16446)
1 parent be9b4d1 commit b7e208b

5 files changed

Lines changed: 59 additions & 95 deletions

File tree

packages/app/e2e/AGENTS.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ test("test description", async ({ page, sdk, gotoSession }) => {
7272
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
7373
- `withSession(sdk, title, callback)` - Create temp session
7474
- `withProject(...)` - Create temp project/workspace
75+
- `sessionIDFromUrl(url)` - Read session ID from URL
76+
- `slugFromUrl(url)` - Read workspace slug from URL
77+
- `waitSlug(page, skip?)` - Wait for resolved workspace slug
7578
- `trackSession(sessionID, directory?)` - Register session for fixture cleanup
7679
- `trackDirectory(directory)` - Register directory for fixture cleanup
7780
- `clickListItem(container, filter)` - Click list item by key/text
@@ -169,9 +172,10 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
169172
1. Choose appropriate folder or create new one
170173
2. Import from `../fixtures`
171174
3. Use helper functions from `../actions` and `../selectors`
172-
4. Clean up any created resources
173-
5. Use specific selectors (avoid CSS classes)
174-
6. Test one feature per test file
175+
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.
176+
5. Clean up any created resources
177+
6. Use specific selectors (avoid CSS classes)
178+
7. Test one feature per test file
175179

176180
## Local Development
177181

packages/app/e2e/actions.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,33 @@ export async function cleanupTestProject(directory: string) {
199199
await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined)
200200
}
201201

202+
export function slugFromUrl(url: string) {
203+
return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
204+
}
205+
206+
export async function waitSlug(page: Page, skip: string[] = []) {
207+
let prev = ""
208+
let next = ""
209+
await expect
210+
.poll(
211+
() => {
212+
const slug = slugFromUrl(page.url())
213+
if (!slug) return ""
214+
if (skip.includes(slug)) return ""
215+
if (slug !== prev) {
216+
prev = slug
217+
next = ""
218+
return ""
219+
}
220+
next = slug
221+
return slug
222+
},
223+
{ timeout: 45_000 },
224+
)
225+
.not.toBe("")
226+
return next
227+
}
228+
202229
export function sessionIDFromUrl(url: string) {
203230
const match = /\/session\/([^/?#]+)/.exec(url)
204231
return match?.[1]

packages/app/e2e/projects/projects-switch.spec.ts

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { base64Decode } from "@opencode-ai/util/encode"
22
import type { Page } from "@playwright/test"
33
import { test, expect } from "../fixtures"
4-
import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl } from "../actions"
4+
import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions"
55
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
6-
import { dirSlug } from "../utils"
7-
8-
function slugFromUrl(url: string) {
9-
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
10-
}
6+
import { dirSlug, resolveDirectory } from "../utils"
117

128
async function workspaces(page: Page, directory: string, enabled: boolean) {
139
await page.evaluate(
@@ -76,7 +72,6 @@ test("switching back to a project opens the latest workspace session", async ({
7672

7773
const other = await createTestProject()
7874
const otherSlug = dirSlug(other)
79-
let workspaceDir: string | undefined
8075
try {
8176
await withProject(
8277
async ({ directory, slug, trackSession, trackDirectory }) => {
@@ -89,33 +84,27 @@ test("switching back to a project opens the latest workspace session", async ({
8984

9085
await page.getByRole("button", { name: "New workspace" }).first().click()
9186

92-
await expect
93-
.poll(
94-
() => {
95-
const next = slugFromUrl(page.url())
96-
if (!next) return ""
97-
if (next === slug) return ""
98-
return next
99-
},
100-
{ timeout: 45_000 },
101-
)
102-
.not.toBe("")
103-
104-
const workspaceSlug = slugFromUrl(page.url())
105-
workspaceDir = base64Decode(workspaceSlug)
106-
if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`)
107-
trackDirectory(workspaceDir)
87+
const raw = await waitSlug(page, [slug])
88+
const dir = base64Decode(raw)
89+
if (!dir) throw new Error(`Failed to decode workspace slug: ${raw}`)
90+
const space = await resolveDirectory(dir)
91+
const next = dirSlug(space)
92+
trackDirectory(space)
10893
await openSidebar(page)
10994

110-
const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
111-
await expect(workspace).toBeVisible()
112-
await workspace.hover()
95+
const item = page.locator(`${workspaceItemSelector(next)}, ${workspaceItemSelector(raw)}`).first()
96+
await expect(item).toBeVisible()
97+
await item.hover()
11398

114-
const newSession = page.locator(workspaceNewSessionSelector(workspaceSlug)).first()
115-
await expect(newSession).toBeVisible()
116-
await newSession.click({ force: true })
99+
const btn = page.locator(`${workspaceNewSessionSelector(next)}, ${workspaceNewSessionSelector(raw)}`).first()
100+
await expect(btn).toBeVisible()
101+
await btn.click({ force: true })
117102

118-
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))
103+
// A new workspace can be discovered via a transient slug before the route and sidebar
104+
// settle to the canonical workspace path on Windows, so interact with either and assert
105+
// against the resolved workspace slug.
106+
await waitSlug(page)
107+
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
119108

120109
// Create a session by sending a prompt
121110
const prompt = page.locator(promptSelector)
@@ -128,9 +117,9 @@ test("switching back to a project opens the latest workspace session", async ({
128117

129118
const created = sessionIDFromUrl(page.url())
130119
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
131-
trackSession(created, workspaceDir)
120+
trackSession(created, space)
132121

133-
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
122+
await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
134123

135124
await openSidebar(page)
136125

packages/app/e2e/projects/workspace-new-session.spec.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,10 @@
11
import { base64Decode } from "@opencode-ai/util/encode"
22
import type { Page } from "@playwright/test"
33
import { test, expect } from "../fixtures"
4-
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions"
4+
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions"
55
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
66
import { createSdk } from "../utils"
77

8-
function slugFromUrl(url: string) {
9-
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
10-
}
11-
12-
async function waitSlug(page: Page, skip: string[] = []) {
13-
let prev = ""
14-
await expect
15-
.poll(
16-
() => {
17-
const slug = slugFromUrl(page.url())
18-
if (!slug) return ""
19-
if (skip.includes(slug)) return ""
20-
if (slug !== prev) {
21-
prev = slug
22-
return ""
23-
}
24-
return slug
25-
},
26-
{ timeout: 45_000 },
27-
)
28-
.not.toBe("")
29-
return slugFromUrl(page.url())
30-
}
31-
328
async function waitWorkspaceReady(page: Page, slug: string) {
339
await openSidebar(page)
3410
await expect

packages/app/e2e/projects/workspaces.spec.ts

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,12 @@ import {
1414
openSidebar,
1515
openWorkspaceMenu,
1616
setWorkspacesEnabled,
17+
slugFromUrl,
18+
waitSlug,
1719
} from "../actions"
1820
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
1921
import { createSdk, dirSlug } from "../utils"
2022

21-
function slugFromUrl(url: string) {
22-
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
23-
}
24-
25-
async function waitSlug(page: Page, skip: string[] = []) {
26-
let prev = ""
27-
await expect
28-
.poll(
29-
() => {
30-
const slug = slugFromUrl(page.url())
31-
if (!slug) return ""
32-
if (skip.includes(slug)) return ""
33-
if (slug !== prev) {
34-
prev = slug
35-
return ""
36-
}
37-
return slug
38-
},
39-
{ timeout: 45_000 },
40-
)
41-
.not.toBe("")
42-
return slugFromUrl(page.url())
43-
}
44-
4523
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
4624
const rootSlug = project.slug
4725
await openSidebar(page)
@@ -353,17 +331,7 @@ test("can reorder workspaces by drag and drop", async ({ page, withProject }) =>
353331
for (const _ of [0, 1]) {
354332
const prev = slugFromUrl(page.url())
355333
await page.getByRole("button", { name: "New workspace" }).first().click()
356-
await expect
357-
.poll(
358-
() => {
359-
const slug = slugFromUrl(page.url())
360-
return slug.length > 0 && slug !== rootSlug && slug !== prev
361-
},
362-
{ timeout: 45_000 },
363-
)
364-
.toBe(true)
365-
366-
const slug = slugFromUrl(page.url())
334+
const slug = await waitSlug(page, [rootSlug, prev])
367335
const dir = base64Decode(slug)
368336
workspaces.push({ slug, directory: dir })
369337

0 commit comments

Comments
 (0)