1import { HTML_RENDER_DEFAULT_FONTS, type HtmlRenderTheme } from "@t3tools/shared/htmlRender";
2import { BUILT_IN_THEME_IDS } from "@t3tools/shared/themePalettes";
3import { act, createElement, useEffect } from "react";
4import { create, type ReactTestRenderer } from "react-test-renderer";
5import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
6
7import {
8 getStandardThemeColors,
9 setEnvironmentThemes,
10 type ThemeAppearance,
11 type ThemeHalves,
12 type ThemePreference,
13} from "../themePalette";
14import { useHtmlRenderTheme } from "./useHtmlRenderTheme";
15
16const selection = vi.hoisted(() => ({
17 theme: "ocean" as ThemePreference,
18 resolvedTheme: "dark" as ThemeAppearance,
19 themeHalves: null as ThemeHalves | null,
20}));
21
22vi.mock("./useTheme", () => ({ useTheme: () => selection }));
23vi.mock("./useSettings", () => ({
24 useClientSettings: (
25 select: (settings: { fontFamilySans: string; fontFamilyCode: string }) => string,
26 ) => select({ fontFamilySans: "", fontFamilyCode: "" }),
27}));
28
29let renderer: ReactTestRenderer | undefined;
30
31beforeEach(() => {
32 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
33 selection.themeHalves = null;
34});
35
36afterEach(() => {
37 act(() => renderer?.unmount());
38 renderer = undefined;
39 setEnvironmentThemes([]);
40 vi.unstubAllGlobals();
41});
42
43/** Returns the committed hook result after React has flushed mount effects. */
44function readRenderTheme(): HtmlRenderTheme {
45 let theme: HtmlRenderTheme | undefined;
46 /** Captures the theme in an effect to avoid mutating test state during render. */
47 function Probe() {
48 const resolved = useHtmlRenderTheme();
49 useEffect(() => {
50 theme = resolved;
51 }, [resolved]);
52 return null;
53 }
54 act(() => {
55 renderer = create(createElement(Probe));
56 });
57 if (!theme) throw new Error("The HTML render theme was not resolved.");
58 return theme;
59}
60
61describe("useHtmlRenderTheme", () => {
62 for (const appearance of ["light", "dark"] as const) {
63 it.each([appearance, ...BUILT_IN_THEME_IDS])(
64 `injects hex colors for %s in ${appearance} mode`,
65 (preference) => {
66 selection.theme = preference;
67 selection.resolvedTheme = appearance;
68
69 const theme = readRenderTheme();
70
71 expect(theme.appearance).toBe(appearance);
72 for (const [variable, value] of Object.entries(theme.variables)) {
73 if (variable === "--radius" || variable.startsWith("--font-")) continue;
74 expect(value, variable).toMatch(/^#[\da-f]{6}(?:[\da-f]{2})?$/i);
75 }
76 expect(theme.variables["--font-sans"]).toBe(HTML_RENDER_DEFAULT_FONTS.sans);
77 expect(theme.variables["--font-mono"]).toBe(HTML_RENDER_DEFAULT_FONTS.mono);
78 expect(theme.variables["--radius"]).toBe("0.625rem");
79 },
80 );
81 }
82
83 it("preserves alpha when converting a published palette", () => {
84 setEnvironmentThemes([
85 {
86 id: "transparent",
87 label: "Transparent",
88 appearance: "dark",
89 colors: { ...getStandardThemeColors("dark"), accent: "oklch(0.5 0 0 / 0.5)" },
90 },
91 ]);
92 selection.theme = "transparent";
93 selection.resolvedTheme = "dark";
94
95 const theme = readRenderTheme();
96
97 expect(theme.variables["--accent"]).toBe("#63636380");
98 expect(theme.variables["--chart-1"]).toBe("#63636380");
99 });
100});