summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e
diff options
context:
space:
mode:
authorBrendan Allan <[email protected]>2026-03-19 21:59:14 +0800
committerGitHub <[email protected]>2026-03-19 19:29:14 +0530
commit84f60d97a0c37c9136dfd965a66a0c8685a19e71 (patch)
tree3257e9da5114ddf3775b310db9b5707cc48267c4 /packages/app/e2e
parentcbf4b68fee8ff45c10a484b27f94e2dbe5f5dea2 (diff)
downloadopencode-84f60d97a0c37c9136dfd965a66a0c8685a19e71.tar.gz
opencode-84f60d97a0c37c9136dfd965a66a0c8685a19e71.zip
app: fix workspace flicker when switching directories (#18207)
Co-authored-by: Shoubhit Dash <[email protected]>
Diffstat (limited to 'packages/app/e2e')
-rw-r--r--packages/app/e2e/actions.ts25
-rw-r--r--packages/app/e2e/projects/projects-switch.spec.ts8
-rw-r--r--packages/app/e2e/projects/workspace-new-session.spec.ts60
-rw-r--r--packages/app/e2e/projects/workspaces.spec.ts29
-rw-r--r--packages/app/e2e/session/session-model-persistence.spec.ts14
5 files changed, 83 insertions, 53 deletions
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index aa047fb28..88d71f94c 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -1,3 +1,4 @@
+import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { expect, type Locator, type Page } from "@playwright/test"
import fs from "node:fs/promises"
import os from "node:os"
@@ -361,6 +362,30 @@ export async function waitSlug(page: Page, skip: string[] = []) {
return next
}
+export async function resolveSlug(slug: string) {
+ const directory = base64Decode(slug)
+ if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
+ const resolved = await resolveDirectory(directory)
+ return { directory: resolved, slug: base64Encode(resolved), raw: slug }
+}
+
+export async function waitDir(page: Page, directory: string) {
+ const target = await resolveDirectory(directory)
+ await expect
+ .poll(
+ async () => {
+ const slug = slugFromUrl(page.url())
+ if (!slug) return ""
+ return resolveSlug(slug)
+ .then((item) => item.directory)
+ .catch(() => "")
+ },
+ { timeout: 45_000 },
+ )
+ .toBe(target)
+ return { directory: target, slug: base64Encode(target) }
+}
+
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 6ad64f592..1416aec72 100644
--- a/packages/app/e2e/projects/projects-switch.spec.ts
+++ b/packages/app/e2e/projects/projects-switch.spec.ts
@@ -1,7 +1,7 @@
import { base64Decode } from "@opencode-ai/util/encode"
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
-import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions"
+import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitDir, waitSlug } from "../actions"
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
import { dirSlug, resolveDirectory } from "../utils"
@@ -100,11 +100,8 @@ test("switching back to a project opens the latest workspace session", async ({
await expect(btn).toBeVisible()
await btn.click({ force: true })
- // 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(?:[/?#]|$)`))
+ await waitDir(page, space)
// Create a session by sending a prompt
const prompt = page.locator(promptSelector)
@@ -132,6 +129,7 @@ test("switching back to a project opens the latest workspace session", async ({
await expect(rootButton).toBeVisible()
await rootButton.click()
+ await waitDir(page, space)
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created)
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
},
diff --git a/packages/app/e2e/projects/workspace-new-session.spec.ts b/packages/app/e2e/projects/workspace-new-session.spec.ts
index 18fa46d32..0858f2627 100644
--- a/packages/app/e2e/projects/workspace-new-session.spec.ts
+++ b/packages/app/e2e/projects/workspace-new-session.spec.ts
@@ -1,18 +1,25 @@
-import { base64Decode } from "@opencode-ai/util/encode"
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
-import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions"
+import { openSidebar, resolveSlug, sessionIDFromUrl, setWorkspacesEnabled, waitDir, waitSlug } from "../actions"
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
import { createSdk } from "../utils"
-async function waitWorkspaceReady(page: Page, slug: string) {
+function item(space: { slug: string; raw: string }) {
+ return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}`
+}
+
+function button(space: { slug: string; raw: string }) {
+ return `${workspaceNewSessionSelector(space.slug)}, ${workspaceNewSessionSelector(space.raw)}`
+}
+
+async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) {
await openSidebar(page)
await expect
.poll(
async () => {
- const item = page.locator(workspaceItemSelector(slug)).first()
+ const row = page.locator(item(space)).first()
try {
- await item.hover({ timeout: 500 })
+ await row.hover({ timeout: 500 })
return true
} catch {
return false
@@ -27,29 +34,30 @@ async function createWorkspace(page: Page, root: string, seen: string[]) {
await openSidebar(page)
await page.getByRole("button", { name: "New workspace" }).first().click()
- const slug = await waitSlug(page, [root, ...seen])
- const directory = base64Decode(slug)
- if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
- return { slug, directory }
+ const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
+ await waitDir(page, next.directory)
+ return next
}
-async function openWorkspaceNewSession(page: Page, slug: string) {
- await waitWorkspaceReady(page, slug)
+async function openWorkspaceNewSession(page: Page, space: { slug: string; raw: string; directory: string }) {
+ await waitWorkspaceReady(page, space)
- const item = page.locator(workspaceItemSelector(slug)).first()
- await item.hover()
+ const row = page.locator(item(space)).first()
+ await row.hover()
- const button = page.locator(workspaceNewSessionSelector(slug)).first()
- await expect(button).toBeVisible()
- await button.click({ force: true })
+ const next = page.locator(button(space)).first()
+ await expect(next).toBeVisible()
+ await next.click({ force: true })
- const next = await waitSlug(page)
- await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
- return next
+ return waitDir(page, space.directory)
}
-async function createSessionFromWorkspace(page: Page, slug: string, text: string) {
- const next = await openWorkspaceNewSession(page, slug)
+async function createSessionFromWorkspace(
+ page: Page,
+ space: { slug: string; raw: string; directory: string },
+ text: string,
+) {
+ const next = await openWorkspaceNewSession(page, space)
const prompt = page.locator(promptSelector)
await expect(prompt).toBeVisible()
@@ -60,13 +68,13 @@ async function createSessionFromWorkspace(page: Page, slug: string, text: string
await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
await prompt.press("Enter")
- await expect.poll(() => slugFromUrl(page.url())).toBe(next)
+ await waitDir(page, next.directory)
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
const sessionID = sessionIDFromUrl(page.url())
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
- await expect(page).toHaveURL(new RegExp(`/${next}/session/${sessionID}(?:[/?#]|$)`))
- return { sessionID, slug: next }
+ await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
+ return { sessionID, slug: next.slug }
}
async function sessionDirectory(directory: string, sessionID: string) {
@@ -87,11 +95,11 @@ test("new sessions from sidebar workspace actions stay in selected workspace", a
const first = await createWorkspace(page, root, [])
trackDirectory(first.directory)
- await waitWorkspaceReady(page, first.slug)
+ await waitWorkspaceReady(page, first)
const second = await createWorkspace(page, root, [first.slug])
trackDirectory(second.directory)
- await waitWorkspaceReady(page, second.slug)
+ await waitWorkspaceReady(page, second)
const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
trackSession(firstSession.sessionID, first.directory)
diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts
index aeeccb9bb..8ee899f18 100644
--- a/packages/app/e2e/projects/workspaces.spec.ts
+++ b/packages/app/e2e/projects/workspaces.spec.ts
@@ -1,4 +1,3 @@
-import { base64Decode } from "@opencode-ai/util/encode"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
@@ -13,8 +12,10 @@ import {
confirmDialog,
openSidebar,
openWorkspaceMenu,
+ resolveSlug,
setWorkspacesEnabled,
slugFromUrl,
+ waitDir,
waitSlug,
} from "../actions"
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
@@ -27,15 +28,15 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
await setWorkspacesEnabled(page, rootSlug, true)
await page.getByRole("button", { name: "New workspace" }).first().click()
- const slug = await waitSlug(page, [rootSlug])
- const dir = base64Decode(slug)
+ const next = await resolveSlug(await waitSlug(page, [rootSlug]))
+ await waitDir(page, next.directory)
await openSidebar(page)
await expect
.poll(
async () => {
- const item = page.locator(workspaceItemSelector(slug)).first()
+ const item = page.locator(workspaceItemSelector(next.slug)).first()
try {
await item.hover({ timeout: 500 })
return true
@@ -47,7 +48,7 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
)
.toBe(true)
- return { rootSlug, slug, directory: dir }
+ return { rootSlug, slug: next.slug, directory: next.directory }
}
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
@@ -79,15 +80,15 @@ test("can create a workspace", async ({ page, withProject }) => {
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
await page.getByRole("button", { name: "New workspace" }).first().click()
- const workspaceSlug = await waitSlug(page, [slug])
- const workspaceDir = base64Decode(workspaceSlug)
+ const next = await resolveSlug(await waitSlug(page, [slug]))
+ await waitDir(page, next.directory)
await openSidebar(page)
await expect
.poll(
async () => {
- const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
+ const item = page.locator(workspaceItemSelector(next.slug)).first()
try {
await item.hover({ timeout: 500 })
return true
@@ -99,9 +100,9 @@ test("can create a workspace", async ({ page, withProject }) => {
)
.toBe(true)
- await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
+ await expect(page.locator(workspaceItemSelector(next.slug)).first()).toBeVisible()
- await cleanupTestProject(workspaceDir)
+ await cleanupTestProject(next.directory)
})
})
@@ -119,7 +120,7 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
- const activeDir = base64Decode(slugFromUrl(page.url()))
+ const activeDir = await resolveSlug(slugFromUrl(page.url())).then((item) => item.directory)
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
await openSidebar(page)
@@ -331,9 +332,9 @@ 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()
- const slug = await waitSlug(page, [rootSlug, prev])
- const dir = base64Decode(slug)
- workspaces.push({ slug, directory: dir })
+ const next = await resolveSlug(await waitSlug(page, [rootSlug, prev]))
+ await waitDir(page, next.directory)
+ workspaces.push(next)
await openSidebar(page)
}
diff --git a/packages/app/e2e/session/session-model-persistence.spec.ts b/packages/app/e2e/session/session-model-persistence.spec.ts
index 933d5e6f9..2c2e4e886 100644
--- a/packages/app/e2e/session/session-model-persistence.spec.ts
+++ b/packages/app/e2e/session/session-model-persistence.spec.ts
@@ -1,7 +1,6 @@
-import { base64Decode } from "@opencode-ai/util/encode"
import type { Locator, Page } from "@playwright/test"
import { test, expect } from "../fixtures"
-import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, waitSessionIdle, waitSlug } from "../actions"
+import { openSidebar, resolveSlug, sessionIDFromUrl, setWorkspacesEnabled, waitSessionIdle, waitSlug } from "../actions"
import {
promptAgentSelector,
promptModelSelector,
@@ -224,10 +223,9 @@ async function createWorkspace(page: Page, root: string, seen: string[]) {
await openSidebar(page)
await page.getByRole("button", { name: "New workspace" }).first().click()
- const slug = await waitSlug(page, [root, ...seen])
- const directory = base64Decode(slug)
- if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
- return { slug, directory }
+ const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
+ await expect(page).toHaveURL(new RegExp(`/${next.slug}/session(?:[/?#]|$)`))
+ return next
}
async function waitWorkspace(page: Page, slug: string) {
@@ -257,8 +255,8 @@ async function newWorkspaceSession(page: Page, slug: string) {
await expect(button).toBeVisible()
await button.click({ force: true })
- const next = await waitSlug(page)
- await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
+ const next = await resolveSlug(await waitSlug(page))
+ await expect(page).toHaveURL(new RegExp(`/${next.slug}/session(?:[/?#]|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
return currentDir(page)
}