summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e
diff options
context:
space:
mode:
authorFilip <[email protected]>2026-01-31 21:01:21 +0100
committerGitHub <[email protected]>2026-01-31 14:01:21 -0600
commit33252a65b408ddec45bcd063c535c2170ce413c6 (patch)
tree46c340bf6f5b852ba49b0add37ef09b66af5abfe /packages/app/e2e
parente70d984320571597f89421d85c2f74009951027c (diff)
downloadopencode-33252a65b408ddec45bcd063c535c2170ce413c6.tar.gz
opencode-33252a65b408ddec45bcd063c535c2170ce413c6.zip
test(app): general settings, shortcuts, providers and status popover (#11517)
Diffstat (limited to 'packages/app/e2e')
-rw-r--r--packages/app/e2e/AGENTS.md176
-rw-r--r--packages/app/e2e/actions.ts22
-rw-r--r--packages/app/e2e/fixtures.ts3
-rw-r--r--packages/app/e2e/selectors.ts13
-rw-r--r--packages/app/e2e/settings/settings-keybinds.spec.ts317
-rw-r--r--packages/app/e2e/settings/settings-language.spec.ts28
-rw-r--r--packages/app/e2e/settings/settings-models.spec.ts122
-rw-r--r--packages/app/e2e/settings/settings-providers.spec.ts136
-rw-r--r--packages/app/e2e/settings/settings.spec.ts275
-rw-r--r--packages/app/e2e/status/status-popover.spec.ts94
10 files changed, 1142 insertions, 44 deletions
diff --git a/packages/app/e2e/AGENTS.md b/packages/app/e2e/AGENTS.md
new file mode 100644
index 000000000..59662dbea
--- /dev/null
+++ b/packages/app/e2e/AGENTS.md
@@ -0,0 +1,176 @@
+# E2E Testing Guide
+
+## Build/Lint/Test Commands
+
+```bash
+# Run all e2e tests
+bun test:e2e
+
+# Run specific test file
+bun test:e2e -- app/home.spec.ts
+
+# Run single test by title
+bun test:e2e -- -g "home renders and shows core entrypoints"
+
+# Run tests with UI mode (for debugging)
+bun test:e2e:ui
+
+# Run tests locally with full server setup
+bun test:e2e:local
+
+# View test report
+bun test:e2e:report
+
+# Typecheck
+bun typecheck
+```
+
+## Test Structure
+
+All tests live in `packages/app/e2e/`:
+
+```
+e2e/
+├── fixtures.ts # Test fixtures (test, expect, gotoSession, sdk)
+├── actions.ts # Reusable action helpers
+├── selectors.ts # DOM selectors
+├── utils.ts # Utilities (serverUrl, modKey, path helpers)
+└── [feature]/
+ └── *.spec.ts # Test files
+```
+
+## Test Patterns
+
+### Basic Test Structure
+
+```typescript
+import { test, expect } from "../fixtures"
+import { promptSelector } from "../selectors"
+import { withSession } from "../actions"
+
+test("test description", async ({ page, sdk, gotoSession }) => {
+ await gotoSession() // or gotoSession(sessionID)
+
+ // Your test code
+ await expect(page.locator(promptSelector)).toBeVisible()
+})
+```
+
+### Using Fixtures
+
+- `page` - Playwright page
+- `sdk` - OpenCode SDK client for API calls
+- `gotoSession(sessionID?)` - Navigate to session
+
+### Helper Functions
+
+**Actions** (`actions.ts`):
+
+- `openPalette(page)` - Open command palette
+- `openSettings(page)` - Open settings dialog
+- `closeDialog(page, dialog)` - Close any dialog
+- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
+- `withSession(sdk, title, callback)` - Create temp session
+- `clickListItem(container, filter)` - Click list item by key/text
+
+**Selectors** (`selectors.ts`):
+
+- `promptSelector` - Prompt input
+- `terminalSelector` - Terminal panel
+- `sessionItemSelector(id)` - Session in sidebar
+- `listItemSelector` - Generic list items
+
+**Utils** (`utils.ts`):
+
+- `modKey` - Meta (Mac) or Control (Linux/Win)
+- `serverUrl` - Backend server URL
+- `sessionPath(dir, id?)` - Build session URL
+
+## Code Style Guidelines
+
+### Imports
+
+Always import from `../fixtures`, not `@playwright/test`:
+
+```typescript
+// ✅ Good
+import { test, expect } from "../fixtures"
+
+// ❌ Bad
+import { test, expect } from "@playwright/test"
+```
+
+### Naming Conventions
+
+- Test files: `feature-name.spec.ts`
+- Test names: lowercase, descriptive: `"sidebar can be toggled"`
+- Variables: camelCase
+- Constants: SCREAMING_SNAKE_CASE
+
+### Error Handling
+
+Tests should clean up after themselves:
+
+```typescript
+test("test with cleanup", async ({ page, sdk, gotoSession }) => {
+ await withSession(sdk, "test session", async (session) => {
+ await gotoSession(session.id)
+ // Test code...
+ }) // Auto-deletes session
+})
+```
+
+### Timeouts
+
+Default: 60s per test, 10s per assertion. Override when needed:
+
+```typescript
+test.setTimeout(120_000) // For long LLM operations
+test("slow test", async () => {
+ await expect.poll(() => check(), { timeout: 90_000 }).toBe(true)
+})
+```
+
+### Selectors
+
+Use `data-component`, `data-action`, or semantic roles:
+
+```typescript
+// ✅ Good
+await page.locator('[data-component="prompt-input"]').click()
+await page.getByRole("button", { name: "Open settings" }).click()
+
+// ❌ Bad
+await page.locator(".css-class-name").click()
+await page.locator("#id-name").click()
+```
+
+### Keyboard Shortcuts
+
+Use `modKey` for cross-platform compatibility:
+
+```typescript
+import { modKey } from "../utils"
+
+await page.keyboard.press(`${modKey}+B`) // Toggle sidebar
+await page.keyboard.press(`${modKey}+Comma`) // Open settings
+```
+
+## Writing New Tests
+
+1. Choose appropriate folder or create new one
+2. Import from `../fixtures`
+3. Use helper functions from `../actions` and `../selectors`
+4. Clean up any created resources
+5. Use specific selectors (avoid CSS classes)
+6. Test one feature per test file
+
+## Local Development
+
+For UI debugging, use:
+
+```bash
+bun test:e2e:ui
+```
+
+This opens Playwright's interactive UI for step-through debugging.
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index 7308adc7e..1eb2da1db 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -269,3 +269,25 @@ export async function withSession<T>(
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
}
}
+
+export async function openStatusPopover(page: Page) {
+ await defocus(page)
+
+ const rightSection = page.locator(titlebarRightSelector)
+ const trigger = rightSection.getByRole("button", { name: /status/i }).first()
+
+ const popoverBody = page.locator(popoverBodySelector).filter({ has: page.locator('[data-component="tabs"]') })
+
+ const opened = await popoverBody
+ .isVisible()
+ .then((x) => x)
+ .catch(() => false)
+
+ if (!opened) {
+ await expect(trigger).toBeVisible()
+ await trigger.click()
+ await expect(popoverBody).toBeVisible()
+ }
+
+ return { rightSection, popoverBody }
+}
diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts
index 0c3150609..e28b4cef4 100644
--- a/packages/app/e2e/fixtures.ts
+++ b/packages/app/e2e/fixtures.ts
@@ -3,6 +3,9 @@ import { seedProjects } from "./actions"
import { promptSelector } from "./selectors"
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
+
+export const settingsKey = "settings.v3"
+
type TestFixtures = {
sdk: ReturnType<typeof createSdk>
gotoSession: (sessionID?: string) => Promise<void>
diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts
index 8beade573..90cfef8db 100644
--- a/packages/app/e2e/selectors.ts
+++ b/packages/app/e2e/selectors.ts
@@ -3,6 +3,17 @@ export const terminalSelector = '[data-component="terminal"]'
export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
+export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
+export const settingsThemeSelector = '[data-action="settings-theme"]'
+export const settingsFontSelector = '[data-action="settings-font"]'
+export const settingsNotificationsAgentSelector = '[data-action="settings-notifications-agent"]'
+export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
+export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
+export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]'
+export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]'
+export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]'
+export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]'
+export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]'
export const sidebarNavSelector = '[data-component="sidebar-nav-desktop"]'
@@ -33,3 +44,5 @@ export const listItemSelector = '[data-slot="list-item"]'
export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]`
export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]`
+
+export const keybindButtonSelector = (id: string) => `[data-keybind-id="${id}"]`
diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts
new file mode 100644
index 000000000..eceb82b74
--- /dev/null
+++ b/packages/app/e2e/settings/settings-keybinds.spec.ts
@@ -0,0 +1,317 @@
+import { test, expect } from "../fixtures"
+import { openSettings, closeDialog, withSession } from "../actions"
+import { keybindButtonSelector } from "../selectors"
+import { modKey } from "../utils"
+
+test("changing sidebar toggle keybind works", 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+KeyH`)
+ await page.waitForTimeout(100)
+
+ const newKeybind = await keybindButton.textContent()
+ expect(newKeybind).toContain("H")
+
+ const stored = await page.evaluate(() => {
+ const raw = localStorage.getItem("settings.v3")
+ return raw ? JSON.parse(raw) : null
+ })
+ expect(stored?.keybinds?.["sidebar.toggle"]).toBe("mod+shift+h")
+
+ await closeDialog(page, dialog)
+
+ const main = page.locator("main")
+ const initialClasses = (await main.getAttribute("class")) ?? ""
+ const initiallyClosed = initialClasses.includes("xl:border-l")
+
+ await page.keyboard.press(`${modKey}+Shift+H`)
+ await page.waitForTimeout(100)
+
+ const afterToggleClasses = (await main.getAttribute("class")) ?? ""
+ const afterToggleClosed = afterToggleClasses.includes("xl:border-l")
+ expect(afterToggleClosed).toBe(!initiallyClosed)
+
+ await page.keyboard.press(`${modKey}+Shift+H`)
+ await page.waitForTimeout(100)
+
+ const finalClasses = (await main.getAttribute("class")) ?? ""
+ const finalClosed = finalClasses.includes("xl:border-l")
+ expect(finalClosed).toBe(initiallyClosed)
+})
+
+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" } }))
+ })
+
+ 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 customKeybind = await keybindButton.textContent()
+ expect(customKeybind).toContain("X")
+
+ const resetButton = dialog.getByRole("button", { name: "Reset to defaults" })
+ await expect(resetButton).toBeVisible()
+ await expect(resetButton).toBeEnabled()
+ await resetButton.click()
+ await page.waitForTimeout(100)
+
+ const restoredKeybind = await keybindButton.textContent()
+ expect(restoredKeybind).toContain("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("clearing a keybind works", 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("Delete")
+ await page.waitForTimeout(100)
+
+ const clearedKeybind = await keybindButton.textContent()
+ expect(clearedKeybind).toMatch(/unassigned|press/i)
+
+ const stored = await page.evaluate(() => {
+ const raw = localStorage.getItem("settings.v3")
+ return raw ? JSON.parse(raw) : null
+ })
+ expect(stored?.keybinds?.["sidebar.toggle"]).toBe("none")
+
+ await closeDialog(page, dialog)
+
+ await page.keyboard.press(`${modKey}+B`)
+ await page.waitForTimeout(100)
+
+ const stillOnSession = page.url().includes("/session")
+ expect(stillOnSession).toBe(true)
+})
+
+test("changing settings open keybind works", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ await dialog.getByRole("tab", { name: "Shortcuts" }).click()
+
+ const keybindButton = dialog.locator(keybindButtonSelector("settings.open"))
+ await expect(keybindButton).toBeVisible()
+
+ const initialKeybind = await keybindButton.textContent()
+ expect(initialKeybind).toContain(",")
+
+ await keybindButton.click()
+ await expect(keybindButton).toHaveText(/press/i)
+
+ await page.keyboard.press(`${modKey}+Slash`)
+ await page.waitForTimeout(100)
+
+ const newKeybind = await keybindButton.textContent()
+ expect(newKeybind).toContain("/")
+
+ const stored = await page.evaluate(() => {
+ const raw = localStorage.getItem("settings.v3")
+ return raw ? JSON.parse(raw) : null
+ })
+ expect(stored?.keybinds?.["settings.open"]).toBe("mod+/")
+
+ await closeDialog(page, dialog)
+
+ const settingsDialog = page.getByRole("dialog")
+ await expect(settingsDialog).toHaveCount(0)
+
+ await page.keyboard.press(`${modKey}+Slash`)
+ await page.waitForTimeout(100)
+
+ await expect(settingsDialog).toBeVisible()
+
+ await closeDialog(page, settingsDialog)
+})
+
+test("changing new session keybind works", async ({ page, sdk, gotoSession }) => {
+ await withSession(sdk, "test session for keybind", async (session) => {
+ await gotoSession(session.id)
+
+ const initialUrl = page.url()
+ expect(initialUrl).toContain(`/session/${session.id}`)
+
+ const dialog = await openSettings(page)
+ await dialog.getByRole("tab", { name: "Shortcuts" }).click()
+
+ const keybindButton = dialog.locator(keybindButtonSelector("session.new"))
+ await expect(keybindButton).toBeVisible()
+
+ await keybindButton.click()
+ await expect(keybindButton).toHaveText(/press/i)
+
+ await page.keyboard.press(`${modKey}+Shift+KeyN`)
+ await page.waitForTimeout(100)
+
+ const newKeybind = await keybindButton.textContent()
+ expect(newKeybind).toContain("N")
+
+ const stored = await page.evaluate(() => {
+ const raw = localStorage.getItem("settings.v3")
+ return raw ? JSON.parse(raw) : null
+ })
+ expect(stored?.keybinds?.["session.new"]).toBe("mod+shift+n")
+
+ await closeDialog(page, dialog)
+
+ await page.keyboard.press(`${modKey}+Shift+N`)
+ await page.waitForTimeout(200)
+
+ const newUrl = page.url()
+ expect(newUrl).toMatch(/\/session\/?$/)
+ expect(newUrl).not.toContain(session.id)
+ })
+})
+
+test("changing file open keybind works", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ await dialog.getByRole("tab", { name: "Shortcuts" }).click()
+
+ const keybindButton = dialog.locator(keybindButtonSelector("file.open"))
+ await expect(keybindButton).toBeVisible()
+
+ const initialKeybind = await keybindButton.textContent()
+ expect(initialKeybind).toContain("P")
+
+ await keybindButton.click()
+ await expect(keybindButton).toHaveText(/press/i)
+
+ await page.keyboard.press(`${modKey}+Shift+KeyF`)
+ await page.waitForTimeout(100)
+
+ const newKeybind = await keybindButton.textContent()
+ expect(newKeybind).toContain("F")
+
+ const stored = await page.evaluate(() => {
+ const raw = localStorage.getItem("settings.v3")
+ return raw ? JSON.parse(raw) : null
+ })
+ expect(stored?.keybinds?.["file.open"]).toBe("mod+shift+f")
+
+ await closeDialog(page, dialog)
+
+ const filePickerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder(/search files/i) })
+ await expect(filePickerDialog).toHaveCount(0)
+
+ await page.keyboard.press(`${modKey}+Shift+F`)
+ await page.waitForTimeout(100)
+
+ await expect(filePickerDialog).toBeVisible()
+
+ await page.keyboard.press("Escape")
+ await expect(filePickerDialog).toHaveCount(0)
+})
+
+test("changing terminal toggle keybind works", 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}+KeyY`)
+ await page.waitForTimeout(100)
+
+ const newKeybind = await keybindButton.textContent()
+ expect(newKeybind).toContain("Y")
+
+ const stored = await page.evaluate(() => {
+ const raw = localStorage.getItem("settings.v3")
+ return raw ? JSON.parse(raw) : null
+ })
+ expect(stored?.keybinds?.["terminal.toggle"]).toBe("mod+y")
+
+ await closeDialog(page, dialog)
+
+ await page.keyboard.press(`${modKey}+Y`)
+ await page.waitForTimeout(100)
+
+ const pageStable = await page.evaluate(() => document.readyState === "complete")
+ expect(pageStable).toBe(true)
+})
+
+test("changing command palette keybind works", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ await dialog.getByRole("tab", { name: "Shortcuts" }).click()
+
+ const keybindButton = dialog.locator(keybindButtonSelector("command.palette"))
+ await expect(keybindButton).toBeVisible()
+
+ const initialKeybind = await keybindButton.textContent()
+ expect(initialKeybind).toContain("P")
+
+ await keybindButton.click()
+ await expect(keybindButton).toHaveText(/press/i)
+
+ await page.keyboard.press(`${modKey}+Shift+KeyK`)
+ await page.waitForTimeout(100)
+
+ const newKeybind = await keybindButton.textContent()
+ expect(newKeybind).toContain("K")
+
+ const stored = await page.evaluate(() => {
+ const raw = localStorage.getItem("settings.v3")
+ return raw ? JSON.parse(raw) : null
+ })
+ expect(stored?.keybinds?.["command.palette"]).toBe("mod+shift+k")
+
+ await closeDialog(page, dialog)
+
+ const palette = page.getByRole("dialog").filter({ has: page.getByRole("textbox").first() })
+ await expect(palette).toHaveCount(0)
+
+ await page.keyboard.press(`${modKey}+Shift+K`)
+ await page.waitForTimeout(100)
+
+ await expect(palette).toBeVisible()
+ await expect(palette.getByRole("textbox").first()).toBeVisible()
+
+ await page.keyboard.press("Escape")
+ await expect(palette).toHaveCount(0)
+})
diff --git a/packages/app/e2e/settings/settings-language.spec.ts b/packages/app/e2e/settings/settings-language.spec.ts
deleted file mode 100644
index b326a7d81..000000000
--- a/packages/app/e2e/settings/settings-language.spec.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { test, expect } from "../fixtures"
-import { settingsLanguageSelectSelector } from "../selectors"
-import { openSettings } from "../actions"
-
-test("smoke changing language updates settings labels", async ({ page, gotoSession }) => {
- await page.addInitScript(() => {
- localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
- })
-
- await gotoSession()
-
- const dialog = await openSettings(page)
-
- const heading = dialog.getByRole("heading", { level: 2 })
- await expect(heading).toHaveText("General")
-
- const select = dialog.locator(settingsLanguageSelectSelector)
- await expect(select).toBeVisible()
- await select.locator('[data-slot="select-select-trigger"]').click()
-
- await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
-
- await expect(heading).toHaveText("Allgemein")
-
- await select.locator('[data-slot="select-select-trigger"]').click()
- await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
- await expect(heading).toHaveText("General")
-})
diff --git a/packages/app/e2e/settings/settings-models.spec.ts b/packages/app/e2e/settings/settings-models.spec.ts
new file mode 100644
index 000000000..f7397abe8
--- /dev/null
+++ b/packages/app/e2e/settings/settings-models.spec.ts
@@ -0,0 +1,122 @@
+import { test, expect } from "../fixtures"
+import { promptSelector } from "../selectors"
+import { closeDialog, openSettings } from "../actions"
+
+test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ await page.locator(promptSelector).click()
+ await page.keyboard.type("/model")
+
+ const command = page.locator('[data-slash-id="model.choose"]')
+ await expect(command).toBeVisible()
+ await command.hover()
+ await page.keyboard.press("Enter")
+
+ const picker = page.getByRole("dialog")
+ await expect(picker).toBeVisible()
+
+ const target = picker.locator('[data-slot="list-item"]').first()
+ await expect(target).toBeVisible()
+
+ const key = await target.getAttribute("data-key")
+ if (!key) throw new Error("Failed to resolve model key from list item")
+
+ const name = (await target.locator("span").first().innerText()).trim()
+ if (!name) throw new Error("Failed to resolve model name from list item")
+
+ await page.keyboard.press("Escape")
+ await expect(picker).toHaveCount(0)
+
+ const settings = await openSettings(page)
+
+ await settings.getByRole("tab", { name: "Models" }).click()
+ const search = settings.getByPlaceholder("Search models")
+ await expect(search).toBeVisible()
+ await search.fill(name)
+
+ const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
+ const input = toggle.locator('[data-slot="switch-input"]')
+ await expect(toggle).toBeVisible()
+ await expect(input).toHaveAttribute("aria-checked", "true")
+ await toggle.locator('[data-slot="switch-control"]').click()
+ await expect(input).toHaveAttribute("aria-checked", "false")
+
+ await closeDialog(page, settings)
+
+ await page.locator(promptSelector).click()
+ await page.keyboard.type("/model")
+ await expect(command).toBeVisible()
+ await command.hover()
+ await page.keyboard.press("Enter")
+
+ const pickerAgain = page.getByRole("dialog")
+ await expect(pickerAgain).toBeVisible()
+ await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
+
+ await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
+
+ await page.keyboard.press("Escape")
+ await expect(pickerAgain).toHaveCount(0)
+})
+
+test("showing a hidden model restores it to the model picker", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ await page.locator(promptSelector).click()
+ await page.keyboard.type("/model")
+
+ const command = page.locator('[data-slash-id="model.choose"]')
+ await expect(command).toBeVisible()
+ await command.hover()
+ await page.keyboard.press("Enter")
+
+ const picker = page.getByRole("dialog")
+ await expect(picker).toBeVisible()
+
+ const target = picker.locator('[data-slot="list-item"]').first()
+ await expect(target).toBeVisible()
+
+ const key = await target.getAttribute("data-key")
+ if (!key) throw new Error("Failed to resolve model key from list item")
+
+ const name = (await target.locator("span").first().innerText()).trim()
+ if (!name) throw new Error("Failed to resolve model name from list item")
+
+ await page.keyboard.press("Escape")
+ await expect(picker).toHaveCount(0)
+
+ const settings = await openSettings(page)
+
+ await settings.getByRole("tab", { name: "Models" }).click()
+ const search = settings.getByPlaceholder("Search models")
+ await expect(search).toBeVisible()
+ await search.fill(name)
+
+ const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
+ const input = toggle.locator('[data-slot="switch-input"]')
+ await expect(toggle).toBeVisible()
+ await expect(input).toHaveAttribute("aria-checked", "true")
+
+ await toggle.locator('[data-slot="switch-control"]').click()
+ await expect(input).toHaveAttribute("aria-checked", "false")
+
+ await toggle.locator('[data-slot="switch-control"]').click()
+ await expect(input).toHaveAttribute("aria-checked", "true")
+
+ await closeDialog(page, settings)
+
+ await page.locator(promptSelector).click()
+ await page.keyboard.type("/model")
+ await expect(command).toBeVisible()
+ await command.hover()
+ await page.keyboard.press("Enter")
+
+ const pickerAgain = page.getByRole("dialog")
+ await expect(pickerAgain).toBeVisible()
+
+ await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toBeVisible()
+
+ await page.keyboard.press("Escape")
+ await expect(pickerAgain).toHaveCount(0)
+})
diff --git a/packages/app/e2e/settings/settings-providers.spec.ts b/packages/app/e2e/settings/settings-providers.spec.ts
index 2bd2616bc..a55eb3498 100644
--- a/packages/app/e2e/settings/settings-providers.spec.ts
+++ b/packages/app/e2e/settings/settings-providers.spec.ts
@@ -1,30 +1,136 @@
import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { closeDialog, openSettings, clickListItem } from "../actions"
+import { closeDialog, openSettings } from "../actions"
-test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => {
+test("custom provider form can be filled and validates input", async ({ page, gotoSession }) => {
await gotoSession()
- const dialog = await openSettings(page)
+ const settings = await openSettings(page)
+ await settings.getByRole("tab", { name: "Providers" }).click()
- await dialog.getByRole("tab", { name: "Providers" }).click()
- await expect(dialog.getByText("Connected providers", { exact: true })).toBeVisible()
- await expect(dialog.getByText("Popular providers", { exact: true })).toBeVisible()
+ const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
+ await expect(customProviderSection).toBeVisible()
- await dialog.getByRole("button", { name: "Show more providers" }).click()
+ const connectButton = customProviderSection.getByRole("button", { name: "Connect" })
+ await connectButton.click()
- const providerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder("Search providers") })
+ const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
+ await expect(providerDialog).toBeVisible()
+
+ await providerDialog.getByLabel("Provider ID").fill("test-provider")
+ await providerDialog.getByLabel("Display name").fill("Test Provider")
+ await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/fake")
+ await providerDialog.getByLabel("API key").fill("fake-key")
+
+ await providerDialog.getByPlaceholder("model-id").first().fill("test-model")
+ await providerDialog.getByPlaceholder("Display Name").first().fill("Test Model")
+
+ await expect(providerDialog.getByRole("textbox", { name: "Provider ID" })).toHaveValue("test-provider")
+ await expect(providerDialog.getByRole("textbox", { name: "Display name" })).toHaveValue("Test Provider")
+ await expect(providerDialog.getByRole("textbox", { name: "Base URL" })).toHaveValue("http://localhost:9999/fake")
+ await expect(providerDialog.getByRole("textbox", { name: "API key" })).toHaveValue("fake-key")
+ await expect(providerDialog.getByPlaceholder("model-id").first()).toHaveValue("test-model")
+ await expect(providerDialog.getByPlaceholder("Display Name").first()).toHaveValue("Test Model")
+
+ await page.keyboard.press("Escape")
+ await expect(providerDialog).toHaveCount(0)
+
+ await closeDialog(page, settings)
+})
+
+test("custom provider form shows validation errors", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const settings = await openSettings(page)
+ await settings.getByRole("tab", { name: "Providers" }).click()
+ const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
+ await customProviderSection.getByRole("button", { name: "Connect" }).click()
+
+ const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
await expect(providerDialog).toBeVisible()
- await expect(providerDialog.getByPlaceholder("Search providers")).toBeVisible()
- await expect(providerDialog.locator('[data-slot="list-item"]').first()).toBeVisible()
+
+ await providerDialog.getByLabel("Provider ID").fill("invalid provider id")
+ await providerDialog.getByLabel("Base URL").fill("not-a-url")
+
+ await providerDialog.getByRole("button", { name: /submit|save/i }).click()
+
+ await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /lowercase/i })).toBeVisible()
+ await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /http/i })).toBeVisible()
await page.keyboard.press("Escape")
await expect(providerDialog).toHaveCount(0)
- await expect(page.locator(promptSelector)).toBeVisible()
- const stillOpen = await dialog.isVisible().catch(() => false)
- if (!stillOpen) return
+ await closeDialog(page, settings)
+})
+
+test("custom provider form can add and remove models", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const settings = await openSettings(page)
+ await settings.getByRole("tab", { name: "Providers" }).click()
+
+ const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
+ await customProviderSection.getByRole("button", { name: "Connect" }).click()
+
+ const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
+ await expect(providerDialog).toBeVisible()
+
+ await providerDialog.getByLabel("Provider ID").fill("multi-model-test")
+ await providerDialog.getByLabel("Display name").fill("Multi Model Test")
+ await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/multi")
+
+ await providerDialog.getByPlaceholder("model-id").first().fill("model-1")
+ await providerDialog.getByPlaceholder("Display Name").first().fill("Model 1")
+
+ const idInputsBefore = await providerDialog.getByPlaceholder("model-id").count()
+ await providerDialog.getByRole("button", { name: "Add model" }).click()
+ const idInputsAfter = await providerDialog.getByPlaceholder("model-id").count()
+ expect(idInputsAfter).toBe(idInputsBefore + 1)
+
+ await providerDialog.getByPlaceholder("model-id").nth(1).fill("model-2")
+ await providerDialog.getByPlaceholder("Display Name").nth(1).fill("Model 2")
+
+ await expect(providerDialog.getByPlaceholder("model-id").nth(1)).toHaveValue("model-2")
+ await expect(providerDialog.getByPlaceholder("Display Name").nth(1)).toHaveValue("Model 2")
+
+ await page.keyboard.press("Escape")
+ await expect(providerDialog).toHaveCount(0)
+
+ await closeDialog(page, settings)
+})
+
+test("custom provider form can add and remove headers", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const settings = await openSettings(page)
+ await settings.getByRole("tab", { name: "Providers" }).click()
+
+ const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
+ await customProviderSection.getByRole("button", { name: "Connect" }).click()
+
+ const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
+ await expect(providerDialog).toBeVisible()
+
+ await providerDialog.getByLabel("Provider ID").fill("header-test")
+ await providerDialog.getByLabel("Display name").fill("Header Test")
+ await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/headers")
+
+ await providerDialog.getByPlaceholder("model-id").first().fill("model-x")
+ await providerDialog.getByPlaceholder("Display Name").first().fill("Model X")
+
+ const headerInputsBefore = await providerDialog.getByPlaceholder("Header-Name").count()
+ await providerDialog.getByRole("button", { name: "Add header" }).click()
+ const headerInputsAfter = await providerDialog.getByPlaceholder("Header-Name").count()
+ expect(headerInputsAfter).toBe(headerInputsBefore + 1)
+
+ await providerDialog.getByPlaceholder("Header-Name").first().fill("Authorization")
+ await providerDialog.getByPlaceholder("value").first().fill("Bearer token123")
+
+ await expect(providerDialog.getByPlaceholder("Header-Name").first()).toHaveValue("Authorization")
+ await expect(providerDialog.getByPlaceholder("value").first()).toHaveValue("Bearer token123")
+
+ await page.keyboard.press("Escape")
+ await expect(providerDialog).toHaveCount(0)
- await closeDialog(page, dialog)
+ await closeDialog(page, settings)
})
diff --git a/packages/app/e2e/settings/settings.spec.ts b/packages/app/e2e/settings/settings.spec.ts
index 55b767076..d9cd449b4 100644
--- a/packages/app/e2e/settings/settings.spec.ts
+++ b/packages/app/e2e/settings/settings.spec.ts
@@ -1,5 +1,6 @@
-import { test, expect } from "../fixtures"
+import { test, expect, settingsKey } from "../fixtures"
import { closeDialog, openSettings } from "../actions"
+import { settingsColorSchemeSelector, settingsFontSelector, settingsLanguageSelectSelector, settingsNotificationsAgentSelector, settingsNotificationsErrorsSelector, settingsNotificationsPermissionsSelector, settingsReleaseNotesSelector, settingsSoundsAgentSelector, settingsThemeSelector, settingsUpdatesStartupSelector } from "../selectors"
test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
await gotoSession()
@@ -12,3 +13,275 @@ test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSe
await closeDialog(page, dialog)
})
+
+
+test("changing language updates settings labels", async ({ page, gotoSession }) => {
+ await page.addInitScript(() => {
+ localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
+ })
+
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+
+ const heading = dialog.getByRole("heading", { level: 2 })
+ await expect(heading).toHaveText("General")
+
+ const select = dialog.locator(settingsLanguageSelectSelector)
+ await expect(select).toBeVisible()
+ await select.locator('[data-slot="select-select-trigger"]').click()
+
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
+
+ await expect(heading).toHaveText("Allgemein")
+
+ await select.locator('[data-slot="select-select-trigger"]').click()
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
+ await expect(heading).toHaveText("General")
+})
+
+test("changing color scheme persists in localStorage", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const select = dialog.locator(settingsColorSchemeSelector)
+ await expect(select).toBeVisible()
+
+ await select.locator('[data-slot="select-select-trigger"]').click()
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
+
+ const colorScheme = await page.evaluate(() => {
+ return document.documentElement.getAttribute("data-color-scheme")
+ })
+ expect(colorScheme).toBe("dark")
+
+ await select.locator('[data-slot="select-select-trigger"]').click()
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Light" }).click()
+
+ const lightColorScheme = await page.evaluate(() => {
+ return document.documentElement.getAttribute("data-color-scheme")
+ })
+ expect(lightColorScheme).toBe("light")
+})
+
+test("changing theme persists in localStorage", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const select = dialog.locator(settingsThemeSelector)
+ await expect(select).toBeVisible()
+
+ await select.locator('[data-slot="select-select-trigger"]').click()
+
+ const items = page.locator('[data-slot="select-select-item"]')
+ const count = await items.count()
+ expect(count).toBeGreaterThan(1)
+
+ const firstTheme = await items.nth(1).locator('[data-slot="select-select-item-label"]').textContent()
+ expect(firstTheme).toBeTruthy()
+
+ await items.nth(1).click()
+
+ await page.keyboard.press("Escape")
+
+ const storedThemeId = await page.evaluate(() => {
+ return localStorage.getItem("opencode-theme-id")
+ })
+
+ expect(storedThemeId).not.toBeNull()
+ expect(storedThemeId).not.toBe("oc-1")
+
+ const dataTheme = await page.evaluate(() => {
+ return document.documentElement.getAttribute("data-theme")
+ })
+ expect(dataTheme).toBe(storedThemeId)
+})
+
+test("changing font persists in localStorage and updates CSS variable", async ({ page, gotoSession }) => {
+
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const select = dialog.locator(settingsFontSelector)
+ await expect(select).toBeVisible()
+
+ const initialFontFamily = await page.evaluate(() => {
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
+ })
+ expect(initialFontFamily).toContain("IBM Plex Mono")
+
+ await select.locator('[data-slot="select-select-trigger"]').click()
+
+ const items = page.locator('[data-slot="select-select-item"]')
+ await items.nth(2).click()
+
+ await page.waitForTimeout(100)
+
+ const stored = await page.evaluate((key) => {
+ const raw = localStorage.getItem(key)
+ return raw ? JSON.parse(raw) : null
+ }, settingsKey)
+
+ expect(stored?.appearance?.font).not.toBe("ibm-plex-mono")
+
+ const newFontFamily = await page.evaluate(() => {
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
+ })
+ expect(newFontFamily).not.toBe(initialFontFamily)
+})
+
+test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const switchContainer = dialog.locator(settingsNotificationsAgentSelector)
+ await expect(switchContainer).toBeVisible()
+
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(initialState).toBe(true)
+
+ await switchContainer.locator('[data-slot="switch-control"]').click()
+ await page.waitForTimeout(100)
+
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(newState).toBe(false)
+
+ const stored = await page.evaluate((key) => {
+ const raw = localStorage.getItem(key)
+ return raw ? JSON.parse(raw) : null
+ }, settingsKey)
+
+ expect(stored?.notifications?.agent).toBe(false)
+})
+
+test("toggling notification permissions switch updates localStorage", async ({ page, gotoSession }) => {
+
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const switchContainer = dialog.locator(settingsNotificationsPermissionsSelector)
+ await expect(switchContainer).toBeVisible()
+
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(initialState).toBe(true)
+
+ await switchContainer.locator('[data-slot="switch-control"]').click()
+ await page.waitForTimeout(100)
+
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(newState).toBe(false)
+
+ const stored = await page.evaluate((key) => {
+ const raw = localStorage.getItem(key)
+ return raw ? JSON.parse(raw) : null
+ }, settingsKey)
+
+ expect(stored?.notifications?.permissions).toBe(false)
+})
+
+test("toggling notification errors switch updates localStorage", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const switchContainer = dialog.locator(settingsNotificationsErrorsSelector)
+ await expect(switchContainer).toBeVisible()
+
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(initialState).toBe(false)
+
+ await switchContainer.locator('[data-slot="switch-control"]').click()
+ await page.waitForTimeout(100)
+
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(newState).toBe(true)
+
+ const stored = await page.evaluate((key) => {
+ const raw = localStorage.getItem(key)
+ return raw ? JSON.parse(raw) : null
+ }, settingsKey)
+
+ expect(stored?.notifications?.errors).toBe(true)
+})
+
+test("changing sound agent selection persists in localStorage", async ({ page, gotoSession }) => {
+
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const select = dialog.locator(settingsSoundsAgentSelector)
+ await expect(select).toBeVisible()
+
+ await select.locator('[data-slot="select-select-trigger"]').click()
+
+ const items = page.locator('[data-slot="select-select-item"]')
+ await items.nth(2).click()
+
+ const stored = await page.evaluate((key) => {
+ const raw = localStorage.getItem(key)
+ return raw ? JSON.parse(raw) : null
+ }, settingsKey)
+
+ expect(stored?.sounds?.agent).not.toBe("staplebops-01")
+})
+
+test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => {
+
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const switchContainer = dialog.locator(settingsUpdatesStartupSelector)
+ await expect(switchContainer).toBeVisible()
+
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
+
+ const isDisabled = await toggleInput.evaluate((el: HTMLInputElement) => el.disabled)
+ if (isDisabled) {
+ test.skip()
+ return
+ }
+
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(initialState).toBe(true)
+
+ await switchContainer.locator('[data-slot="switch-control"]').click()
+ await page.waitForTimeout(100)
+
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(newState).toBe(false)
+
+ const stored = await page.evaluate((key) => {
+ const raw = localStorage.getItem(key)
+ return raw ? JSON.parse(raw) : null
+ }, settingsKey)
+
+ expect(stored?.updates?.startup).toBe(false)
+})
+
+test("toggling release notes switch updates localStorage", async ({ page, gotoSession }) => {
+
+ await gotoSession()
+
+ const dialog = await openSettings(page)
+ const switchContainer = dialog.locator(settingsReleaseNotesSelector)
+ await expect(switchContainer).toBeVisible()
+
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(initialState).toBe(true)
+
+ await switchContainer.locator('[data-slot="switch-control"]').click()
+ await page.waitForTimeout(100)
+
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
+ expect(newState).toBe(false)
+
+ const stored = await page.evaluate((key) => {
+ const raw = localStorage.getItem(key)
+ return raw ? JSON.parse(raw) : null
+ }, settingsKey)
+
+ expect(stored?.general?.releaseNotes).toBe(false)
+})
diff --git a/packages/app/e2e/status/status-popover.spec.ts b/packages/app/e2e/status/status-popover.spec.ts
new file mode 100644
index 000000000..4334cecb4
--- /dev/null
+++ b/packages/app/e2e/status/status-popover.spec.ts
@@ -0,0 +1,94 @@
+import { test, expect } from "../fixtures"
+import { openStatusPopover, defocus } from "../actions"
+
+test("status popover opens and shows tabs", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const { popoverBody } = await openStatusPopover(page)
+
+ await expect(popoverBody.getByRole("tab", { name: /servers/i })).toBeVisible()
+ await expect(popoverBody.getByRole("tab", { name: /mcp/i })).toBeVisible()
+ await expect(popoverBody.getByRole("tab", { name: /lsp/i })).toBeVisible()
+ await expect(popoverBody.getByRole("tab", { name: /plugins/i })).toBeVisible()
+
+ await page.keyboard.press("Escape")
+ await expect(popoverBody).toHaveCount(0)
+})
+
+test("status popover servers tab shows current server", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const { popoverBody } = await openStatusPopover(page)
+
+ const serversTab = popoverBody.getByRole("tab", { name: /servers/i })
+ await expect(serversTab).toHaveAttribute("aria-selected", "true")
+
+ const serverList = popoverBody.locator('[role="tabpanel"]').first()
+ await expect(serverList.locator("button").first()).toBeVisible()
+})
+
+test("status popover can switch to mcp tab", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const { popoverBody } = await openStatusPopover(page)
+
+ const mcpTab = popoverBody.getByRole("tab", { name: /mcp/i })
+ await mcpTab.click()
+
+ const ariaSelected = await mcpTab.getAttribute("aria-selected")
+ expect(ariaSelected).toBe("true")
+
+ const mcpContent = popoverBody.locator('[role="tabpanel"]:visible').first()
+ await expect(mcpContent).toBeVisible()
+})
+
+test("status popover can switch to lsp tab", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const { popoverBody } = await openStatusPopover(page)
+
+ const lspTab = popoverBody.getByRole("tab", { name: /lsp/i })
+ await lspTab.click()
+
+ const ariaSelected = await lspTab.getAttribute("aria-selected")
+ expect(ariaSelected).toBe("true")
+
+ const lspContent = popoverBody.locator('[role="tabpanel"]:visible').first()
+ await expect(lspContent).toBeVisible()
+})
+
+test("status popover can switch to plugins tab", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const { popoverBody } = await openStatusPopover(page)
+
+ const pluginsTab = popoverBody.getByRole("tab", { name: /plugins/i })
+ await pluginsTab.click()
+
+ const ariaSelected = await pluginsTab.getAttribute("aria-selected")
+ expect(ariaSelected).toBe("true")
+
+ const pluginsContent = popoverBody.locator('[role="tabpanel"]:visible').first()
+ await expect(pluginsContent).toBeVisible()
+})
+
+test("status popover closes on escape", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const { popoverBody } = await openStatusPopover(page)
+ await expect(popoverBody).toBeVisible()
+
+ await page.keyboard.press("Escape")
+ await expect(popoverBody).toHaveCount(0)
+})
+
+test("status popover closes when clicking outside", async ({ page, gotoSession }) => {
+ await gotoSession()
+
+ const { popoverBody } = await openStatusPopover(page)
+ await expect(popoverBody).toBeVisible()
+
+ await defocus(page)
+
+ await expect(popoverBody).toHaveCount(0)
+})