settings.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. import { test, expect, settingsKey } from "../fixtures"
  2. import { closeDialog, openSettings } from "../actions"
  3. import {
  4. settingsColorSchemeSelector,
  5. settingsCodeFontSelector,
  6. settingsLanguageSelectSelector,
  7. settingsNotificationsAgentSelector,
  8. settingsNotificationsErrorsSelector,
  9. settingsNotificationsPermissionsSelector,
  10. settingsReleaseNotesSelector,
  11. settingsSoundsAgentSelector,
  12. settingsSoundsErrorsSelector,
  13. settingsSoundsPermissionsSelector,
  14. settingsThemeSelector,
  15. settingsUIFontSelector,
  16. settingsUpdatesStartupSelector,
  17. } from "../selectors"
  18. test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
  19. await gotoSession()
  20. const dialog = await openSettings(page)
  21. await dialog.getByRole("tab", { name: "Shortcuts" }).click()
  22. await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible()
  23. await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible()
  24. await closeDialog(page, dialog)
  25. })
  26. test("changing language updates settings labels", async ({ page, gotoSession }) => {
  27. await page.addInitScript(() => {
  28. localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
  29. })
  30. await gotoSession()
  31. const dialog = await openSettings(page)
  32. const heading = dialog.getByRole("heading", { level: 2 })
  33. await expect(heading).toHaveText("General")
  34. const select = dialog.locator(settingsLanguageSelectSelector)
  35. await expect(select).toBeVisible()
  36. await select.locator('[data-slot="select-select-trigger"]').click()
  37. await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
  38. await expect(heading).toHaveText("Allgemein")
  39. await select.locator('[data-slot="select-select-trigger"]').click()
  40. await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
  41. await expect(heading).toHaveText("General")
  42. })
  43. test("changing color scheme persists in localStorage", async ({ page, gotoSession }) => {
  44. await gotoSession()
  45. const dialog = await openSettings(page)
  46. const select = dialog.locator(settingsColorSchemeSelector)
  47. await expect(select).toBeVisible()
  48. await select.locator('[data-slot="select-select-trigger"]').click()
  49. await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
  50. const colorScheme = await page.evaluate(() => {
  51. return document.documentElement.getAttribute("data-color-scheme")
  52. })
  53. expect(colorScheme).toBe("dark")
  54. await select.locator('[data-slot="select-select-trigger"]').click()
  55. await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Light" }).click()
  56. const lightColorScheme = await page.evaluate(() => {
  57. return document.documentElement.getAttribute("data-color-scheme")
  58. })
  59. expect(lightColorScheme).toBe("light")
  60. })
  61. test("changing theme persists in localStorage", async ({ page, gotoSession }) => {
  62. await gotoSession()
  63. const dialog = await openSettings(page)
  64. const select = dialog.locator(settingsThemeSelector)
  65. await expect(select).toBeVisible()
  66. const currentThemeId = await page.evaluate(() => {
  67. return document.documentElement.getAttribute("data-theme")
  68. })
  69. const currentTheme = (await select.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
  70. const trigger = select.locator('[data-slot="select-select-trigger"]')
  71. const items = page.locator('[data-slot="select-select-item"]')
  72. await trigger.click()
  73. const open = await expect
  74. .poll(async () => (await items.count()) > 0, { timeout: 5_000 })
  75. .toBe(true)
  76. .then(() => true)
  77. .catch(() => false)
  78. if (!open) {
  79. await trigger.click()
  80. await expect.poll(async () => (await items.count()) > 0, { timeout: 10_000 }).toBe(true)
  81. }
  82. await expect(items.first()).toBeVisible()
  83. const count = await items.count()
  84. expect(count).toBeGreaterThan(1)
  85. const nextTheme = (await items.locator('[data-slot="select-select-item-label"]').allTextContents())
  86. .map((x) => x.trim())
  87. .find((x) => x && x !== currentTheme)
  88. expect(nextTheme).toBeTruthy()
  89. await items.filter({ hasText: nextTheme! }).first().click()
  90. await page.keyboard.press("Escape")
  91. const storedThemeId = await page.evaluate(() => {
  92. return localStorage.getItem("opencode-theme-id")
  93. })
  94. expect(storedThemeId).not.toBeNull()
  95. expect(storedThemeId).not.toBe(currentThemeId)
  96. const dataTheme = await page.evaluate(() => {
  97. return document.documentElement.getAttribute("data-theme")
  98. })
  99. expect(dataTheme).toBe(storedThemeId)
  100. })
  101. test("legacy oc-1 theme migrates to oc-2", async ({ page, gotoSession }) => {
  102. await page.addInitScript(() => {
  103. localStorage.setItem("opencode-theme-id", "oc-1")
  104. localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
  105. localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
  106. })
  107. await gotoSession()
  108. await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2")
  109. await expect
  110. .poll(async () => {
  111. return await page.evaluate(() => {
  112. return localStorage.getItem("opencode-theme-id")
  113. })
  114. })
  115. .toBe("oc-2")
  116. await expect
  117. .poll(async () => {
  118. return await page.evaluate(() => {
  119. return localStorage.getItem("opencode-theme-css-light")
  120. })
  121. })
  122. .toBeNull()
  123. await expect
  124. .poll(async () => {
  125. return await page.evaluate(() => {
  126. return localStorage.getItem("opencode-theme-css-dark")
  127. })
  128. })
  129. .toBeNull()
  130. })
  131. test("typing a code font with spaces persists and updates CSS variable", async ({ page, gotoSession }) => {
  132. await gotoSession()
  133. const dialog = await openSettings(page)
  134. const input = dialog.locator(settingsCodeFontSelector)
  135. await expect(input).toBeVisible()
  136. await expect(input).toHaveAttribute("placeholder", "System Mono")
  137. const initialFontFamily = await page.evaluate(() =>
  138. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  139. )
  140. const initialUIFamily = await page.evaluate(() =>
  141. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  142. )
  143. expect(initialFontFamily).toContain("ui-monospace")
  144. const next = "Test Mono"
  145. await input.click()
  146. await input.clear()
  147. await input.pressSequentially(next)
  148. await expect(input).toHaveValue(next)
  149. await expect
  150. .poll(async () => {
  151. return await page.evaluate((key) => {
  152. const raw = localStorage.getItem(key)
  153. return raw ? JSON.parse(raw) : null
  154. }, settingsKey)
  155. })
  156. .toMatchObject({
  157. appearance: {
  158. mono: next,
  159. },
  160. })
  161. const newFontFamily = await page.evaluate(() =>
  162. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  163. )
  164. const newUIFamily = await page.evaluate(() =>
  165. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  166. )
  167. expect(newFontFamily).toContain(next)
  168. expect(newFontFamily).not.toBe(initialFontFamily)
  169. expect(newUIFamily).toBe(initialUIFamily)
  170. })
  171. test("typing a UI font with spaces persists and updates CSS variable", async ({ page, gotoSession }) => {
  172. await gotoSession()
  173. const dialog = await openSettings(page)
  174. const input = dialog.locator(settingsUIFontSelector)
  175. await expect(input).toBeVisible()
  176. await expect(input).toHaveAttribute("placeholder", "System Sans")
  177. const initialFontFamily = await page.evaluate(() =>
  178. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  179. )
  180. const initialCodeFamily = await page.evaluate(() =>
  181. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  182. )
  183. expect(initialFontFamily).toContain("ui-sans-serif")
  184. const next = "Test Sans"
  185. await input.click()
  186. await input.clear()
  187. await input.pressSequentially(next)
  188. await expect(input).toHaveValue(next)
  189. await expect
  190. .poll(async () => {
  191. return await page.evaluate((key) => {
  192. const raw = localStorage.getItem(key)
  193. return raw ? JSON.parse(raw) : null
  194. }, settingsKey)
  195. })
  196. .toMatchObject({
  197. appearance: {
  198. sans: next,
  199. },
  200. })
  201. const newFontFamily = await page.evaluate(() =>
  202. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  203. )
  204. const newCodeFamily = await page.evaluate(() =>
  205. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  206. )
  207. expect(newFontFamily).toContain(next)
  208. expect(newFontFamily).not.toBe(initialFontFamily)
  209. expect(newCodeFamily).toBe(initialCodeFamily)
  210. })
  211. test("clearing the code font field restores the default placeholder and stack", async ({ page, gotoSession }) => {
  212. await gotoSession()
  213. const dialog = await openSettings(page)
  214. const input = dialog.locator(settingsCodeFontSelector)
  215. await expect(input).toBeVisible()
  216. await input.click()
  217. await input.clear()
  218. await input.pressSequentially("Reset Mono")
  219. await expect
  220. .poll(async () => {
  221. return await page.evaluate((key) => {
  222. const raw = localStorage.getItem(key)
  223. return raw ? JSON.parse(raw) : null
  224. }, settingsKey)
  225. })
  226. .toMatchObject({
  227. appearance: {
  228. mono: "Reset Mono",
  229. },
  230. })
  231. await input.clear()
  232. await input.press("Space")
  233. await expect(input).toHaveValue("")
  234. await expect(input).toHaveAttribute("placeholder", "System Mono")
  235. await expect
  236. .poll(async () => {
  237. return await page.evaluate((key) => {
  238. const raw = localStorage.getItem(key)
  239. return raw ? JSON.parse(raw) : null
  240. }, settingsKey)
  241. })
  242. .toMatchObject({
  243. appearance: {
  244. mono: "",
  245. },
  246. })
  247. const fontFamily = await page.evaluate(() =>
  248. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  249. )
  250. expect(fontFamily).toContain("ui-monospace")
  251. expect(fontFamily).not.toContain("Reset Mono")
  252. })
  253. test("clearing the UI font field restores the default placeholder and stack", async ({ page, gotoSession }) => {
  254. await gotoSession()
  255. const dialog = await openSettings(page)
  256. const input = dialog.locator(settingsUIFontSelector)
  257. await expect(input).toBeVisible()
  258. await input.click()
  259. await input.clear()
  260. await input.pressSequentially("Reset Sans")
  261. await expect
  262. .poll(async () => {
  263. return await page.evaluate((key) => {
  264. const raw = localStorage.getItem(key)
  265. return raw ? JSON.parse(raw) : null
  266. }, settingsKey)
  267. })
  268. .toMatchObject({
  269. appearance: {
  270. sans: "Reset Sans",
  271. },
  272. })
  273. await input.clear()
  274. await input.press("Space")
  275. await expect(input).toHaveValue("")
  276. await expect(input).toHaveAttribute("placeholder", "System Sans")
  277. await expect
  278. .poll(async () => {
  279. return await page.evaluate((key) => {
  280. const raw = localStorage.getItem(key)
  281. return raw ? JSON.parse(raw) : null
  282. }, settingsKey)
  283. })
  284. .toMatchObject({
  285. appearance: {
  286. sans: "",
  287. },
  288. })
  289. const fontFamily = await page.evaluate(() =>
  290. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  291. )
  292. expect(fontFamily).toContain("ui-sans-serif")
  293. expect(fontFamily).not.toContain("Reset Sans")
  294. })
  295. test("color scheme, code font, and UI font rehydrate after reload", async ({ page, gotoSession }) => {
  296. await gotoSession()
  297. const dialog = await openSettings(page)
  298. const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector)
  299. await expect(colorSchemeSelect).toBeVisible()
  300. await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click()
  301. await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
  302. await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
  303. const code = dialog.locator(settingsCodeFontSelector)
  304. const ui = dialog.locator(settingsUIFontSelector)
  305. await expect(code).toBeVisible()
  306. await expect(ui).toBeVisible()
  307. const initialMono = await page.evaluate(() =>
  308. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  309. )
  310. const initialSans = await page.evaluate(() =>
  311. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  312. )
  313. const initialSettings = await page.evaluate((key) => {
  314. const raw = localStorage.getItem(key)
  315. return raw ? JSON.parse(raw) : null
  316. }, settingsKey)
  317. const mono = initialSettings?.appearance?.mono === "Reload Mono" ? "Reload Mono 2" : "Reload Mono"
  318. const sans = initialSettings?.appearance?.sans === "Reload Sans" ? "Reload Sans 2" : "Reload Sans"
  319. await code.click()
  320. await code.clear()
  321. await code.pressSequentially(mono)
  322. await expect(code).toHaveValue(mono)
  323. await ui.click()
  324. await ui.clear()
  325. await ui.pressSequentially(sans)
  326. await expect(ui).toHaveValue(sans)
  327. await expect
  328. .poll(async () => {
  329. return await page.evaluate((key) => {
  330. const raw = localStorage.getItem(key)
  331. return raw ? JSON.parse(raw) : null
  332. }, settingsKey)
  333. })
  334. .toMatchObject({
  335. appearance: {
  336. mono,
  337. sans,
  338. },
  339. })
  340. const updatedSettings = await page.evaluate((key) => {
  341. const raw = localStorage.getItem(key)
  342. return raw ? JSON.parse(raw) : null
  343. }, settingsKey)
  344. const updatedMono = await page.evaluate(() =>
  345. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  346. )
  347. const updatedSans = await page.evaluate(() =>
  348. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  349. )
  350. expect(updatedMono).toContain(mono)
  351. expect(updatedMono).not.toBe(initialMono)
  352. expect(updatedSans).toContain(sans)
  353. expect(updatedSans).not.toBe(initialSans)
  354. expect(updatedSettings?.appearance?.mono).toBe(mono)
  355. expect(updatedSettings?.appearance?.sans).toBe(sans)
  356. await closeDialog(page, dialog)
  357. await page.reload()
  358. await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
  359. await expect
  360. .poll(async () => {
  361. return await page.evaluate((key) => {
  362. const raw = localStorage.getItem(key)
  363. return raw ? JSON.parse(raw) : null
  364. }, settingsKey)
  365. })
  366. .toMatchObject({
  367. appearance: {
  368. mono,
  369. sans,
  370. },
  371. })
  372. const rehydratedSettings = await page.evaluate((key) => {
  373. const raw = localStorage.getItem(key)
  374. return raw ? JSON.parse(raw) : null
  375. }, settingsKey)
  376. await expect
  377. .poll(async () => {
  378. return await page.evaluate(() =>
  379. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  380. )
  381. })
  382. .toContain(mono)
  383. await expect
  384. .poll(async () => {
  385. return await page.evaluate(() =>
  386. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  387. )
  388. })
  389. .toContain(sans)
  390. const rehydratedMono = await page.evaluate(() =>
  391. getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
  392. )
  393. const rehydratedSans = await page.evaluate(() =>
  394. getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
  395. )
  396. expect(rehydratedMono).toContain(mono)
  397. expect(rehydratedMono).not.toBe(initialMono)
  398. expect(rehydratedSans).toContain(sans)
  399. expect(rehydratedSans).not.toBe(initialSans)
  400. expect(rehydratedSettings?.appearance?.mono).toBe(mono)
  401. expect(rehydratedSettings?.appearance?.sans).toBe(sans)
  402. })
  403. test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
  404. await gotoSession()
  405. const dialog = await openSettings(page)
  406. const switchContainer = dialog.locator(settingsNotificationsAgentSelector)
  407. await expect(switchContainer).toBeVisible()
  408. const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
  409. const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  410. expect(initialState).toBe(true)
  411. await switchContainer.locator('[data-slot="switch-control"]').click()
  412. await page.waitForTimeout(100)
  413. const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  414. expect(newState).toBe(false)
  415. const stored = await page.evaluate((key) => {
  416. const raw = localStorage.getItem(key)
  417. return raw ? JSON.parse(raw) : null
  418. }, settingsKey)
  419. expect(stored?.notifications?.agent).toBe(false)
  420. })
  421. test("toggling notification permissions switch updates localStorage", async ({ page, gotoSession }) => {
  422. await gotoSession()
  423. const dialog = await openSettings(page)
  424. const switchContainer = dialog.locator(settingsNotificationsPermissionsSelector)
  425. await expect(switchContainer).toBeVisible()
  426. const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
  427. const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  428. expect(initialState).toBe(true)
  429. await switchContainer.locator('[data-slot="switch-control"]').click()
  430. await page.waitForTimeout(100)
  431. const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  432. expect(newState).toBe(false)
  433. const stored = await page.evaluate((key) => {
  434. const raw = localStorage.getItem(key)
  435. return raw ? JSON.parse(raw) : null
  436. }, settingsKey)
  437. expect(stored?.notifications?.permissions).toBe(false)
  438. })
  439. test("toggling notification errors switch updates localStorage", async ({ page, gotoSession }) => {
  440. await gotoSession()
  441. const dialog = await openSettings(page)
  442. const switchContainer = dialog.locator(settingsNotificationsErrorsSelector)
  443. await expect(switchContainer).toBeVisible()
  444. const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
  445. const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  446. expect(initialState).toBe(false)
  447. await switchContainer.locator('[data-slot="switch-control"]').click()
  448. await page.waitForTimeout(100)
  449. const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  450. expect(newState).toBe(true)
  451. const stored = await page.evaluate((key) => {
  452. const raw = localStorage.getItem(key)
  453. return raw ? JSON.parse(raw) : null
  454. }, settingsKey)
  455. expect(stored?.notifications?.errors).toBe(true)
  456. })
  457. test("changing sound agent selection persists in localStorage", async ({ page, gotoSession }) => {
  458. await gotoSession()
  459. const dialog = await openSettings(page)
  460. const select = dialog.locator(settingsSoundsAgentSelector)
  461. await expect(select).toBeVisible()
  462. await select.locator('[data-slot="select-select-trigger"]').click()
  463. const items = page.locator('[data-slot="select-select-item"]')
  464. await items.nth(2).click()
  465. const stored = await page.evaluate((key) => {
  466. const raw = localStorage.getItem(key)
  467. return raw ? JSON.parse(raw) : null
  468. }, settingsKey)
  469. expect(stored?.sounds?.agent).not.toBe("staplebops-01")
  470. })
  471. test("selecting none disables agent sound", async ({ page, gotoSession }) => {
  472. await gotoSession()
  473. const dialog = await openSettings(page)
  474. const select = dialog.locator(settingsSoundsAgentSelector)
  475. const trigger = select.locator('[data-slot="select-select-trigger"]')
  476. await expect(select).toBeVisible()
  477. await expect(trigger).toBeEnabled()
  478. await trigger.click()
  479. const items = page.locator('[data-slot="select-select-item"]')
  480. await expect(items.first()).toBeVisible()
  481. await items.first().click()
  482. const stored = await page.evaluate((key) => {
  483. const raw = localStorage.getItem(key)
  484. return raw ? JSON.parse(raw) : null
  485. }, settingsKey)
  486. expect(stored?.sounds?.agentEnabled).toBe(false)
  487. })
  488. test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => {
  489. await gotoSession()
  490. const dialog = await openSettings(page)
  491. const permissionsSelect = dialog.locator(settingsSoundsPermissionsSelector)
  492. const errorsSelect = dialog.locator(settingsSoundsErrorsSelector)
  493. await expect(permissionsSelect).toBeVisible()
  494. await expect(errorsSelect).toBeVisible()
  495. const initial = await page.evaluate((key) => {
  496. const raw = localStorage.getItem(key)
  497. return raw ? JSON.parse(raw) : null
  498. }, settingsKey)
  499. const permissionsCurrent =
  500. (await permissionsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
  501. await permissionsSelect.locator('[data-slot="select-select-trigger"]').click()
  502. const permissionItems = page.locator('[data-slot="select-select-item"]')
  503. expect(await permissionItems.count()).toBeGreaterThan(1)
  504. if (permissionsCurrent) {
  505. await permissionItems.filter({ hasNotText: permissionsCurrent }).first().click()
  506. }
  507. if (!permissionsCurrent) {
  508. await permissionItems.nth(1).click()
  509. }
  510. const errorsCurrent =
  511. (await errorsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
  512. await errorsSelect.locator('[data-slot="select-select-trigger"]').click()
  513. const errorItems = page.locator('[data-slot="select-select-item"]')
  514. expect(await errorItems.count()).toBeGreaterThan(1)
  515. if (errorsCurrent) {
  516. await errorItems.filter({ hasNotText: errorsCurrent }).first().click()
  517. }
  518. if (!errorsCurrent) {
  519. await errorItems.nth(1).click()
  520. }
  521. await expect
  522. .poll(async () => {
  523. return await page.evaluate((key) => {
  524. const raw = localStorage.getItem(key)
  525. return raw ? JSON.parse(raw) : null
  526. }, settingsKey)
  527. })
  528. .toMatchObject({
  529. sounds: {
  530. permissions: expect.any(String),
  531. errors: expect.any(String),
  532. },
  533. })
  534. const stored = await page.evaluate((key) => {
  535. const raw = localStorage.getItem(key)
  536. return raw ? JSON.parse(raw) : null
  537. }, settingsKey)
  538. expect(stored?.sounds?.permissions).not.toBe(initial?.sounds?.permissions)
  539. expect(stored?.sounds?.errors).not.toBe(initial?.sounds?.errors)
  540. })
  541. test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => {
  542. await gotoSession()
  543. const dialog = await openSettings(page)
  544. const switchContainer = dialog.locator(settingsUpdatesStartupSelector)
  545. await expect(switchContainer).toBeVisible()
  546. const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
  547. const isDisabled = await toggleInput.evaluate((el: HTMLInputElement) => el.disabled)
  548. if (isDisabled) {
  549. test.skip()
  550. return
  551. }
  552. const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  553. expect(initialState).toBe(true)
  554. await switchContainer.locator('[data-slot="switch-control"]').click()
  555. await page.waitForTimeout(100)
  556. const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  557. expect(newState).toBe(false)
  558. const stored = await page.evaluate((key) => {
  559. const raw = localStorage.getItem(key)
  560. return raw ? JSON.parse(raw) : null
  561. }, settingsKey)
  562. expect(stored?.updates?.startup).toBe(false)
  563. })
  564. test("toggling release notes switch updates localStorage", async ({ page, gotoSession }) => {
  565. await gotoSession()
  566. const dialog = await openSettings(page)
  567. const switchContainer = dialog.locator(settingsReleaseNotesSelector)
  568. await expect(switchContainer).toBeVisible()
  569. const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
  570. const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  571. expect(initialState).toBe(true)
  572. await switchContainer.locator('[data-slot="switch-control"]').click()
  573. await page.waitForTimeout(100)
  574. const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
  575. expect(newState).toBe(false)
  576. const stored = await page.evaluate((key) => {
  577. const raw = localStorage.getItem(key)
  578. return raw ? JSON.parse(raw) : null
  579. }, settingsKey)
  580. expect(stored?.general?.releaseNotes).toBe(false)
  581. })