diff options
| author | Kit Langton <[email protected]> | 2026-04-02 14:17:28 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-04-02 18:17:28 +0000 |
| commit | c3ef69c8664ab84464d720f75eb9bf35289c1847 (patch) | |
| tree | d7b1aa78355c39472804bdfbbd9db87722d0587b /packages/app/e2e/projects/workspaces.spec.ts | |
| parent | 363891126c9b2a909db0e724eb6377bd3d12b38b (diff) | |
| download | opencode-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.ts | 507 |
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() }) |
