Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion packages/app/e2e/app/titlebar-history.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test, expect } from "../fixtures"
import { openSidebar, withSession } from "../actions"
import { defocus, openSidebar, withSession } from "../actions"
import { promptSelector } from "../selectors"
import { modKey } from "../utils"

test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
await page.setViewportSize({ width: 1400, height: 800 })
Expand Down Expand Up @@ -40,3 +41,84 @@ test("titlebar back/forward navigates between sessions", async ({ page, slug, sd
})
})
})

test("titlebar forward is cleared after branching history from sidebar", async ({ page, slug, sdk, gotoSession }) => {
await page.setViewportSize({ width: 1400, height: 800 })

const stamp = Date.now()

await withSession(sdk, `e2e titlebar history a ${stamp}`, async (a) => {
await withSession(sdk, `e2e titlebar history b ${stamp}`, async (b) => {
await withSession(sdk, `e2e titlebar history c ${stamp}`, async (c) => {
await gotoSession(a.id)

await openSidebar(page)

const second = page.locator(`[data-session-id="${b.id}"] a`).first()
await expect(second).toBeVisible()
await second.scrollIntoViewIfNeeded()
await second.click()

await expect(page).toHaveURL(new RegExp(`/${slug}/session/${b.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()

const back = page.getByRole("button", { name: "Back" })
const forward = page.getByRole("button", { name: "Forward" })

await expect(back).toBeVisible()
await expect(back).toBeEnabled()
await back.click()

await expect(page).toHaveURL(new RegExp(`/${slug}/session/${a.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()

await openSidebar(page)

const third = page.locator(`[data-session-id="${c.id}"] a`).first()
await expect(third).toBeVisible()
await third.scrollIntoViewIfNeeded()
await third.click()

await expect(page).toHaveURL(new RegExp(`/${slug}/session/${c.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()

await expect(forward).toBeVisible()
await expect(forward).toBeDisabled()
})
})
})
})

test("keyboard shortcuts navigate titlebar history", async ({ page, slug, sdk, gotoSession }) => {
await page.setViewportSize({ width: 1400, height: 800 })

const stamp = Date.now()

await withSession(sdk, `e2e titlebar shortcuts 1 ${stamp}`, async (one) => {
await withSession(sdk, `e2e titlebar shortcuts 2 ${stamp}`, async (two) => {
await gotoSession(one.id)

await openSidebar(page)

const link = page.locator(`[data-session-id="${two.id}"] a`).first()
await expect(link).toBeVisible()
await link.scrollIntoViewIfNeeded()
await link.click()

await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()

await defocus(page)
await page.keyboard.press(`${modKey}+[`)

await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()

await defocus(page)
await page.keyboard.press(`${modKey}+]`)

await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
})
})
})
46 changes: 29 additions & 17 deletions packages/app/e2e/files/file-tree.spec.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
import { test, expect } from "../fixtures"

test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
await gotoSession()

const toggle = page.getByRole("button", { name: "Toggle file tree" })
const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
const panel = page.locator("#file-tree-panel")
const treeTabs = panel.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')

await expect(toggle).toBeVisible()
if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
await expect(toggle).toHaveAttribute("aria-expanded", "true")
await expect(panel).toBeVisible()
await expect(treeTabs).toBeVisible()

await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click()
const allTab = treeTabs.getByRole("tab", { name: /^all files$/i })
await expect(allTab).toBeVisible()
await allTab.click()
await expect(allTab).toHaveAttribute("aria-selected", "true")

const node = (name: string) => treeTabs.getByRole("button", { name, exact: true })
const tree = treeTabs.locator('[data-slot="tabs-content"]:not([hidden])')
await expect(tree).toBeVisible()

await expect(node("packages")).toBeVisible()
await node("packages").click()
const expand = async (name: string) => {
const folder = tree.getByRole("button", { name, exact: true }).first()
await expect(folder).toBeVisible()
await expect(folder).toHaveAttribute("aria-expanded", /true|false/)
if ((await folder.getAttribute("aria-expanded")) === "false") await folder.click()
await expect(folder).toHaveAttribute("aria-expanded", "true")
}

await expect(node("app")).toBeVisible()
await node("app").click()
await expand("packages")
await expand("app")
await expand("src")
await expand("components")

await expect(node("src")).toBeVisible()
await node("src").click()

await expect(node("components")).toBeVisible()
await node("components").click()

await expect(node("file-tree.tsx")).toBeVisible()
await node("file-tree.tsx").click()
const file = tree.getByRole("button", { name: "file-tree.tsx", exact: true }).first()
await expect(file).toBeVisible()
await file.click()

const tab = page.getByRole("tab", { name: "file-tree.tsx" })
await expect(tab).toBeVisible()
await tab.click()
await expect(tab).toHaveAttribute("aria-selected", "true")

const code = page.locator('[data-component="code"]').first()
await expect(code.getByText("export default function FileTree")).toBeVisible()
await expect(code).toBeVisible()
await expect(code).toContainText("export default function FileTree")
})
32 changes: 15 additions & 17 deletions packages/app/e2e/projects/projects-close.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from "../fixtures"
import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem } from "../actions"
import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors"
import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem, openProjectMenu } from "../actions"
import { projectCloseHoverSelector, projectSwitchSelector } from "../selectors"
import { dirSlug } from "../utils"

test("can close a project via hover card close button", async ({ page, withProject }) => {
Expand Down Expand Up @@ -31,16 +31,15 @@ test("can close a project via hover card close button", async ({ page, withProje
}
})

test("can close a project via project header more options menu", async ({ page, withProject }) => {
test("closing active project navigates to another open project", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })

const other = await createTestProject()
const otherName = other.split("/").pop() ?? other
const otherSlug = dirSlug(other)

try {
await withProject(
async () => {
async ({ slug }) => {
await openSidebar(page)

const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
Expand All @@ -49,21 +48,20 @@ test("can close a project via project header more options menu", async ({ page,

await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))

const header = page
.locator(".group\\/project")
.filter({ has: page.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`) })
.first()
await expect(header).toContainText(otherName)
const menu = await openProjectMenu(page, otherSlug)

const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first()
await expect(trigger).toHaveCount(1)
await trigger.focus()
await page.keyboard.press("Enter")
await clickMenuItem(menu, /^Close$/i, { force: true })

const menu = page.locator('[data-component="dropdown-menu-content"]').first()
await expect(menu).toBeVisible({ timeout: 10_000 })
await expect
.poll(() => {
const pathname = new URL(page.url()).pathname
if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
if (pathname === "/") return "home"
return ""
})
.toMatch(/^(project|home)$/)

await clickMenuItem(menu, /^Close$/i, { force: true })
await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`))
await expect(otherButton).toHaveCount(0)
},
{ extra: [other] },
Expand Down
44 changes: 43 additions & 1 deletion packages/app/e2e/projects/workspaces.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { base64Decode } from "@opencode-ai/util/encode"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import type { Page } from "@playwright/test"

Expand All @@ -10,11 +11,18 @@ import {
cleanupTestProject,
clickMenuItem,
confirmDialog,
openProjectMenu,
openSidebar,
openWorkspaceMenu,
setWorkspacesEnabled,
} from "../actions"
import { inlineInputSelector, workspaceItemSelector } from "../selectors"
import {
inlineInputSelector,
projectSwitchSelector,
projectWorkspacesToggleSelector,
workspaceItemSelector,
} from "../selectors"
import { dirSlug } from "../utils"

function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
Expand Down Expand Up @@ -126,6 +134,40 @@ test("can create a workspace", async ({ page, withProject }) => {
})
})

test("non-git projects keep workspace mode disabled", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })

const nonGit = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-nongit-"))
const nonGitSlug = dirSlug(nonGit)

await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")

try {
await withProject(
async () => {
await openSidebar(page)

const nonGitButton = page.locator(projectSwitchSelector(nonGitSlug)).first()
await expect(nonGitButton).toBeVisible()
await nonGitButton.click()
await expect(page).toHaveURL(new RegExp(`/${nonGitSlug}/session`))

const menu = await openProjectMenu(page, nonGitSlug)
const toggle = menu.locator(projectWorkspacesToggleSelector(nonGitSlug)).first()

await expect(toggle).toBeVisible()
await expect(toggle).toBeDisabled()

await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
},
{ extra: [nonGit] },
)
} finally {
await cleanupTestProject(nonGit)
}
})

test("can rename a workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })

Expand Down
Loading
Loading