diff --git a/packages/app/e2e/app/titlebar-history.spec.ts b/packages/app/e2e/app/titlebar-history.spec.ts index ec65dca0b35..9d6091176ec 100644 --- a/packages/app/e2e/app/titlebar-history.spec.ts +++ b/packages/app/e2e/app/titlebar-history.spec.ts @@ -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 }) @@ -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() + }) + }) +}) diff --git a/packages/app/e2e/files/file-tree.spec.ts b/packages/app/e2e/files/file-tree.spec.ts index 844da1b329b..321d96af57a 100644 --- a/packages/app/e2e/files/file-tree.spec.ts +++ b/packages/app/e2e/files/file-tree.spec.ts @@ -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") }) diff --git a/packages/app/e2e/projects/projects-close.spec.ts b/packages/app/e2e/projects/projects-close.spec.ts index 95768d21e9e..4b39ed82c37 100644 --- a/packages/app/e2e/projects/projects-close.spec.ts +++ b/packages/app/e2e/projects/projects-close.spec.ts @@ -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 }) => { @@ -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() @@ -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] }, diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts index 41a28e3e380..071c398b22d 100644 --- a/packages/app/e2e/projects/workspaces.spec.ts +++ b/packages/app/e2e/projects/workspaces.spec.ts @@ -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" @@ -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] ?? "" @@ -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 }) diff --git a/packages/app/e2e/session/session-undo-redo.spec.ts b/packages/app/e2e/session/session-undo-redo.spec.ts index c7af038c27a..2a250dd866a 100644 --- a/packages/app/e2e/session/session-undo-redo.spec.ts +++ b/packages/app/e2e/session/session-undo-redo.spec.ts @@ -23,10 +23,15 @@ async function seedConversation(input: { const messages = await input.sdk.session .messages({ sessionID: input.sessionID, limit: 50 }) .then((r) => r.data ?? []) - const users = messages.filter((m) => m.info.role === "user") + const users = messages.filter( + (m) => + m.info.role === "user" && + m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)), + ) if (users.length === 0) return false - const user = users.reduce((acc, item) => (item.info.id > acc.info.id ? item : acc)) + const user = users[users.length - 1] + if (!user) return false userMessageID = user.info.id const assistantText = messages @@ -124,3 +129,107 @@ test("slash redo clears revert and restores latest state", async ({ page, withPr }) }) }) + +test("slash undo/redo traverses multi-step revert stack", async ({ page, withProject }) => { + test.setTimeout(120_000) + + const firstToken = `undo_redo_first_${Date.now()}` + const secondToken = `undo_redo_second_${Date.now()}` + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + + await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => { + await project.gotoSession(session.id) + + const first = await seedConversation({ + page, + sdk, + sessionID: session.id, + token: firstToken, + }) + const second = await seedConversation({ + page, + sdk, + sessionID: session.id, + token: secondToken, + }) + + expect(first.userMessageID).not.toBe(second.userMessageID) + + const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`) + const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`) + + await expect(firstMessage.first()).toBeVisible() + await expect(secondMessage.first()).toBeVisible() + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/undo") + + const undo = page.locator('[data-slash-id="session.undo"]').first() + await expect(undo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBe(second.userMessageID) + + await expect(firstMessage.first()).toBeVisible() + await expect(secondMessage).toHaveCount(0) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/undo") + await expect(undo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBe(first.userMessageID) + + await expect(firstMessage).toHaveCount(0) + await expect(secondMessage).toHaveCount(0) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/redo") + + const redo = page.locator('[data-slash-id="session.redo"]').first() + await expect(redo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBe(second.userMessageID) + + await expect(firstMessage.first()).toBeVisible() + await expect(secondMessage).toHaveCount(0) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/redo") + await expect(redo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBeUndefined() + + await expect(firstMessage.first()).toBeVisible() + await expect(secondMessage.first()).toBeVisible() + }) + }) +}) diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts index a8e7f335266..5e98bd158a1 100644 --- a/packages/app/e2e/settings/settings-keybinds.spec.ts +++ b/packages/app/e2e/settings/settings-keybinds.spec.ts @@ -9,7 +9,7 @@ test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => { const dialog = await openSettings(page) await dialog.getByRole("tab", { name: "Shortcuts" }).click() - const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")) + const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")).first() await expect(keybindButton).toBeVisible() const initialKeybind = await keybindButton.textContent() @@ -51,6 +51,40 @@ test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => { expect(finalClosed).toBe(initiallyClosed) }) +test("sidebar toggle keybind guards against shortcut conflicts", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")) + await expect(keybindButton).toBeVisible() + + const initialKeybind = await keybindButton.textContent() + expect(initialKeybind).toContain("B") + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyP`) + await page.waitForTimeout(100) + + const toast = page.locator('[data-component="toast"]').last() + await expect(toast).toBeVisible() + await expect(toast).toContainText(/already/i) + + await keybindButton.click() + await expect(keybindButton).toContainText("B") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined() + + await closeDialog(page, dialog) +}) + test("resetting all keybinds to defaults works", async ({ page, gotoSession }) => { await page.addInitScript(() => { localStorage.setItem("settings.v3", JSON.stringify({ keybinds: { "sidebar.toggle": "mod+shift+x" } })) @@ -277,6 +311,44 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) => await expect(terminal).not.toBeVisible() }) +test("terminal toggle keybind persists after reload", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle")) + await expect(keybindButton).toBeVisible() + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyY`) + await page.waitForTimeout(100) + + await expect(keybindButton).toContainText("Y") + await closeDialog(page, dialog) + + await page.reload() + + await expect + .poll(async () => { + return await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + if (!raw) return + const parsed = JSON.parse(raw) + return parsed?.keybinds?.["terminal.toggle"] + }) + }) + .toBe("mod+shift+y") + + const reloaded = await openSettings(page) + await reloaded.getByRole("tab", { name: "Shortcuts" }).click() + const reloadedKeybind = reloaded.locator(keybindButtonSelector("terminal.toggle")).first() + await expect(reloadedKeybind).toContainText("Y") + await closeDialog(page, reloaded) +}) + test("changing command palette keybind works", async ({ page, gotoSession }) => { await gotoSession() diff --git a/packages/app/e2e/settings/settings.spec.ts b/packages/app/e2e/settings/settings.spec.ts index 2865419f0de..42534968b21 100644 --- a/packages/app/e2e/settings/settings.spec.ts +++ b/packages/app/e2e/settings/settings.spec.ts @@ -9,6 +9,8 @@ import { settingsNotificationsPermissionsSelector, settingsReleaseNotesSelector, settingsSoundsAgentSelector, + settingsSoundsErrorsSelector, + settingsSoundsPermissionsSelector, settingsThemeSelector, settingsUpdatesStartupSelector, } from "../selectors" @@ -139,6 +141,105 @@ test("changing font persists in localStorage and updates CSS variable", async ({ expect(newFontFamily).not.toBe(initialFontFamily) }) +test("color scheme and font rehydrate after reload", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + + const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector) + await expect(colorSchemeSelect).toBeVisible() + await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click() + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click() + await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark") + + const fontSelect = dialog.locator(settingsFontSelector) + await expect(fontSelect).toBeVisible() + + const initialFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + + const initialSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const currentFont = + (await fontSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await fontSelect.locator('[data-slot="select-select-trigger"]').click() + + const fontItems = page.locator('[data-slot="select-select-item"]') + expect(await fontItems.count()).toBeGreaterThan(1) + + if (currentFont) { + await fontItems.filter({ hasNotText: currentFont }).first().click() + } + if (!currentFont) { + await fontItems.nth(1).click() + } + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + appearance: { + font: expect.any(String), + }, + }) + + const updatedSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const updatedFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + expect(updatedFontFamily).not.toBe(initialFontFamily) + expect(updatedSettings?.appearance?.font).not.toBe(initialSettings?.appearance?.font) + + await closeDialog(page, dialog) + await page.reload() + + await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark") + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + appearance: { + font: updatedSettings?.appearance?.font, + }, + }) + + const rehydratedSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + await expect + .poll(async () => { + return await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + }) + .not.toBe(initialFontFamily) + + const rehydratedFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + expect(rehydratedFontFamily).not.toBe(initialFontFamily) + expect(rehydratedSettings?.appearance?.font).toBe(updatedSettings?.appearance?.font) +}) + test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => { await gotoSession() @@ -234,6 +335,67 @@ test("changing sound agent selection persists in localStorage", async ({ page, g expect(stored?.sounds?.agent).not.toBe("staplebops-01") }) +test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const permissionsSelect = dialog.locator(settingsSoundsPermissionsSelector) + const errorsSelect = dialog.locator(settingsSoundsErrorsSelector) + await expect(permissionsSelect).toBeVisible() + await expect(errorsSelect).toBeVisible() + + const initial = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const permissionsCurrent = + (await permissionsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await permissionsSelect.locator('[data-slot="select-select-trigger"]').click() + const permissionItems = page.locator('[data-slot="select-select-item"]') + expect(await permissionItems.count()).toBeGreaterThan(1) + if (permissionsCurrent) { + await permissionItems.filter({ hasNotText: permissionsCurrent }).first().click() + } + if (!permissionsCurrent) { + await permissionItems.nth(1).click() + } + + const errorsCurrent = + (await errorsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await errorsSelect.locator('[data-slot="select-select-trigger"]').click() + const errorItems = page.locator('[data-slot="select-select-item"]') + expect(await errorItems.count()).toBeGreaterThan(1) + if (errorsCurrent) { + await errorItems.filter({ hasNotText: errorsCurrent }).first().click() + } + if (!errorsCurrent) { + await errorItems.nth(1).click() + } + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + sounds: { + permissions: expect.any(String), + errors: expect.any(String), + }, + }) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.sounds?.permissions).not.toBe(initial?.sounds?.permissions) + expect(stored?.sounds?.errors).not.toBe(initial?.sounds?.errors) +}) + test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => { await gotoSession() diff --git a/packages/app/e2e/sidebar/sidebar.spec.ts b/packages/app/e2e/sidebar/sidebar.spec.ts index 6239a04bd79..5c78c2220d2 100644 --- a/packages/app/e2e/sidebar/sidebar.spec.ts +++ b/packages/app/e2e/sidebar/sidebar.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { openSidebar, toggleSidebar } from "../actions" +import { openSidebar, toggleSidebar, withSession } from "../actions" test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { await gotoSession() @@ -12,3 +12,26 @@ test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { await toggleSidebar(page) await expect(page.locator("main")).not.toHaveClass(/xl:border-l/) }) + +test("sidebar collapsed state persists across navigation and reload", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, "sidebar persist session 1", async (session1) => { + await withSession(sdk, "sidebar persist session 2", async (session2) => { + await gotoSession(session1.id) + + await openSidebar(page) + await toggleSidebar(page) + await expect(page.locator("main")).toHaveClass(/xl:border-l/) + + await gotoSession(session2.id) + await expect(page.locator("main")).toHaveClass(/xl:border-l/) + + await page.reload() + await expect(page.locator("main")).toHaveClass(/xl:border-l/) + + const opened = await page.evaluate( + () => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened, + ) + await expect(opened).toBe(false) + }) + }) +})