import { test, expect, settingsKey } from "../fixtures" import { closeDialog, openSettings } from "../actions" import { settingsColorSchemeSelector, settingsCodeFontSelector, settingsLanguageSelectSelector, settingsNotificationsAgentSelector, settingsNotificationsErrorsSelector, settingsNotificationsPermissionsSelector, settingsReleaseNotesSelector, settingsSoundsAgentSelector, settingsSoundsErrorsSelector, settingsSoundsPermissionsSelector, settingsThemeSelector, settingsUIFontSelector, settingsUpdatesStartupSelector, } from "../selectors" test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => { await gotoSession() const dialog = await openSettings(page) await dialog.getByRole("tab", { name: "Shortcuts" }).click() await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible() await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible() 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() const currentThemeId = await page.evaluate(() => { return document.documentElement.getAttribute("data-theme") }) const currentTheme = (await select.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" const trigger = select.locator('[data-slot="select-select-trigger"]') const items = page.locator('[data-slot="select-select-item"]') await trigger.click() const open = await expect .poll(async () => (await items.count()) > 0, { timeout: 5_000 }) .toBe(true) .then(() => true) .catch(() => false) if (!open) { await trigger.click() await expect.poll(async () => (await items.count()) > 0, { timeout: 10_000 }).toBe(true) } await expect(items.first()).toBeVisible() const count = await items.count() expect(count).toBeGreaterThan(1) const nextTheme = (await items.locator('[data-slot="select-select-item-label"]').allTextContents()) .map((x) => x.trim()) .find((x) => x && x !== currentTheme) expect(nextTheme).toBeTruthy() await items.filter({ hasText: nextTheme! }).first().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(currentThemeId) const dataTheme = await page.evaluate(() => { return document.documentElement.getAttribute("data-theme") }) expect(dataTheme).toBe(storedThemeId) }) test("legacy oc-1 theme migrates to oc-2", async ({ page, gotoSession }) => { await page.addInitScript(() => { localStorage.setItem("opencode-theme-id", "oc-1") localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;") localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;") }) await gotoSession() await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2") await expect .poll(async () => { return await page.evaluate(() => { return localStorage.getItem("opencode-theme-id") }) }) .toBe("oc-2") await expect .poll(async () => { return await page.evaluate(() => { return localStorage.getItem("opencode-theme-css-light") }) }) .toBeNull() await expect .poll(async () => { return await page.evaluate(() => { return localStorage.getItem("opencode-theme-css-dark") }) }) .toBeNull() }) test("typing a code font with spaces persists and updates CSS variable", async ({ page, gotoSession }) => { await gotoSession() const dialog = await openSettings(page) const input = dialog.locator(settingsCodeFontSelector) await expect(input).toBeVisible() await expect(input).toHaveAttribute("placeholder", "System Mono") const initialFontFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) const initialUIFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) expect(initialFontFamily).toContain("ui-monospace") const next = "Test Mono" await input.click() await input.clear() await input.pressSequentially(next) await expect(input).toHaveValue(next) await expect .poll(async () => { return await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) }) .toMatchObject({ appearance: { mono: next, }, }) const newFontFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) const newUIFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) expect(newFontFamily).toContain(next) expect(newFontFamily).not.toBe(initialFontFamily) expect(newUIFamily).toBe(initialUIFamily) }) test("typing a UI font with spaces persists and updates CSS variable", async ({ page, gotoSession }) => { await gotoSession() const dialog = await openSettings(page) const input = dialog.locator(settingsUIFontSelector) await expect(input).toBeVisible() await expect(input).toHaveAttribute("placeholder", "System Sans") const initialFontFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) const initialCodeFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) expect(initialFontFamily).toContain("ui-sans-serif") const next = "Test Sans" await input.click() await input.clear() await input.pressSequentially(next) await expect(input).toHaveValue(next) await expect .poll(async () => { return await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) }) .toMatchObject({ appearance: { sans: next, }, }) const newFontFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) const newCodeFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) expect(newFontFamily).toContain(next) expect(newFontFamily).not.toBe(initialFontFamily) expect(newCodeFamily).toBe(initialCodeFamily) }) test("clearing the code font field restores the default placeholder and stack", async ({ page, gotoSession }) => { await gotoSession() const dialog = await openSettings(page) const input = dialog.locator(settingsCodeFontSelector) await expect(input).toBeVisible() await input.click() await input.clear() await input.pressSequentially("Reset Mono") await expect .poll(async () => { return await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) }) .toMatchObject({ appearance: { mono: "Reset Mono", }, }) await input.clear() await input.press("Space") await expect(input).toHaveValue("") await expect(input).toHaveAttribute("placeholder", "System Mono") await expect .poll(async () => { return await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) }) .toMatchObject({ appearance: { mono: "", }, }) const fontFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) expect(fontFamily).toContain("ui-monospace") expect(fontFamily).not.toContain("Reset Mono") }) test("clearing the UI font field restores the default placeholder and stack", async ({ page, gotoSession }) => { await gotoSession() const dialog = await openSettings(page) const input = dialog.locator(settingsUIFontSelector) await expect(input).toBeVisible() await input.click() await input.clear() await input.pressSequentially("Reset Sans") await expect .poll(async () => { return await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) }) .toMatchObject({ appearance: { sans: "Reset Sans", }, }) await input.clear() await input.press("Space") await expect(input).toHaveValue("") await expect(input).toHaveAttribute("placeholder", "System Sans") await expect .poll(async () => { return await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) }) .toMatchObject({ appearance: { sans: "", }, }) const fontFamily = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) expect(fontFamily).toContain("ui-sans-serif") expect(fontFamily).not.toContain("Reset Sans") }) test("color scheme, code font, and UI 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 code = dialog.locator(settingsCodeFontSelector) const ui = dialog.locator(settingsUIFontSelector) await expect(code).toBeVisible() await expect(ui).toBeVisible() const initialMono = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) const initialSans = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) const initialSettings = await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) const mono = initialSettings?.appearance?.mono === "Reload Mono" ? "Reload Mono 2" : "Reload Mono" const sans = initialSettings?.appearance?.sans === "Reload Sans" ? "Reload Sans 2" : "Reload Sans" await code.click() await code.clear() await code.pressSequentially(mono) await expect(code).toHaveValue(mono) await ui.click() await ui.clear() await ui.pressSequentially(sans) await expect(ui).toHaveValue(sans) await expect .poll(async () => { return await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) }) .toMatchObject({ appearance: { mono, sans, }, }) const updatedSettings = await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) const updatedMono = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) const updatedSans = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) expect(updatedMono).toContain(mono) expect(updatedMono).not.toBe(initialMono) expect(updatedSans).toContain(sans) expect(updatedSans).not.toBe(initialSans) expect(updatedSettings?.appearance?.mono).toBe(mono) expect(updatedSettings?.appearance?.sans).toBe(sans) 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: { mono, sans, }, }) 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(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) }) .toContain(mono) await expect .poll(async () => { return await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) }) .toContain(sans) const rehydratedMono = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), ) const rehydratedSans = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), ) expect(rehydratedMono).toContain(mono) expect(rehydratedMono).not.toBe(initialMono) expect(rehydratedSans).toContain(sans) expect(rehydratedSans).not.toBe(initialSans) expect(rehydratedSettings?.appearance?.mono).toBe(mono) expect(rehydratedSettings?.appearance?.sans).toBe(sans) }) 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("selecting none disables agent sound", async ({ page, gotoSession }) => { await gotoSession() const dialog = await openSettings(page) const select = dialog.locator(settingsSoundsAgentSelector) const trigger = select.locator('[data-slot="select-select-trigger"]') await expect(select).toBeVisible() await expect(trigger).toBeEnabled() await trigger.click() const items = page.locator('[data-slot="select-select-item"]') await expect(items.first()).toBeVisible() await items.first().click() const stored = await page.evaluate((key) => { const raw = localStorage.getItem(key) return raw ? JSON.parse(raw) : null }, settingsKey) expect(stored?.sounds?.agentEnabled).toBe(false) }) 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() 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) })