summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-03-06 22:33:34 +0000
committerGitHub <[email protected]>2026-03-06 16:33:34 -0600
commitb0bc3d87f59fb28340fc4c047131919031890898 (patch)
tree35c56cbdb069fb720de642d7a4cc0bd6332e3747 /packages/app/e2e
parenta2634337b84643c08df5337243e8f82399c85615 (diff)
downloadopencode-b0bc3d87f59fb28340fc4c047131919031890898.tar.gz
opencode-b0bc3d87f59fb28340fc4c047131919031890898.zip
feat(app): sidebar reveal animation, hover peek overlay, and weaker dividers (#16374)
Co-authored-by: Adam <[email protected]>
Diffstat (limited to 'packages/app/e2e')
-rw-r--r--packages/app/e2e/actions.ts93
-rw-r--r--packages/app/e2e/app/titlebar-history.spec.ts4
-rw-r--r--packages/app/e2e/projects/project-edit.spec.ts18
-rw-r--r--packages/app/e2e/projects/projects-switch.spec.ts46
-rw-r--r--packages/app/e2e/projects/workspaces.spec.ts3
-rw-r--r--packages/app/e2e/settings/settings-keybinds.spec.ts15
-rw-r--r--packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts11
-rw-r--r--packages/app/e2e/sidebar/sidebar-session-links.spec.ts1
-rw-r--r--packages/app/e2e/sidebar/sidebar.spec.ts13
9 files changed, 110 insertions, 94 deletions
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index 919a1add8..5d244ba02 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -5,10 +5,10 @@ import path from "node:path"
import { execSync } from "node:child_process"
import { modKey, serverUrl } from "./utils"
import {
- sessionItemSelector,
dropdownMenuTriggerSelector,
dropdownMenuContentSelector,
projectMenuTriggerSelector,
+ projectCloseMenuSelector,
projectWorkspacesToggleSelector,
titlebarRightSelector,
popoverBodySelector,
@@ -61,9 +61,9 @@ export async function closeDialog(page: Page, dialog: Locator) {
}
export async function isSidebarClosed(page: Page) {
- const main = page.locator("main")
- const classes = (await main.getAttribute("class")) ?? ""
- return classes.includes("xl:border-l")
+ const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
+ await expect(button).toBeVisible()
+ return (await button.getAttribute("aria-expanded")) !== "true"
}
export async function toggleSidebar(page: Page) {
@@ -75,48 +75,34 @@ export async function openSidebar(page: Page) {
if (!(await isSidebarClosed(page))) return
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
- const visible = await button
- .isVisible()
- .then((x) => x)
- .catch(() => false)
-
- if (visible) await button.click()
- if (!visible) await toggleSidebar(page)
+ await button.click()
- const main = page.locator("main")
- const opened = await expect(main)
- .not.toHaveClass(/xl:border-l/, { timeout: 1500 })
+ const opened = await expect(button)
+ .toHaveAttribute("aria-expanded", "true", { timeout: 1500 })
.then(() => true)
.catch(() => false)
if (opened) return
await toggleSidebar(page)
- await expect(main).not.toHaveClass(/xl:border-l/)
+ await expect(button).toHaveAttribute("aria-expanded", "true")
}
export async function closeSidebar(page: Page) {
if (await isSidebarClosed(page)) return
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
- const visible = await button
- .isVisible()
- .then((x) => x)
- .catch(() => false)
-
- if (visible) await button.click()
- if (!visible) await toggleSidebar(page)
+ await button.click()
- const main = page.locator("main")
- const closed = await expect(main)
- .toHaveClass(/xl:border-l/, { timeout: 1500 })
+ const closed = await expect(button)
+ .toHaveAttribute("aria-expanded", "false", { timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closed) return
await toggleSidebar(page)
- await expect(main).toHaveClass(/xl:border-l/)
+ await expect(button).toHaveAttribute("aria-expanded", "false")
}
export async function openSettings(page: Page) {
@@ -220,7 +206,7 @@ export function sessionIDFromUrl(url: string) {
}
export async function hoverSessionItem(page: Page, sessionID: string) {
- const sessionEl = page.locator(sessionItemSelector(sessionID)).first()
+ const sessionEl = page.locator(`[data-session-id="${sessionID}"]`).last()
await expect(sessionEl).toBeVisible()
await sessionEl.hover()
return sessionEl
@@ -570,32 +556,42 @@ export async function openProjectMenu(page: Page, projectSlug: string) {
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
await expect(trigger).toHaveCount(1)
+ const menu = page
+ .locator(dropdownMenuContentSelector)
+ .filter({ has: page.locator(projectCloseMenuSelector(projectSlug)) })
+ .first()
+ const close = menu.locator(projectCloseMenuSelector(projectSlug)).first()
+
+ const clicked = await trigger
+ .click({ timeout: 1500 })
+ .then(() => true)
+ .catch(() => false)
+
+ if (clicked) {
+ const opened = await menu
+ .waitFor({ state: "visible", timeout: 1500 })
+ .then(() => true)
+ .catch(() => false)
+ if (opened) {
+ await expect(close).toBeVisible()
+ return menu
+ }
+ }
+
await trigger.focus()
await page.keyboard.press("Enter")
- const menu = page.locator(dropdownMenuContentSelector).first()
const opened = await menu
.waitFor({ state: "visible", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (opened) {
- const viewport = page.viewportSize()
- const x = viewport ? Math.max(viewport.width - 5, 0) : 1200
- const y = viewport ? Math.max(viewport.height - 5, 0) : 800
- await page.mouse.move(x, y)
+ await expect(close).toBeVisible()
return menu
}
- await trigger.click({ force: true })
-
- await expect(menu).toBeVisible()
-
- const viewport = page.viewportSize()
- const x = viewport ? Math.max(viewport.width - 5, 0) : 1200
- const y = viewport ? Math.max(viewport.height - 5, 0) : 800
- await page.mouse.move(x, y)
- return menu
+ throw new Error(`Failed to open project menu: ${projectSlug}`)
}
export async function setWorkspacesEnabled(page: Page, projectSlug: string, enabled: boolean) {
@@ -608,11 +604,18 @@ export async function setWorkspacesEnabled(page: Page, projectSlug: string, enab
if (current === enabled) return
- await openProjectMenu(page, projectSlug)
+ const flip = async (timeout?: number) => {
+ const menu = await openProjectMenu(page, projectSlug)
+ const toggle = menu.locator(projectWorkspacesToggleSelector(projectSlug)).first()
+ await expect(toggle).toBeVisible()
+ return toggle.click({ force: true, timeout })
+ }
+
+ const flipped = await flip(1500)
+ .then(() => true)
+ .catch(() => false)
- const toggle = page.locator(projectWorkspacesToggleSelector(projectSlug)).first()
- await expect(toggle).toBeVisible()
- await toggle.click({ force: true })
+ if (!flipped) await flip()
const expected = enabled ? "New workspace" : "New session"
await expect(page.getByRole("button", { name: expected }).first()).toBeVisible()
diff --git a/packages/app/e2e/app/titlebar-history.spec.ts b/packages/app/e2e/app/titlebar-history.spec.ts
index 9d6091176..a4592ff1d 100644
--- a/packages/app/e2e/app/titlebar-history.spec.ts
+++ b/packages/app/e2e/app/titlebar-history.spec.ts
@@ -16,7 +16,6 @@ test("titlebar back/forward navigates between sessions", async ({ page, slug, sd
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}(?:\\?|#|$)`))
@@ -56,7 +55,6 @@ test("titlebar forward is cleared after branching history from sidebar", async (
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}(?:\\?|#|$)`))
@@ -76,7 +74,6 @@ test("titlebar forward is cleared after branching history from sidebar", async (
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}(?:\\?|#|$)`))
@@ -102,7 +99,6 @@ test("keyboard shortcuts navigate titlebar history", async ({ page, slug, sdk, g
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}(?:\\?|#|$)`))
diff --git a/packages/app/e2e/projects/project-edit.spec.ts b/packages/app/e2e/projects/project-edit.spec.ts
index 4a286fea7..7c20f29ec 100644
--- a/packages/app/e2e/projects/project-edit.spec.ts
+++ b/packages/app/e2e/projects/project-edit.spec.ts
@@ -1,25 +1,15 @@
import { test, expect } from "../fixtures"
-import { openSidebar } from "../actions"
+import { clickMenuItem, openProjectMenu, openSidebar } from "../actions"
test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
- await withProject(async () => {
+ await withProject(async ({ slug }) => {
await openSidebar(page)
const open = async () => {
- const header = page.locator(".group\\/project").first()
- await header.hover()
- const trigger = header.getByRole("button", { name: "More options" }).first()
- await expect(trigger).toBeVisible()
- await trigger.click({ force: true })
-
- const menu = page.locator('[data-component="dropdown-menu-content"]').first()
- await expect(menu).toBeVisible()
-
- const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
- await expect(editItem).toBeVisible()
- await editItem.click({ force: true })
+ const menu = await openProjectMenu(page, slug)
+ await clickMenuItem(menu, /^Edit$/i, { force: true })
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts
index 81cca6988..2725100f4 100644
--- a/packages/app/e2e/projects/projects-switch.spec.ts
+++ b/packages/app/e2e/projects/projects-switch.spec.ts
@@ -1,13 +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,
- setWorkspacesEnabled,
- sessionIDFromUrl,
-} from "../actions"
+import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl } from "../actions"
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
import { createSdk, dirSlug, sessionPath } from "../utils"
@@ -15,6 +9,37 @@ function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
}
+async function workspaces(page: Page, directory: string, enabled: boolean) {
+ await page.evaluate(
+ ({ directory, enabled }: { directory: string; enabled: boolean }) => {
+ const key = "opencode.global.dat:layout"
+ const raw = localStorage.getItem(key)
+ const data = raw ? JSON.parse(raw) : {}
+ const sidebar = data.sidebar && typeof data.sidebar === "object" ? data.sidebar : {}
+ const current =
+ sidebar.workspaces && typeof sidebar.workspaces === "object" && !Array.isArray(sidebar.workspaces)
+ ? sidebar.workspaces
+ : {}
+ const next = { ...current }
+
+ if (enabled) next[directory] = true
+ if (!enabled) delete next[directory]
+
+ localStorage.setItem(
+ key,
+ JSON.stringify({
+ ...data,
+ sidebar: {
+ ...sidebar,
+ workspaces: next,
+ },
+ }),
+ )
+ },
+ { directory, enabled },
+ )
+}
+
test("can switch between projects from sidebar", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
@@ -60,8 +85,11 @@ test("switching back to a project opens the latest workspace session", async ({
async ({ directory, slug }) => {
rootDir = directory
await defocus(page)
+ await workspaces(page, directory, true)
+ await page.reload()
+ await expect(page.locator(promptSelector)).toBeVisible()
await openSidebar(page)
- await setWorkspacesEnabled(page, slug, true)
+ await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
await page.getByRole("button", { name: "New workspace" }).first().click()
diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts
index 386739526..41c6bea8f 100644
--- a/packages/app/e2e/projects/workspaces.spec.ts
+++ b/packages/app/e2e/projects/workspaces.spec.ts
@@ -336,9 +336,6 @@ test("can reorder workspaces by drag and drop", async ({ page, withProject }) =>
const src = page.locator(workspaceItemSelector(from)).first()
const dst = page.locator(workspaceItemSelector(to)).first()
- await src.scrollIntoViewIfNeeded()
- await dst.scrollIntoViewIfNeeded()
-
const a = await src.boundingBox()
const b = await dst.boundingBox()
if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts
index 5e98bd158..e0d590b31 100644
--- a/packages/app/e2e/settings/settings-keybinds.spec.ts
+++ b/packages/app/e2e/settings/settings-keybinds.spec.ts
@@ -32,22 +32,19 @@ test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => {
await closeDialog(page, dialog)
- const main = page.locator("main")
- const initialClasses = (await main.getAttribute("class")) ?? ""
- const initiallyClosed = initialClasses.includes("xl:border-l")
+ const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
+ const initiallyClosed = (await button.getAttribute("aria-expanded")) !== "true"
await page.keyboard.press(`${modKey}+Shift+H`)
- await page.waitForTimeout(100)
+ await expect(button).toHaveAttribute("aria-expanded", initiallyClosed ? "true" : "false")
- const afterToggleClasses = (await main.getAttribute("class")) ?? ""
- const afterToggleClosed = afterToggleClasses.includes("xl:border-l")
+ const afterToggleClosed = (await button.getAttribute("aria-expanded")) !== "true"
expect(afterToggleClosed).toBe(!initiallyClosed)
await page.keyboard.press(`${modKey}+Shift+H`)
- await page.waitForTimeout(100)
+ await expect(button).toHaveAttribute("aria-expanded", initiallyClosed ? "false" : "true")
- const finalClasses = (await main.getAttribute("class")) ?? ""
- const finalClosed = finalClasses.includes("xl:border-l")
+ const finalClosed = (await button.getAttribute("aria-expanded")) !== "true"
expect(finalClosed).toBe(initiallyClosed)
})
diff --git a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts
index e37f94f3a..09701f3f9 100644
--- a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts
+++ b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts
@@ -1,6 +1,6 @@
import { test, expect } from "../fixtures"
import { closeSidebar, hoverSessionItem } from "../actions"
-import { projectSwitchSelector, sessionItemSelector } from "../selectors"
+import { projectSwitchSelector } from "../selectors"
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
const stamp = Date.now()
@@ -15,12 +15,15 @@ test("collapsed sidebar popover stays open when archiving a session", async ({ p
await gotoSession(one.id)
await closeSidebar(page)
+ const oneItem = page.locator(`[data-session-id="${one.id}"]`).last()
+ const twoItem = page.locator(`[data-session-id="${two.id}"]`).last()
+
const project = page.locator(projectSwitchSelector(slug)).first()
await expect(project).toBeVisible()
await project.hover()
- await expect(page.locator(sessionItemSelector(one.id)).first()).toBeVisible()
- await expect(page.locator(sessionItemSelector(two.id)).first()).toBeVisible()
+ await expect(oneItem).toBeVisible()
+ await expect(twoItem).toBeVisible()
const item = await hoverSessionItem(page, one.id)
await item
@@ -28,7 +31,7 @@ test("collapsed sidebar popover stays open when archiving a session", async ({ p
.first()
.click()
- await expect(page.locator(sessionItemSelector(two.id)).first()).toBeVisible()
+ await expect(twoItem).toBeVisible()
} finally {
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
diff --git a/packages/app/e2e/sidebar/sidebar-session-links.spec.ts b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts
index cda2278a9..052b7cb84 100644
--- a/packages/app/e2e/sidebar/sidebar-session-links.spec.ts
+++ b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts
@@ -18,7 +18,6 @@ test("sidebar session links navigate to the selected session", async ({ page, sl
const target = page.locator(`[data-session-id="${two.id}"] a`).first()
await expect(target).toBeVisible()
- await target.scrollIntoViewIfNeeded()
await target.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
diff --git a/packages/app/e2e/sidebar/sidebar.spec.ts b/packages/app/e2e/sidebar/sidebar.spec.ts
index 5c78c2220..c6bf3fa9a 100644
--- a/packages/app/e2e/sidebar/sidebar.spec.ts
+++ b/packages/app/e2e/sidebar/sidebar.spec.ts
@@ -5,12 +5,14 @@ test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
await gotoSession()
await openSidebar(page)
+ const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
+ await expect(button).toHaveAttribute("aria-expanded", "true")
await toggleSidebar(page)
- await expect(page.locator("main")).toHaveClass(/xl:border-l/)
+ await expect(button).toHaveAttribute("aria-expanded", "false")
await toggleSidebar(page)
- await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
+ await expect(button).toHaveAttribute("aria-expanded", "true")
})
test("sidebar collapsed state persists across navigation and reload", async ({ page, sdk, gotoSession }) => {
@@ -19,14 +21,15 @@ test("sidebar collapsed state persists across navigation and reload", async ({ p
await gotoSession(session1.id)
await openSidebar(page)
+ const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
await toggleSidebar(page)
- await expect(page.locator("main")).toHaveClass(/xl:border-l/)
+ await expect(button).toHaveAttribute("aria-expanded", "false")
await gotoSession(session2.id)
- await expect(page.locator("main")).toHaveClass(/xl:border-l/)
+ await expect(button).toHaveAttribute("aria-expanded", "false")
await page.reload()
- await expect(page.locator("main")).toHaveClass(/xl:border-l/)
+ await expect(button).toHaveAttribute("aria-expanded", "false")
const opened = await page.evaluate(
() => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened,