summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e/projects/workspaces.spec.ts
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-02 14:17:28 -0400
committerGitHub <[email protected]>2026-04-02 18:17:28 +0000
commitc3ef69c8664ab84464d720f75eb9bf35289c1847 (patch)
treed7b1aa78355c39472804bdfbbd9db87722d0587b /packages/app/e2e/projects/workspaces.spec.ts
parent363891126c9b2a909db0e724eb6377bd3d12b38b (diff)
downloadopencode-c3ef69c8664ab84464d720f75eb9bf35289c1847.tar.gz
opencode-c3ef69c8664ab84464d720f75eb9bf35289c1847.zip
test(app): add a golden path for mocked e2e prompts (#20593)
Diffstat (limited to 'packages/app/e2e/projects/workspaces.spec.ts')
-rw-r--r--packages/app/e2e/projects/workspaces.spec.ts507
1 files changed, 250 insertions, 257 deletions
diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts
index 297cdb9fc..206baa47c 100644
--- a/packages/app/e2e/projects/workspaces.spec.ts
+++ b/packages/app/e2e/projects/workspaces.spec.ts
@@ -19,10 +19,10 @@ import {
waitDir,
waitSlug,
} from "../actions"
-import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
-import { createSdk, dirSlug } from "../utils"
+import { inlineInputSelector, workspaceItemSelector } from "../selectors"
+import { dirSlug } from "../utils"
-async function setupWorkspaceTest(page: Page, project: { slug: string }) {
+async function setupWorkspaceTest(page: Page, project: { slug: string; trackDirectory: (directory: string) => void }) {
const rootSlug = project.slug
await openSidebar(page)
@@ -31,6 +31,7 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
await page.getByRole("button", { name: "New workspace" }).first().click()
const next = await resolveSlug(await waitSlug(page, [rootSlug]))
await waitDir(page, next.directory)
+ project.trackDirectory(next.directory)
await openSidebar(page)
@@ -52,62 +53,59 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
return { rootSlug, slug: next.slug, directory: next.directory }
}
-test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
+test("can enable and disable workspaces from project menu", async ({ page, project }) => {
await page.setViewportSize({ width: 1400, height: 800 })
+ await project.open()
- await withProject(async ({ slug }) => {
- await openSidebar(page)
+ await openSidebar(page)
- await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
- await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
+ await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
+ await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
- await setWorkspacesEnabled(page, slug, true)
- await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
- await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible()
+ await setWorkspacesEnabled(page, project.slug, true)
+ await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
+ await expect(page.locator(workspaceItemSelector(project.slug)).first()).toBeVisible()
- await setWorkspacesEnabled(page, slug, false)
- await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
- await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
- })
+ await setWorkspacesEnabled(page, project.slug, false)
+ await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
+ await expect(page.locator(workspaceItemSelector(project.slug))).toHaveCount(0)
})
-test("can create a workspace", async ({ page, withProject }) => {
+test("can create a workspace", async ({ page, project }) => {
await page.setViewportSize({ width: 1400, height: 800 })
+ await project.open()
- await withProject(async ({ slug }) => {
- await openSidebar(page)
- await setWorkspacesEnabled(page, slug, true)
-
- await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
+ await openSidebar(page)
+ await setWorkspacesEnabled(page, project.slug, true)
- await page.getByRole("button", { name: "New workspace" }).first().click()
- const next = await resolveSlug(await waitSlug(page, [slug]))
- await waitDir(page, next.directory)
+ await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
- await openSidebar(page)
+ await page.getByRole("button", { name: "New workspace" }).first().click()
+ const next = await resolveSlug(await waitSlug(page, [project.slug]))
+ await waitDir(page, next.directory)
+ project.trackDirectory(next.directory)
- await expect
- .poll(
- async () => {
- const item = page.locator(workspaceItemSelector(next.slug)).first()
- try {
- await item.hover({ timeout: 500 })
- return true
- } catch {
- return false
- }
- },
- { timeout: 60_000 },
- )
- .toBe(true)
+ await openSidebar(page)
- await expect(page.locator(workspaceItemSelector(next.slug)).first()).toBeVisible()
+ await expect
+ .poll(
+ async () => {
+ const item = page.locator(workspaceItemSelector(next.slug)).first()
+ try {
+ await item.hover({ timeout: 500 })
+ return true
+ } catch {
+ return false
+ }
+ },
+ { timeout: 60_000 },
+ )
+ .toBe(true)
- await cleanupTestProject(next.directory)
- })
+ await expect(page.locator(workspaceItemSelector(next.slug)).first()).toBeVisible()
})
-test("non-git projects keep workspace mode disabled", async ({ page, withProject }) => {
+test("non-git projects keep workspace mode disabled", async ({ page, project }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const nonGit = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-nongit-"))
@@ -116,260 +114,255 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject
await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")
try {
- await withProject(async () => {
- await page.goto(`/${nonGitSlug}/session`)
-
- await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
+ await project.open({ extra: [nonGit] })
+ await page.goto(`/${nonGitSlug}/session`)
- const activeDir = await resolveSlug(slugFromUrl(page.url())).then((item) => item.directory)
- expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
+ await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
- await openSidebar(page)
- await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
-
- const trigger = page.locator('[data-action="project-menu"]').first()
- const hasMenu = await trigger
- .isVisible()
- .then((x) => x)
- .catch(() => false)
- if (!hasMenu) return
+ const activeDir = await resolveSlug(slugFromUrl(page.url())).then((item) => item.directory)
+ expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
- await trigger.click({ force: true })
-
- const menu = page.locator(dropdownMenuContentSelector).first()
- await expect(menu).toBeVisible()
-
- const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first()
-
- await expect(toggle).toBeVisible()
- await expect(toggle).toBeDisabled()
- await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
- })
+ await openSidebar(page)
+ await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
+ await expect(page.getByRole("button", { name: "Create Git repository" })).toBeVisible()
} finally {
await cleanupTestProject(nonGit)
}
})
-test("can rename a workspace", async ({ page, withProject }) => {
+test("can rename a workspace", async ({ page, project }) => {
await page.setViewportSize({ width: 1400, height: 800 })
-
- await withProject(async (project) => {
- const { slug } = await setupWorkspaceTest(page, project)
-
- const rename = `e2e workspace ${Date.now()}`
- const menu = await openWorkspaceMenu(page, slug)
- await clickMenuItem(menu, /^Rename$/i, { force: true })
-
- await expect(menu).toHaveCount(0)
-
- const item = page.locator(workspaceItemSelector(slug)).first()
- await expect(item).toBeVisible()
- const input = item.locator(inlineInputSelector).first()
- await expect(input).toBeVisible()
- await input.fill(rename)
- await input.press("Enter")
- await expect(item).toContainText(rename)
- })
+ await project.open()
+
+ const { slug } = await setupWorkspaceTest(page, project)
+
+ const rename = `e2e workspace ${Date.now()}`
+ const menu = await openWorkspaceMenu(page, slug)
+ await clickMenuItem(menu, /^Rename$/i, { force: true })
+
+ await expect(menu).toHaveCount(0)
+
+ const item = page.locator(workspaceItemSelector(slug)).first()
+ await expect(item).toBeVisible()
+ const input = item.locator(inlineInputSelector).first()
+ const shown = await input
+ .isVisible()
+ .then((x) => x)
+ .catch(() => false)
+ if (!shown) {
+ const retry = await openWorkspaceMenu(page, slug)
+ await clickMenuItem(retry, /^Rename$/i, { force: true })
+ await expect(retry).toHaveCount(0)
+ }
+ await expect(input).toBeVisible()
+ await input.fill(rename)
+ await input.press("Enter")
+ await expect(item).toContainText(rename)
})
-test("can reset a workspace", async ({ page, sdk, withProject }) => {
+test("can reset a workspace", async ({ page, project }) => {
await page.setViewportSize({ width: 1400, height: 800 })
+ await project.open()
- await withProject(async (project) => {
- const { slug, directory: createdDir } = await setupWorkspaceTest(page, project)
+ const { slug, directory: createdDir } = await setupWorkspaceTest(page, project)
- const readme = path.join(createdDir, "README.md")
- const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
- const original = await fs.readFile(readme, "utf8")
- const dirty = `${original.trimEnd()}\n\nchange_${Date.now()}\n`
- await fs.writeFile(readme, dirty, "utf8")
- await fs.writeFile(extra, `created_${Date.now()}\n`, "utf8")
+ const readme = path.join(createdDir, "README.md")
+ const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
+ const original = await fs.readFile(readme, "utf8")
+ const dirty = `${original.trimEnd()}\n\nchange_${Date.now()}\n`
+ await fs.writeFile(readme, dirty, "utf8")
+ await fs.writeFile(extra, `created_${Date.now()}\n`, "utf8")
- await expect
- .poll(async () => {
- return await fs
- .stat(extra)
- .then(() => true)
- .catch(() => false)
- })
- .toBe(true)
+ await expect
+ .poll(async () => {
+ return await fs
+ .stat(extra)
+ .then(() => true)
+ .catch(() => false)
+ })
+ .toBe(true)
- await expect
- .poll(async () => {
- const files = await sdk.file
+ await expect
+ .poll(async () => {
+ const files = await project.sdk.file
+ .status({ directory: createdDir })
+ .then((r) => r.data ?? [])
+ .catch(() => [])
+ return files.length
+ })
+ .toBeGreaterThan(0)
+
+ const menu = await openWorkspaceMenu(page, slug)
+ await clickMenuItem(menu, /^Reset$/i, { force: true })
+ await confirmDialog(page, /^Reset workspace$/i)
+
+ await expect
+ .poll(
+ async () => {
+ const files = await project.sdk.file
.status({ directory: createdDir })
.then((r) => r.data ?? [])
.catch(() => [])
return files.length
- })
- .toBeGreaterThan(0)
-
- const menu = await openWorkspaceMenu(page, slug)
- await clickMenuItem(menu, /^Reset$/i, { force: true })
- await confirmDialog(page, /^Reset workspace$/i)
-
- await expect
- .poll(
- async () => {
- const files = await sdk.file
- .status({ directory: createdDir })
- .then((r) => r.data ?? [])
- .catch(() => [])
- return files.length
- },
- { timeout: 60_000 },
- )
- .toBe(0)
+ },
+ { timeout: 120_000 },
+ )
+ .toBe(0)
- await expect.poll(() => fs.readFile(readme, "utf8"), { timeout: 60_000 }).toBe(original)
+ await expect.poll(() => fs.readFile(readme, "utf8"), { timeout: 120_000 }).toBe(original)
- await expect
- .poll(async () => {
- return await fs
- .stat(extra)
- .then(() => true)
- .catch(() => false)
- })
- .toBe(false)
- })
+ await expect
+ .poll(async () => {
+ return await fs
+ .stat(extra)
+ .then(() => true)
+ .catch(() => false)
+ })
+ .toBe(false)
})
-test("can delete a workspace", async ({ page, withProject }) => {
+test("can reorder workspaces by drag and drop", async ({ page, project }) => {
await page.setViewportSize({ width: 1400, height: 800 })
+ await project.open()
+ const rootSlug = project.slug
- await withProject(async (project) => {
- const sdk = createSdk(project.directory)
- const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project)
+ const listSlugs = async () => {
+ const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
+ const slugs = await nodes.evaluateAll((els) => {
+ return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
+ })
+ return slugs
+ }
+ const waitReady = async (slug: string) => {
await expect
.poll(
async () => {
- const worktrees = await sdk.worktree
- .list()
- .then((r) => r.data ?? [])
- .catch(() => [] as string[])
- return worktrees.includes(directory)
+ const item = page.locator(workspaceItemSelector(slug)).first()
+ try {
+ await item.hover({ timeout: 500 })
+ return true
+ } catch {
+ return false
+ }
},
- { timeout: 30_000 },
+ { timeout: 60_000 },
)
.toBe(true)
+ }
- const menu = await openWorkspaceMenu(page, slug)
- await clickMenuItem(menu, /^Delete$/i, { force: true })
- await confirmDialog(page, /^Delete workspace$/i)
+ const drag = async (from: string, to: string) => {
+ const src = page.locator(workspaceItemSelector(from)).first()
+ const dst = page.locator(workspaceItemSelector(to)).first()
- await expect.poll(() => base64Decode(slugFromUrl(page.url()))).toBe(project.directory)
+ const a = await src.boundingBox()
+ const b = await dst.boundingBox()
+ if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
- await expect
- .poll(
- async () => {
- const worktrees = await sdk.worktree
- .list()
- .then((r) => r.data ?? [])
- .catch(() => [] as string[])
- return worktrees.includes(directory)
- },
- { timeout: 60_000 },
- )
- .toBe(false)
+ await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
+ await page.mouse.down()
+ await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
+ await page.mouse.up()
+ }
+
+ await openSidebar(page)
- await project.gotoSession()
+ await setWorkspacesEnabled(page, rootSlug, true)
+
+ const workspaces = [] as { directory: string; slug: string }[]
+ for (const _ of [0, 1]) {
+ const prev = slugFromUrl(page.url())
+ await page.getByRole("button", { name: "New workspace" }).first().click()
+ const next = await resolveSlug(await waitSlug(page, [rootSlug, prev]))
+ await waitDir(page, next.directory)
+ project.trackDirectory(next.directory)
+ workspaces.push(next)
await openSidebar(page)
- await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
- await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
- })
+ }
+
+ if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
+
+ const a = workspaces[0].slug
+ const b = workspaces[1].slug
+
+ await waitReady(a)
+ await waitReady(b)
+
+ const list = async () => {
+ const slugs = await listSlugs()
+ return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
+ }
+
+ await expect
+ .poll(async () => {
+ const slugs = await list()
+ return slugs.length === 2
+ })
+ .toBe(true)
+
+ const before = await list()
+ const from = before[1]
+ const to = before[0]
+ if (!from || !to) throw new Error("Failed to resolve initial workspace order")
+
+ await drag(from, to)
+
+ await expect.poll(async () => await list()).toEqual([from, to])
})
-test("can reorder workspaces by drag and drop", async ({ page, withProject }) => {
+test("can delete a workspace", async ({ page, project }) => {
await page.setViewportSize({ width: 1400, height: 800 })
- await withProject(async ({ slug: rootSlug }) => {
- const workspaces = [] as { directory: string; slug: string }[]
-
- const listSlugs = async () => {
- const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
- const slugs = await nodes.evaluateAll((els) => {
- return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
- })
- return slugs
- }
-
- const waitReady = async (slug: string) => {
- await expect
- .poll(
- async () => {
- const item = page.locator(workspaceItemSelector(slug)).first()
- try {
- await item.hover({ timeout: 500 })
- return true
- } catch {
- return false
- }
- },
- { timeout: 60_000 },
- )
- .toBe(true)
- }
-
- const drag = async (from: string, to: string) => {
- const src = page.locator(workspaceItemSelector(from)).first()
- const dst = page.locator(workspaceItemSelector(to)).first()
-
- const a = await src.boundingBox()
- const b = await dst.boundingBox()
- if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
-
- await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
- await page.mouse.down()
- await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
- await page.mouse.up()
- }
-
- try {
- await openSidebar(page)
-
- await setWorkspacesEnabled(page, rootSlug, true)
-
- for (const _ of [0, 1]) {
- const prev = slugFromUrl(page.url())
- await page.getByRole("button", { name: "New workspace" }).first().click()
- const next = await resolveSlug(await waitSlug(page, [rootSlug, prev]))
- await waitDir(page, next.directory)
- workspaces.push(next)
-
- await openSidebar(page)
- }
-
- if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
-
- const a = workspaces[0].slug
- const b = workspaces[1].slug
-
- await waitReady(a)
- await waitReady(b)
-
- const list = async () => {
- const slugs = await listSlugs()
- return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
- }
-
- await expect
- .poll(async () => {
- const slugs = await list()
- return slugs.length === 2
- })
- .toBe(true)
-
- const before = await list()
- const from = before[1]
- const to = before[0]
- if (!from || !to) throw new Error("Failed to resolve initial workspace order")
-
- await drag(from, to)
-
- await expect.poll(async () => await list()).toEqual([from, to])
- } finally {
- await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
- }
- })
+ await project.open()
+
+ const rootSlug = project.slug
+ await openSidebar(page)
+ await setWorkspacesEnabled(page, rootSlug, true)
+
+ const created = await project.sdk.worktree.create({ directory: project.directory }).then((res) => res.data)
+ if (!created?.directory) throw new Error("Failed to create workspace for delete test")
+
+ const directory = created.directory
+ const slug = dirSlug(directory)
+ project.trackDirectory(directory)
+
+ await page.reload()
+ await openSidebar(page)
+ await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible({ timeout: 60_000 })
+
+ await expect
+ .poll(
+ async () => {
+ const worktrees = await project.sdk.worktree
+ .list()
+ .then((r) => r.data ?? [])
+ .catch(() => [] as string[])
+ return worktrees.includes(directory)
+ },
+ { timeout: 30_000 },
+ )
+ .toBe(true)
+
+ const menu = await openWorkspaceMenu(page, slug)
+ await clickMenuItem(menu, /^Delete$/i, { force: true })
+ await confirmDialog(page, /^Delete workspace$/i)
+
+ await expect.poll(() => base64Decode(slugFromUrl(page.url()))).toBe(project.directory)
+
+ await expect
+ .poll(
+ async () => {
+ const worktrees = await project.sdk.worktree
+ .list()
+ .then((r) => r.data ?? [])
+ .catch(() => [] as string[])
+ return worktrees.includes(directory)
+ },
+ { timeout: 60_000 },
+ )
+ .toBe(false)
+
+ await openSidebar(page)
+ await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
+ await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
})