Files
minhtrannhatandClaude Opus 5.5 c90c0ddf9d Add the Nord theme and make it the default
The theme picker shows NORD, MOCHA and LATTE. Nord is the base theme
in the CSS, the browser bar color and the PWA manifest. The startup
script applies the theme before the first paint.

A device that saved Mocha changes to Nord one time. After that, a user
can select Mocha again and the app keeps it.

The Nord red #bf616a has only 3.05:1 contrast on the background, so the
danger color is the lighter #dc8890 (4.74:1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 15:10:12 -05:00

75 lines
3.2 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { THEMES, THEME_COLORS, isTheme } from "../src/client/theme.ts";
const html = await Bun.file("src/client/index.html").text();
const css = await Bun.file("src/client/styles.css").text();
const manifest = await Bun.file("src/client/manifest.webmanifest").json();
/** Runs the inline startup script from index.html with a fake browser. */
function startup(saved: Record<string, string>) {
const script = html.match(/<script>([\s\S]*?)<\/script>/)![1]!;
const storage = new Map(Object.entries(saved));
const root = { dataset: {} as Record<string, string> };
let themeColor = "";
const localStorage = {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => { storage.set(key, value); },
};
const document = {
documentElement: root,
querySelector: () => ({ setAttribute: (_: string, value: string) => { themeColor = value; } }),
};
new Function("localStorage", "document", script)(localStorage, document);
return { theme: root.dataset.theme, themeColor, saved: storage.get("timelog-theme") };
}
describe("startup theme", () => {
test("a device with no saved theme gets Nord", () => {
expect(startup({})).toMatchObject({ theme: "nord", themeColor: THEME_COLORS.nord });
});
test("a device that saved Mocha changes to Nord one time", () => {
expect(startup({ "timelog-theme": "mocha" })).toEqual({ theme: "nord", themeColor: THEME_COLORS.nord, saved: "nord" });
});
test("Mocha stays when the user selects it after the change to Nord", () => {
expect(startup({ "timelog-theme": "mocha", "timelog-theme-nord": "1" }))
.toMatchObject({ theme: "mocha", themeColor: THEME_COLORS.mocha });
});
test("Latte stays Latte", () => {
expect(startup({ "timelog-theme": "latte" })).toMatchObject({ theme: "latte", themeColor: THEME_COLORS.latte });
});
test("an unknown saved value gets Nord", () => {
expect(startup({ "timelog-theme": "solarized" })).toMatchObject({ theme: "nord", themeColor: THEME_COLORS.nord });
});
});
describe("theme colors", () => {
test("Nord is first in the picker", () => {
expect(THEMES[0]).toBe("nord");
expect(isTheme("nord") && isTheme("mocha") && isTheme("latte")).toBe(true);
expect(isTheme("solarized")).toBe(false);
});
test("the startup script uses the same colors as the app", () => {
for (const theme of THEMES) expect(html).toContain(`${theme}: "${THEME_COLORS[theme]}"`);
});
test("the CSS has Nord as the base and a block for each other theme", () => {
const base = css.match(/:root \{([\s\S]*?)\}/)![1]!;
expect(base).toContain(`--bg: ${THEME_COLORS.nord};`);
for (const theme of THEMES.filter(t => t !== "nord")) {
const block = css.match(new RegExp(`:root\\[data-theme="${theme}"\\] \\{([\\s\\S]*?)\\}`))?.[1];
expect(block).toContain(`--bg: ${THEME_COLORS[theme]};`);
}
});
test("the first paint and the installed app use Nord", () => {
expect(html).toContain(`<meta name="theme-color" content="${THEME_COLORS.nord}" />`);
expect(manifest.theme_color).toBe(THEME_COLORS.nord);
expect(manifest.background_color).toBe(THEME_COLORS.nord);
});
});