diff --git a/internal/gui/assets/index.html b/internal/gui/assets/index.html index a9310813..23e920e1 100644 --- a/internal/gui/assets/index.html +++ b/internal/gui/assets/index.html @@ -6,6 +6,7 @@ magpie + diff --git a/internal/gui/assets/theme.js b/internal/gui/assets/theme.js new file mode 100644 index 00000000..d3e2b782 --- /dev/null +++ b/internal/gui/assets/theme.js @@ -0,0 +1,11 @@ +// The saved theme (boot.js), or one the page's address asks for, on the page +// before its styles first apply: set only once app.js ran, at the foot of the +// page, a window kept dark under a light system was first drawn light, and +// the header's buttons faded from light to dark as it loaded. app.js sets it +// again; this one is for the first paint. +(function () { + var asked = new URLSearchParams(location.search).get("theme"); + var saved = window.bootPrefs && window.bootPrefs.theme; + var theme = asked || (saved && saved !== "system" ? saved : ""); + if (theme) document.documentElement.dataset.theme = theme; +})(); diff --git a/internal/gui/tests/api-key-theme.test.cjs b/internal/gui/tests/api-key-theme.test.cjs index 554758d6..59a9cde8 100644 --- a/internal/gui/tests/api-key-theme.test.cjs +++ b/internal/gui/tests/api-key-theme.test.cjs @@ -12,7 +12,7 @@ async function palette(page, selectors) { const hex = root.getPropertyValue(name).trim(); return `rgb(${[1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(", ")})`; }; - const colors = Object.fromEntries(["bg", "card", "card-2", "fg", "fg-2", "muted", "pill", "line", "accent", "accent-fg"] + const colors = Object.fromEntries(["bg", "card", "card-2", "fg", "fg-2", "muted", "ctl-fg", "pill", "line", "accent", "accent-fg"] .map((name) => [name, rgb("--" + name)])); const elements = Object.fromEntries(Object.entries(selectors).map(([name, selector]) => { const s = getComputedStyle(document.querySelector(selector)); @@ -60,7 +60,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", assert.equal(input.bg, c.card, "key inputs must not use the browser's default background"); assert.equal(input.color, c.fg); } - assert.equal(e.rotate.color, c.muted); + assert.equal(e.rotate.color, c["ctl-fg"]); // a text button, in a control's quiet text (#477) assert.equal(e.keyPick.bg, "rgba(0, 0, 0, 0)"); assert.equal(e.create.bg, c.accent); assert.equal(e.create.color, c["accent-fg"]); diff --git a/internal/gui/tests/old-webkit.test.cjs b/internal/gui/tests/old-webkit.test.cjs index 8cabee9f..56ea56cf 100644 --- a/internal/gui/tests/old-webkit.test.cjs +++ b/internal/gui/tests/old-webkit.test.cjs @@ -132,10 +132,12 @@ test("no built-in newer than Safari 15.0 unless compat.js fills it in", async () test("compat.js is the first of the page's own scripts", async () => { const html = await read("index.html"); - // boot.js and omarchy.js run in the head, before the first paint: - // omarchy.js does nothing off Omarchy, whose WebKitGTK is a current one + // boot.js, theme.js and omarchy.js run in the head, before the first + // paint: theme.js is ES5 with nothing newer than Safari 10.1's + // URLSearchParams, and omarchy.js does nothing off Omarchy, whose + // WebKitGTK is a current one const all = [...html.matchAll(/ m[1]); - const srcs = all.filter((s) => s !== "boot.js" && s !== "omarchy.js"); + const srcs = all.filter((s) => s !== "boot.js" && s !== "theme.js" && s !== "omarchy.js"); assert.equal(srcs[0], "compat.js", "loaded first: " + srcs.join(", ")); for (const f of await scripts()) assert(all.includes(f), f + " is not loaded by index.html"); }); diff --git a/internal/gui/tests/seg-contrast.test.cjs b/internal/gui/tests/seg-contrast.test.cjs index d209900f..203fb0e3 100644 --- a/internal/gui/tests/seg-contrast.test.cjs +++ b/internal/gui/tests/seg-contrast.test.cjs @@ -8,7 +8,11 @@ // browser computed them, each background laid over the ones under it: the // other views and a text button on the page, a card and the inset editor // read at 4.5:1 with some margin to spare (4.7), the view shown on its thumb -// too, and the thumb is told from the track by more than it was. Chromium +// too, and the thumb is told from the track by more than it was. They are +// read once the page's transitions are over; and the page is first drawn in +// its theme, before app.js runs: a window kept dark under a light system was +// first drawn light, its header's buttons fading to dark as they were read, +// so they measured mid-way (3.96:1 for the view shown). Chromium // and WebKit, English and Chinese; no backend, the API is faked here. const assert = require("node:assert/strict"); const fs = require("node:fs/promises"); @@ -18,9 +22,10 @@ const { chromium, webkit } = require("playwright"); const assets = path.resolve(__dirname, "../assets"); -function server(lang, theme) { +function server(lang, theme, gate) { return async (route) => { const url = new URL(route.request().url()); + if (url.pathname === "/app.js") await gate; if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"${theme}",web:false};` }); if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" }); if (url.pathname === "/api/state") return route.fulfill({ json: { agents: [], profiles: [], settings: { lang, theme } } }); @@ -101,10 +106,20 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", const ctx = await browser.newContext({ viewport: { width: 1000, height: 640 }, colorScheme: scheme }); const page = await ctx.newPage(); page.on("pageerror", (e) => errors.push(e.message)); - await page.route("**/*", server(lang, theme)); - await page.goto("http://magpie.test/"); + let release; + const gate = new Promise((r) => { release = r; }); + await page.route("**/*", server(lang, theme, gate)); + // the page as first drawn, app.js held back: already in its theme + const loaded = page.goto("http://magpie.test/"); + await page.waitForSelector("#nav", { state: "attached" }); + const first = await page.evaluate(() => getComputedStyle(document.body).backgroundColor); + release(); + await loaded; await page.waitForSelector("#nav button.on"); await page.waitForFunction(() => document.querySelector("#nav .thumb")?.getBoundingClientRect().width > 0); + assert.equal(first, await page.evaluate(() => getComputedStyle(document.body).backgroundColor), "the page was first drawn in another theme"); + // what is drawn once it has settled, not a colour on its way + await page.evaluate(() => Promise.all(document.getAnimations().filter((a) => a.transitionProperty).map((a) => a.finished.catch(() => {})))); const r = await ratios(page), at = JSON.stringify(r); for (const [what, v] of Object.entries(r)) { if (what === "thumb against the track") continue;