import assert from "node:assert/strict"; import test from "node:test"; import Color from "color"; import { getDerivedThemeColors, THEMES } from "../../src/lib/themes.ts"; import { withApp } from "../lib/app.mjs"; const THEME_VARIABLES = [ "--background", "--foreground", "--card", "--card-foreground", "--popover", "--popover-foreground", "--primary", "--primary-foreground", "--secondary", "--secondary-foreground", "--muted", "--muted-foreground", "--accent", "--accent-foreground", "--destructive", "--destructive-foreground", "--success", "--success-foreground", "--warning", "--warning-foreground", "--border", "--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5", ]; const DRACULA_THEME = THEMES.find((theme) => theme.id === "dracula").colors; const AYU_LIGHT_THEME = THEMES.find((theme) => theme.id === "ayu-light").colors; async function dismissSurface(app) { await app.pressShortcut({ key: "Escape" }); await new Promise((resolve) => setTimeout(resolve, 100)); } async function themeSnapshot(app) { return app.execute( ` const root = document.documentElement; const rootStyle = getComputedStyle(root); const bodyStyle = getComputedStyle(document.body); const variables = arguments[0]; return { mode: root.classList.contains("light") ? "light" : root.classList.contains("dark") ? "dark" : "unset", inline: Object.fromEntries( variables.map((key) => [key, root.style.getPropertyValue(key).trim()]) ), resolved: Object.fromEntries( variables.map((key) => [key, rootStyle.getPropertyValue(key).trim()]) ), bodyBackground: bodyStyle.backgroundColor, bodyForeground: bodyStyle.color, }; `, [THEME_VARIABLES], ); } async function waitForTheme(app, predicate, description) { return app.waitFor( async () => { const snapshot = await themeSnapshot(app); return predicate(snapshot) ? snapshot : false; }, { description }, ); } function themeVariablesEqual(actual, expected) { return THEME_VARIABLES.every( (key) => actual[key]?.toLowerCase() === expected[key]?.toLowerCase(), ); } async function applyThemeForContrastAudit(app, theme) { await app.execute( ` const [colors, derived, mode] = arguments; const root = document.documentElement; root.classList.remove("light", "dark"); root.classList.add(mode); for (const [key, value] of Object.entries({ ...colors, ...derived })) { root.style.setProperty(key, value, "important"); } `, [theme.colors, getDerivedThemeColors(theme.colors), theme.mode], ); await new Promise((resolve) => setTimeout(resolve, 200)); } async function animatedTabContrastSnapshot(app) { return app.execute(` const rootStyle = getComputedStyle(document.documentElement); const triggers = [ ...document.querySelectorAll('[data-slot="animated-tabs-trigger"]'), ]; const active = triggers.find( (trigger) => trigger.getAttribute("data-state") === "active", ); const inactive = triggers.find( (trigger) => trigger.getAttribute("data-state") === "inactive", ); const content = (trigger) => [...(trigger?.children ?? [])].filter( (child) => child.getAttribute("data-slot") !== "animated-tabs-indicator", ); const activeContent = content(active); const inactiveContent = content(inactive); const indicator = active?.querySelector( '[data-slot="animated-tabs-indicator"]', ); return { mode: document.documentElement.classList.contains("light") ? "light" : "dark", background: rootStyle.getPropertyValue("--background").trim(), accent: rootStyle.getPropertyValue("--accent").trim(), accentForeground: rootStyle .getPropertyValue("--accent-foreground") .trim(), mutedForeground: rootStyle .getPropertyValue("--muted-foreground") .trim(), activeTitle: activeContent[0] ? getComputedStyle(activeContent[0]).color : null, activeCount: activeContent[1] ? getComputedStyle(activeContent[1]).color : null, activeBackground: indicator ? getComputedStyle(indicator).backgroundColor : null, inactiveTitle: inactiveContent[0] ? getComputedStyle(inactiveContent[0]).color : null, inactiveCount: inactiveContent[1] ? getComputedStyle(inactiveContent[1]).color : null, }; `); } function assertColorEquals(actual, expected, description) { assert.ok(actual, `${description} is missing`); assert.equal(Color(actual).hex(), Color(expected).hex(), description); } function assertContrast(foreground, background, minimum, description) { assert.ok(foreground, `${description} foreground is missing`); assert.ok(background, `${description} background is missing`); const ratio = Color(foreground).contrast(Color(background)); assert.ok( ratio >= minimum, `${description} is ${ratio.toFixed(2)}:1; expected at least ${minimum}:1`, ); } async function chooseSelectOption(app, triggerSelector, option) { await app.clickSelector(triggerSelector); await app.clickText(option, { roles: ["option"] }); } async function saveSettings(app) { await app.clickText("Save Settings", { roles: ["button"] }); await app.waitFor( () => app.execute(`return document.querySelector("#theme-select") === null;`), { description: "Settings to close after saving" }, ); } async function assertThemeAcrossNavigation(app, expected) { for (const surface of ["Network", "Extensions", "Profiles"]) { await app.clickSelector(`[aria-label="${surface}"]`); await app.waitFor( async () => JSON.stringify(await themeSnapshot(app)) === JSON.stringify(expected), { description: `theme to remain unchanged on ${surface}` }, ); } } async function dragBackgroundColorPicker(app) { await app.clickSelector('[aria-label="Background"]'); const drag = await app.waitFor( () => app.execute(` const popover = document.querySelector('[data-slot="popover-content"]'); const selection = [...(popover?.querySelectorAll("div") ?? [])].find( (node) => node.style.background.includes("linear-gradient") ); if (!selection) return null; const rect = selection.getBoundingClientRect(); const points = []; for (const yf of [0.2, 0.4, 0.6, 0.8]) { for (const xf of [0.2, 0.4, 0.6, 0.8]) { const point = { x: Math.round(rect.left + rect.width * xf), y: Math.round(rect.top + rect.height * yf), }; const hit = document.elementFromPoint(point.x, point.y); if (hit === selection || selection.contains(hit)) points.push(point); } } return points.length >= 2 ? { start: points[0], end: points[points.length - 1] } : null; `), { description: "two pointer-interactive background color picker points" }, ); await app.execute(` window.__donutE2eThemePointerEvents = []; for (const type of ["pointermove", "pointerdown", "pointerup"]) { window.addEventListener(type, (event) => { window.__donutE2eThemePointerEvents.push({ type, x: event.clientX, y: event.clientY, buttons: event.buttons, target: event.target?.className ?? event.target?.tagName ?? "", }); }, true); } `); await app.session.command("POST", "/actions", { actions: [ { type: "pointer", id: "theme-color-pointer", actions: [ { type: "pointerMove", x: drag.start.x, y: drag.start.y, origin: "viewport", }, { type: "pointerDown", button: 0 }, { type: "pause", duration: 150 }, { type: "pointerMove", x: drag.end.x, y: drag.end.y, duration: 100, origin: "viewport", }, { type: "pointerUp", button: 0 }, ], }, ], }); const pointerEvents = await app.execute( `return window.__donutE2eThemePointerEvents ?? [];`, ); assert.equal(pointerEvents[0]?.type, "pointermove"); assert.equal(pointerEvents[1]?.type, "pointerdown"); assert.equal(pointerEvents.at(-1)?.type, "pointerup"); const dragMoves = pointerEvents.slice(2, -1); assert.ok(dragMoves.length >= 1); assert.ok(dragMoves.every((event) => event.type === "pointermove")); assert.match(pointerEvents[1].target, /cursor-pointer/); assert.match(dragMoves.at(-1).target, /cursor-pointer/); assert.equal(dragMoves.at(-1).buttons, 1); await app.waitFor( () => app.execute( `return document.querySelector("#theme-preset-select")?.textContent?.includes("Your Own") === true;`, ), { description: `customized theme to be marked as Your Own after ${JSON.stringify(pointerEvents)}`, }, ); await app.clickSelector('[aria-label="Background"]'); await app.waitFor( () => app.execute( `return document.querySelector('[data-slot="popover-content"]') === null;`, ), { description: "color picker to close" }, ); } test("all primary navigation buttons and sub-page tabs render and remain interactive", async () => { await withApp("ui-navigation", async (app) => { const surfaces = [ ["Settings", /General|Appearance|Sync/i], ["Network", /Proxies|VPNs|DNS/i], ["Extensions", /Extensions|Groups/i], ["Integrations", /API|MCP/i], ["Account", /Account|Sign in/i], ]; for (const [label, expected] of surfaces) { await app.clickSelector(`[aria-label="${label}"]`); await app.waitFor(async () => expected.test(await app.bodyText()), { description: `${label} surface`, }); assert.match(await app.bodyText(), expected); await dismissSurface(app); } await app.clickSelector('[aria-label="Groups"]'); await app.waitForText("Create"); await dismissSurface(app); await app.clickSelector('[aria-label="More"]'); await app.waitFor( () => app.execute(`return Boolean(document.querySelector("[role='menu']"));`), { description: "More menu" }, ); await dismissSurface(app); await app.clickSelector('[aria-label="Profiles"]'); await app.clickText("New"); await app.waitFor( () => app.execute( `return Boolean(document.querySelector("[role='dialog']"));`, ), { description: "new profile dialog" }, ); assert.match(await app.bodyText(), /profile/i); await dismissSurface(app); }); }); test("every custom theme keeps tabs, counts, group pills, and rail states readable", async () => { await withApp("ui-theme-contrast", async (app) => { await app.clickSelector('[aria-label="Extensions"]'); await app.waitFor( () => app.execute( `return document.querySelectorAll('[data-slot="animated-tabs-trigger"]').length === 2;`, ), { description: "Extension tabs" }, ); for (const theme of THEMES) { await applyThemeForContrastAudit(app, theme); const snapshot = await animatedTabContrastSnapshot(app); assert.equal(snapshot.mode, theme.mode, `${theme.id} appearance mode`); assertColorEquals( snapshot.activeBackground, snapshot.accent, `${theme.id} active tab background`, ); for (const [label, color] of [ ["active tab title", snapshot.activeTitle], ["active tab count", snapshot.activeCount], ]) { assertColorEquals( color, snapshot.accentForeground, `${theme.id} ${label} token`, ); assertContrast( color, snapshot.activeBackground, 4.5, `${theme.id} ${label}`, ); } for (const [label, color] of [ ["inactive tab title", snapshot.inactiveTitle], ["inactive tab count", snapshot.inactiveCount], ]) { assertColorEquals( color, snapshot.mutedForeground, `${theme.id} ${label} token`, ); assertContrast(color, snapshot.background, 4.5, `${theme.id} ${label}`); } await app.clickSelector( '[data-slot="animated-tabs-trigger"][data-state="inactive"]', ); await app.waitFor( () => app.execute( `return Boolean(document.querySelector( '[data-slot="animated-tabs-trigger"][data-state="active"] [data-slot="animated-tabs-indicator"]' ));`, ), { description: `${theme.id} tab indicator after switching` }, ); } await app.clickSelector('[aria-label="Groups"]'); await app.waitFor( () => app.execute( `return Boolean(document.querySelector('[data-slot="group-summary-pill"]'));`, ), { description: "Profile groups summary pill" }, ); for (const theme of THEMES) { await applyThemeForContrastAudit(app, theme); const snapshot = await app.execute(` const rootStyle = getComputedStyle(document.documentElement); const pill = document.querySelector( '[data-slot="group-summary-pill"]', ); const count = pill?.querySelector( '[data-slot="group-summary-count"]', ); const title = pill?.firstElementChild; const rail = document.querySelector('nav [aria-current="page"]'); return { pillBackground: pill ? getComputedStyle(pill).backgroundColor : null, pillTitle: title ? getComputedStyle(title).color : null, pillCount: count ? getComputedStyle(count).color : null, railBackground: rail ? getComputedStyle(rail).backgroundColor : null, railForeground: rail ? getComputedStyle(rail).color : null, accent: rootStyle.getPropertyValue("--accent").trim(), accentForeground: rootStyle .getPropertyValue("--accent-foreground") .trim(), }; `); assertColorEquals( snapshot.pillBackground, snapshot.accent, `${theme.id} group pill background`, ); for (const [label, color] of [ ["group pill title", snapshot.pillTitle], ["group pill count", snapshot.pillCount], ]) { assertColorEquals( color, snapshot.accentForeground, `${theme.id} ${label} token`, ); assertContrast( color, snapshot.pillBackground, 4.5, `${theme.id} ${label}`, ); } assertContrast( snapshot.railForeground, snapshot.railBackground, 3, `${theme.id} selected rail icon`, ); } }); }); test("VLESS proxy form keeps the share URI as one clear, validated input", async () => { await withApp("ui-vless-proxy-form", async (app) => { const uri = "vless://6d6e21a1-4829-4d2b-bc7f-1b25707b61e4@vpn.example.com:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.example.com&fp=chrome&pbk=BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc&sid=0123456789abcdef&type=tcp#E2E"; await app.clickSelector('[aria-label="Network"]'); await app.waitForText("New proxy"); await app.clickSelector('[aria-label="New proxy"]'); await app.waitForText("Add Proxy"); await app.fillSelector("#proxy-name", "E2E VLESS"); await chooseSelectOption(app, "#proxy-type", "VLESS · Vision · REALITY"); assert.equal( await app.execute( `return document.querySelector("#proxy-vless-uri") instanceof HTMLTextAreaElement;`, ), true, ); assert.equal( await app.execute( `return document.querySelector("#proxy-host") === null && document.querySelector("#proxy-port") === null && document.querySelector("#proxy-username") === null && document.querySelector("#proxy-password") === null;`, ), true, ); await app.fillSelector( "#proxy-vless-uri", "vless://6d6e21a1-4829-4d2b-bc7f-1b25707b61e4@vpn.example.com", ); await app.waitFor( () => app.execute( `return document.querySelector("#proxy-vless-uri")?.getAttribute("aria-invalid") === "true";`, ), { description: "invalid VLESS endpoint feedback" }, ); assert.equal( await app.execute( `return [...document.querySelectorAll("[role='dialog'] button")] .find((button) => button.textContent?.trim() === "Add Proxy") ?.disabled === true;`, ), true, ); await app.fillSelector("#proxy-vless-uri", uri); await app.waitFor( () => app.execute( `return document.querySelector("#proxy-vless-uri")?.getAttribute("aria-invalid") === "false";`, ), { description: "valid VLESS endpoint feedback" }, ); await app.clickTextIn('[role="dialog"]', "Add Proxy", { roles: ["button"], }); await app.waitForText("E2E VLESS"); const proxy = (await app.invoke("get_stored_proxies")).find( (item) => item.name === "E2E VLESS", ); assert.ok(proxy); assert.equal(proxy.proxy_settings.proxy_type, "vless"); assert.equal(proxy.proxy_settings.host, "vpn.example.com"); assert.equal(proxy.proxy_settings.port, 443); assert.equal(proxy.proxy_settings.username, null); assert.equal(proxy.proxy_settings.password, null); assert.match(proxy.proxy_settings.vless_uri, /^vless:\/\//); await app.invoke("delete_stored_proxy", { proxyId: proxy.id }); }); }); test("About exposes a searchable, responsive third-party license inventory", async () => { await withApp("ui-about-licenses", async (app) => { await app.clickSelector('[aria-label="More"]'); await app.waitFor( () => app.execute(`return Boolean(document.querySelector("[role='menu']"));`), { description: "More menu" }, ); await app.clickText("About Donut Browser", { exact: false, roles: ["menuitem"], }); await app.waitForText("Open-source anti-detect browser."); const aboutText = await app.execute( `return document.querySelector("[role='dialog']")?.textContent ?? "";`, ); assert.doesNotMatch(aboutText, /AGPL-3\.0|licensed under/i); await app.clickText("Licenses", { roles: ["button"] }); await app.waitFor( () => app.execute( `return document.querySelector("[role='dialog'] [data-slot='dialog-title']")?.textContent === "Licenses";`, ), { description: "Licenses view" }, ); await app.session.command("POST", "/window/rect", { width: 640, height: 400, }); const inventory = await app.execute(` const dialog = document.querySelector("[role='dialog']"); const list = dialog?.querySelector(".scroll-fade"); const search = dialog?.querySelector('input[type="search"]'); const rows = [...(dialog?.querySelectorAll("li") ?? [])].map((row) => [...row.children].map((child) => (child.textContent || "").trim()) ); const rect = dialog?.getBoundingClientRect(); return { activeSearch: document.activeElement === search, rows, scrollable: Boolean(list && list.scrollHeight > list.clientHeight), bounds: rect ? { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, viewportWidth: innerWidth, viewportHeight: innerHeight, } : null, }; `); assert.equal(inventory.activeSearch, true); assert.equal(inventory.scrollable, true); assert.ok(inventory.bounds); assert.ok(inventory.bounds.left >= 0); assert.ok(inventory.bounds.top >= 0); assert.ok(inventory.bounds.right <= inventory.bounds.viewportWidth); assert.ok(inventory.bounds.bottom <= inventory.bounds.viewportHeight); assert.ok( inventory.rows.some( ([name, license]) => name === "Xray-core" && license === "MPL-2.0", ), ); assert.ok( inventory.rows.some( ([name, license]) => name === "Donut Browser" && license === "AGPL-3.0-only", ), ); assert.ok( inventory.rows.some( ([name, license]) => name === "tauri-plugin-opener" && license === "Apache-2.0 OR MIT", ), ); assert.ok(inventory.rows.every((row) => row.length === 2)); const search = await app.session.findCss('input[type="search"]'); await app.session.sendKeys(search, "xray"); await app.waitFor( () => app.execute( `return document.querySelectorAll("[role='dialog'] li").length === 1;`, ), { description: "license search result" }, ); assert.match( await app.execute( `return document.querySelector("[role='dialog'] li")?.textContent ?? "";`, ), /Xray-core.*MPL-2\.0/s, ); await app.clickSelector('button[aria-label="Back"]'); await app.waitFor( () => app.execute( `return document.querySelector("[role='dialog'] [data-slot='dialog-title']")?.textContent === "About";`, ), { description: "About view after returning from licenses" }, ); assert.equal( await app.execute( `return document.activeElement?.textContent?.trim() === "Licenses";`, ), true, ); }); }); test("settings tabs, command palette filtering, and responsive layout survive resize", async () => { await withApp("ui-settings-responsive", async (app) => { await app.clickSelector('[aria-label="Settings"]'); await app.waitForText("Appearance"); for (const tab of ["Appearance", "Sync", "Encryption"]) { const exists = await app.execute( `return [...document.querySelectorAll("[role='tab']")].some( (node) => (node.textContent || "").trim() === arguments[0] );`, [tab], ); if (exists) { await app.clickText(tab, { roles: ["tab"] }); await app.waitFor( () => app.execute( `return [...document.querySelectorAll("[role='tab']")].some( (node) => (node.textContent || "").trim() === arguments[0] && node.getAttribute("data-state") === "active" );`, [tab], ), { description: `${tab} settings tab` }, ); } } await dismissSurface(app); const modifier = process.platform === "darwin" ? { meta: true } : { ctrl: true }; await app.pressShortcut({ key: "k", ...modifier }); await app.waitFor( () => app.execute(`return Boolean(document.querySelector("[cmdk-input]"));`), { description: "command palette" }, ); const input = await app.session.findCss("[cmdk-input]"); await app.session.sendKeys(input, "proxy vpn"); assert.match(await app.bodyText(), /Network|Proxy|VPN/i); await dismissSurface(app); // The native driver owns the top-level window. Resize through the WebDriver // protocol and assert the app still has usable controls at the minimum size. await app.session.command("POST", "/window/rect", { width: 640, height: 400, }); const viewport = await app.execute( "return { width: innerWidth, height: innerHeight };", ); assert.ok(viewport.width >= 600); assert.ok(viewport.height >= 350); assert.equal( await app.execute( `return document.querySelector('[aria-label="Settings"]').getBoundingClientRect().width > 0;`, ), true, ); }); }); test("first-run onboarding stays recoverable, responsive, and platform-aware", async () => { await withApp( "ui-onboarding", async (app) => { await app.waitForText("Welcome to Donut Browser"); assert.equal(await app.invoke("get_onboarding_completed"), false); await app.session.command("POST", "/window/rect", { width: 640, height: 400, }); const layout = await app.execute(` const dialog = document.querySelector("[role='dialog']"); const progress = dialog?.querySelector("[role='progressbar']"); if (!dialog || !progress) return null; const rect = dialog.getBoundingClientRect(); return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, viewportWidth: innerWidth, viewportHeight: innerHeight, progressNow: progress.getAttribute("aria-valuenow"), }; `); assert.ok(layout); assert.ok(layout.left >= 0 && layout.right <= layout.viewportWidth); assert.ok(layout.top >= 0 && layout.bottom <= layout.viewportHeight); assert.equal(layout.progressNow, "1"); await app.clickText("Next", { roles: ["button"] }); await app.waitForText("Licensing"); await app.clickText("I understand", { roles: ["button"] }); await app.waitFor( async () => /Allow microphone & camera|Setting things up|Setup failed/.test( await app.bodyText(), ), { description: "platform-appropriate onboarding step" }, ); const body = await app.bodyText(); if (process.platform !== "darwin") { assert.doesNotMatch(body, /Allow microphone & camera/); assert.match(body, /Setting things up|Setup failed/); } // Setup and the optional product tour are still unfinished, so a crash // or restart must be able to resume onboarding. assert.equal(await app.invoke("get_onboarding_completed"), false); }, { onboardingCompleted: false }, ); }); test("predefined theme remains rendered across navigation and restart", async () => { await withApp("ui-theme-predefined", async (app) => { await app.clickSelector('[aria-label="Settings"]'); await app.waitForText("Appearance"); await chooseSelectOption(app, "#theme-select", "Light"); await saveSettings(app); const persisted = await app.invoke("get_app_settings"); assert.equal(persisted.theme, "light"); const selected = await waitForTheme( app, (snapshot) => snapshot.mode === "light" && Object.values(snapshot.inline).every((value) => value === ""), "predefined light theme to render without custom variables", ); assert.notEqual(selected.bodyBackground, ""); assert.notEqual(selected.bodyForeground, ""); await assertThemeAcrossNavigation(app, selected); await app.restart(); assert.equal((await app.invoke("get_app_settings")).theme, "light"); await app.waitFor( async () => JSON.stringify(await themeSnapshot(app)) === JSON.stringify(selected), { description: "predefined light theme after restart" }, ); }); }); test("preset and manually customized themes survive navigation and restart", async () => { await withApp("ui-theme-custom", async (app) => { await app.clickSelector('[aria-label="Settings"]'); await app.waitForText("Appearance"); await chooseSelectOption(app, "#theme-select", "Custom"); await chooseSelectOption(app, "#theme-preset-select", "Dracula"); await saveSettings(app); const presetSettings = await app.invoke("get_app_settings"); assert.equal(presetSettings.theme, "custom"); assert.deepEqual(presetSettings.custom_theme, DRACULA_THEME); const preset = await waitForTheme( app, (snapshot) => snapshot.mode === "dark" && themeVariablesEqual(snapshot.inline, DRACULA_THEME) && themeVariablesEqual(snapshot.resolved, DRACULA_THEME), "Dracula preset variables to render", ); await assertThemeAcrossNavigation(app, preset); await app.restart(); assert.deepEqual( (await app.invoke("get_app_settings")).custom_theme, DRACULA_THEME, ); await app.waitFor( async () => JSON.stringify(await themeSnapshot(app)) === JSON.stringify(preset), { description: "Dracula preset after restart" }, ); await app.clickSelector('[aria-label="Settings"]'); await app.waitForText("Appearance"); assert.equal( await app.execute( `return document.querySelector("#theme-select")?.textContent?.trim();`, ), "Custom", ); assert.equal( await app.execute( `return document.querySelector("#theme-preset-select")?.textContent?.trim();`, ), "Dracula", ); await dragBackgroundColorPicker(app); await saveSettings(app); const customizedSettings = await app.invoke("get_app_settings"); assert.equal(customizedSettings.theme, "custom"); assert.notEqual( customizedSettings.custom_theme["--background"].toLowerCase(), DRACULA_THEME["--background"], ); assert.deepEqual( Object.keys(customizedSettings.custom_theme).sort(), [...THEME_VARIABLES].sort(), ); const customized = await waitForTheme( app, (snapshot) => snapshot.mode === "dark" && themeVariablesEqual(snapshot.inline, customizedSettings.custom_theme) && themeVariablesEqual(snapshot.resolved, customizedSettings.custom_theme), "manually customized variables to render", ); assert.notEqual(customized.bodyBackground, preset.bodyBackground); await assertThemeAcrossNavigation(app, customized); await app.restart(); assert.deepEqual( (await app.invoke("get_app_settings")).custom_theme, customizedSettings.custom_theme, ); await app.waitFor( async () => JSON.stringify(await themeSnapshot(app)) === JSON.stringify(customized), { description: "manually customized theme after restart" }, ); }); }); test("a light custom preset keeps light component behavior after restart", async () => { await withApp("ui-theme-custom-light", async (app) => { await app.clickSelector('[aria-label="Settings"]'); await app.waitForText("Appearance"); await chooseSelectOption(app, "#theme-select", "Custom"); await chooseSelectOption(app, "#theme-preset-select", "Ayu Light"); await saveSettings(app); const selected = await waitForTheme( app, (snapshot) => snapshot.mode === "light" && themeVariablesEqual(snapshot.inline, AYU_LIGHT_THEME) && themeVariablesEqual(snapshot.resolved, AYU_LIGHT_THEME), "Ayu Light variables and light mode to render", ); await assertThemeAcrossNavigation(app, selected); await app.restart(); assert.deepEqual( (await app.invoke("get_app_settings")).custom_theme, AYU_LIGHT_THEME, ); await app.waitFor( async () => JSON.stringify(await themeSnapshot(app)) === JSON.stringify(selected), { description: "Ayu Light preset after restart" }, ); }); });