evOo-local/packages/ui-tokens/src/appearance.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

120 lines
4.5 KiB
TypeScript

import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
type ColorTokens,
cssVariableName,
darkTokens,
lightTokens,
normalizeAppearancePreference,
persistAppearancePreference,
renderTokensCss,
resolveAppearance,
resolveAppearancePreference,
scaleCssVariableName,
scaleTokens,
tokensForAppearance,
UI_APPEARANCE_STORAGE_KEY,
} from "./index.js";
describe("appearance preference", () => {
it("defaults unknown values to system", () => {
expect(normalizeAppearancePreference(null)).toBe("system");
expect(normalizeAppearancePreference("nope")).toBe("system");
expect(normalizeAppearancePreference("light")).toBe("light");
});
it("resolves system from the platform scheme", () => {
expect(resolveAppearance("system", "light")).toBe("light");
expect(resolveAppearance("system", "dark")).toBe("dark");
expect(resolveAppearance("dark", "light")).toBe("dark");
});
it("reads and writes storage", () => {
const store = new Map<string, string>();
const storage = {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => {
store.set(key, value);
},
};
expect(resolveAppearancePreference({ storage })).toBe("system");
persistAppearancePreference("light", storage);
expect(store.get(UI_APPEARANCE_STORAGE_KEY)).toBe("light");
expect(resolveAppearancePreference({ storage })).toBe("light");
});
it("returns distinct light and dark token sets", () => {
expect(tokensForAppearance("dark")).toBe(darkTokens);
expect(tokensForAppearance("light")).toBe(lightTokens);
for (const key of Object.keys(darkTokens) as (keyof ColorTokens)[]) {
if (key === "destructiveForeground") continue;
expect(darkTokens[key], key).not.toBe(lightTokens[key]);
}
});
it("keeps user message surfaces muted, not cream invert", () => {
const dark = tokensForAppearance("dark");
const light = tokensForAppearance("light");
expect(dark.secondary).not.toBe(dark.primary);
expect(light.secondary).not.toBe(light.primary);
expect(dark.secondaryForeground).not.toBe(dark.primaryForeground);
});
it("separates user bubbles from bot bubbles and the sidebar from the app", () => {
const dark = tokensForAppearance("dark");
const light = tokensForAppearance("light");
expect(dark.chatUser).not.toBe(dark.muted);
expect(light.chatUser).not.toBe(light.muted);
expect(dark.sidebar).not.toBe(dark.background);
expect(light.sidebar).not.toBe(light.background);
});
it("tolerates a throwing localStorage getter", () => {
const desc = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
get() {
throw new Error("blocked");
},
});
try {
expect(resolveAppearancePreference()).toBe("system");
persistAppearancePreference("light");
expect(resolveAppearancePreference()).toBe("system");
} finally {
if (desc) Object.defineProperty(globalThis, "localStorage", desc);
else delete (globalThis as { localStorage?: Storage }).localStorage;
}
});
});
describe("tokens.css", () => {
it("derives kebab-case variable names", () => {
expect(cssVariableName("background")).toBe("--background");
expect(cssVariableName("mutedForeground")).toBe("--muted-foreground");
expect(cssVariableName("sidebarAccentForeground")).toBe("--sidebar-accent-foreground");
});
it("is generated from the TS palette", () => {
const onDisk = readFileSync(fileURLToPath(new URL("./tokens.css", import.meta.url)), "utf8");
expect(onDisk).toBe(renderTokensCss());
});
it("scopes light and dark under data-theme", () => {
const css = renderTokensCss();
expect(css).toContain('[data-theme="dark"] {\n color-scheme: dark;');
expect(css).toContain('[data-theme="light"] {\n color-scheme: light;');
expect(css).toContain(`--background: ${lightTokens.background.toLowerCase()};`);
expect(css).toContain(`--background: ${darkTokens.background.toLowerCase()};`);
});
it("emits shared UI scale tokens in both themes", () => {
const css = renderTokensCss();
expect(scaleCssVariableName("fontSizeBody")).toBe("--font-size-body");
expect(css).toContain(`--font-size-body: ${scaleTokens.fontSizeBody};`);
expect(css).toContain(`--control-height-lg: ${scaleTokens.controlHeightLg};`);
expect(css).toContain(`--icon-md: ${scaleTokens.iconMd};`);
});
});