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.
120 lines
4.5 KiB
TypeScript
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};`);
|
|
});
|
|
});
|