Files
WRNexusJS/packages/styles/test/theme.test.ts
T
ClintchizandClaude Opus 5.5 1179fc2810 chore(release): redesign UI tokens and publish styles 0.8.26, ui 0.8.86
Visual-only redesign ("instrument panel"): cool graphite neutrals for light
and dark, layered hairline elevation, larger radii, a mono token for
eyebrows/data, keycap-style solid and secondary buttons, recessed input
wells and tab tracks, a stronger dialog, and a theme-driven focus outline.
No props, outputs or behaviour change.

Also records the already-published csr 0.8.42, styles 0.8.25 and ui 0.8.85
changes (optional chaining/nullish in the reactive runtime, shared
theme/accent cookie sync, BackToTop hidden attribute).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:00:27 +05:30

121 lines
4.6 KiB
TypeScript

import { test, expect } from "bun:test";
import {
THEME_PALETTE_NAMES,
THEME_PALETTES,
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderActiveThemeCss,
activeThemeCssHref,
renderThemeRuntime,
} from "../src/index.ts";
test("ships eight complete semantic color palettes", () => {
expect(THEME_PALETTE_NAMES).toHaveLength(8);
for (const name of THEME_PALETTE_NAMES) {
expect(Object.keys(THEME_PALETTES[name])).toEqual([
"primary",
"primaryHover",
"primaryContrast",
"secondary",
"secondaryHover",
"secondaryContrast",
"info",
"success",
"warning",
"danger",
]);
}
});
test("named palettes populate semantic tokens in every theme", () => {
const t = resolveThemeConfig({ palette: "violet" });
for (const name of t.names) {
expect(t.themes[name]["color-primary"]).toBe(THEME_PALETTES.violet.primary);
expect(t.themes[name]["color-secondary"]).toBe(THEME_PALETTES.violet.secondary);
expect(t.themes[name]["color-info"]).toBe(THEME_PALETTES.violet.info);
expect(t.themes[name]["color-error"]).toBe(THEME_PALETTES.violet.danger);
}
});
test("custom palettes require every semantic color", () => {
expect(() => resolveThemeConfig({ palette: { primary: "#000" } as never })).toThrow(
"primaryHover, primaryContrast, secondary",
);
expect(() => resolveThemeConfig({ palette: "unknown" as never })).toThrow(
"Unknown theme palette",
);
});
test("resolveThemeConfig merges user tokens over built-in light/dark", () => {
const t = resolveThemeConfig({ default: "dark", themes: { dark: { "color-primary": "#abc" } } });
expect(t.default).toBe("dark");
expect(t.names).toContain("light");
expect(t.names).toContain("dark");
expect(t.themes.dark["color-primary"]).toBe("#abc"); // overridden
expect(t.themes.dark["color-bg"]).toBeDefined(); // built-in kept
});
test("built-in themes use Plus Jakarta Sans and layered charcoal dark surfaces", () => {
const t = resolveThemeConfig();
expect(t.themes.light["font-sans"]).toBe(
'"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
);
expect(t.themes.dark["font-sans"]).toBe('"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif');
expect(t.themes.dark["color-bg"]).toBe("#0c0d10");
expect(t.themes.dark["color-surface"]).toBe("#15171c");
expect(t.themes.dark["color-surface-2"]).toBe("#1c1f25");
});
test("resolveThemeName validates against configured names", () => {
const t = resolveThemeConfig();
expect(resolveThemeName("light", t)).toBe("light");
expect(resolveThemeName("nonsense", t)).toBe(t.default);
expect(resolveThemeName(undefined, t)).toBe(t.default);
});
test("renderThemeCss emits :root + per-theme blocks and --wrn-* vars", () => {
const t = resolveThemeConfig({
default: "dark",
themes: { dark: { "color-primary": "#6c8cff" } },
});
const css = renderThemeCss(t);
expect(css).toContain(":root{");
expect(css).toContain('[data-theme="dark"]{');
expect(css).toContain("--wrn-color-primary:#6c8cff");
expect(css).toContain("color-scheme:dark"); // reserved token → native property
expect(css).toContain("font-family:var(--wrn-font-sans");
});
test("active theme CSS contains only the selected theme and accent", () => {
const theme = resolveThemeConfig({ default: "dark", palette: "rose" });
const full = renderThemeCss(theme);
const active = renderActiveThemeCss(theme, "dark", "rose");
expect(active).toContain(":root{");
expect(active).not.toContain('[data-theme="light"]');
expect(active.length).toBeLessThan(full.length / 4);
expect(activeThemeCssHref("dark", "rose")).toBe("/__wrnexus/theme/dark/rose.css");
expect(() => renderActiveThemeCss(theme, "missing")).toThrow("Unknown theme");
});
test("renderThemeRuntime bakes the theme names for cycling", () => {
const t = resolveThemeConfig();
const js = renderThemeRuntime(t);
expect(js).toContain("data-wrn-theme-toggle");
expect(js).toContain(JSON.stringify(t.names));
expect(js).toContain('addEventListener("wrnexus:navigated"');
});
test("active stylesheets preserve configured geometry and elevation without a later reset", () => {
const theme = resolveThemeConfig({
themes: { dark: { "radius-sm": "3px", "radius-lg": "9px", "shadow-1": "none" } },
});
const css = renderActiveThemeCss(theme, "dark");
expect(css.match(/--wrn-radius-sm:/g)).toHaveLength(1);
expect(css.match(/--wrn-radius-lg:/g)).toHaveLength(1);
expect(css.match(/--wrn-shadow-1:/g)).toHaveLength(1);
expect(css).toContain("--wrn-radius-sm:3px");
expect(css).toContain("--wrn-radius-lg:9px");
expect(css).toContain("--wrn-shadow-1:none");
});