import { type ReactNode, createElement } from "react";
import { renderToString } from "react-dom/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock Tamagui dependencies to avoid needing a full Tamagui config
vi.mock("tamagui", () => ({
Theme: ({ name, children }: { name: string; children: ReactNode }) =>
createElement("div", { "data-tamagui-theme": name }, children),
}));
import type { ThemeName } from "@tamagui/web";
import { Preset } from "./Preset";
import { type PresetContextValue, usePresetContext } from "./PresetContext";
import { defaultKnobs } from "./knobs";
import type { Preset as PresetType } from "./preset.types";
import { clearPresets, registerPreset } from "./shared";
// ---------------------------------------------------------------------------
// Test fixtures
// ---------------------------------------------------------------------------
const boldPreset: PresetType = {
theme: "blue" as ThemeName,
knobs: {
...defaultKnobs,
fillStyle: "filled",
borderRadius: "large",
elevation: "large",
},
intents: {},
tints: ["blue" as ThemeName, "red" as ThemeName],
};
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
function createPresetContextCapture() {
let captured: PresetContextValue | null = null;
function Capture() {
captured = usePresetContext();
return null;
}
return {
Capture,
get result() {
return captured;
},
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("Preset component", () => {
beforeEach(() => {
clearPresets();
});
// -------------------------------------------------------------------------
// 1. Default knobs
// -------------------------------------------------------------------------
it("provides default knobs when no preset is specified", () => {
const capture = createPresetContextCapture();
renderToString(createElement(Preset, null, createElement(capture.Capture)));
expect(capture.result?.preset.knobs).toEqual(defaultKnobs);
});
// -------------------------------------------------------------------------
// 2. Overrides merge on top of defaults
// -------------------------------------------------------------------------
it("merges overrides on top of defaults", () => {
const capture = createPresetContextCapture();
renderToString(
createElement(
Preset,
{ overrides: { borderRadius: "full" } },
createElement(capture.Capture),
),
);
expect(capture.result?.preset.knobs.borderRadius).toBe("full");
expect(capture.result?.preset.knobs.fillStyle).toBe(defaultKnobs.fillStyle);
});
// -------------------------------------------------------------------------
// 3. Cascading — child overrides merge on top of parent
// -------------------------------------------------------------------------
it("cascades child overrides on top of parent", () => {
const capture = createPresetContextCapture();
renderToString(
createElement(
Preset,
{ overrides: { borderRadius: "full" } },
createElement(
Preset,
{ overrides: { elevation: "large" } },
createElement(capture.Capture),
),
),
);
expect(capture.result?.preset.knobs.borderRadius).toBe("full");
expect(capture.result?.preset.knobs.elevation).toBe("large");
});
// -------------------------------------------------------------------------
// 4. cascade=false replaces parent entirely
// -------------------------------------------------------------------------
it("cascade={false} replaces parent entirely", () => {
const capture = createPresetContextCapture();
renderToString(
createElement(
Preset,
{ overrides: { borderRadius: "full" } },
createElement(
Preset,
{ cascade: false, overrides: { elevation: "large" } },
createElement(capture.Capture),
),
),
);
expect(capture.result?.preset.knobs.borderRadius).toBe(defaultKnobs.borderRadius);
expect(capture.result?.preset.knobs.elevation).toBe("large");
});
// -------------------------------------------------------------------------
// 5. Named preset resolution
// -------------------------------------------------------------------------
it("resolves a named preset from the registry", () => {
registerPreset("bold", boldPreset);
const capture = createPresetContextCapture();
renderToString(createElement(Preset, { preset: "bold" }, createElement(capture.Capture)));
expect(capture.result?.preset.knobs.borderRadius).toBe("large");
expect(capture.result?.preset.knobs.elevation).toBe("large");
expect(capture.result?.preset.theme).toBe("blue");
});
// -------------------------------------------------------------------------
// 6. Named preset with overrides
// -------------------------------------------------------------------------
it("applies overrides on top of a named preset", () => {
registerPreset("bold", boldPreset);
const capture = createPresetContextCapture();
renderToString(
createElement(
Preset,
{ preset: "bold", overrides: { borderRadius: "none" } },
createElement(capture.Capture),
),
);
expect(capture.result?.preset.knobs.borderRadius).toBe("none");
expect(capture.result?.preset.knobs.elevation).toBe("large");
});
// -------------------------------------------------------------------------
// 7. Tamagui wrapping
// -------------------------------------------------------------------------
it("wraps children with Tamagui when theme prop is set", () => {
const capture = createPresetContextCapture();
const html = renderToString(
createElement(Preset, { theme: "blue" as ThemeName }, createElement(capture.Capture)),
);
expect(html).toContain('data-tamagui-theme="blue"');
});
it("does not wrap with Tamagui when theme prop is absent", () => {
const capture = createPresetContextCapture();
const html = renderToString(createElement(Preset, null, createElement(capture.Capture)));
expect(html).not.toContain("data-tamagui-theme");
});
// -------------------------------------------------------------------------
// 8. Cascading named preset inside parent
// -------------------------------------------------------------------------
it("cascades a named preset on top of parent overrides", () => {
registerPreset("bold", boldPreset);
const capture = createPresetContextCapture();
renderToString(
createElement(
Preset,
{ overrides: { fillStyle: "outlined" } },
createElement(Preset, { preset: "bold" }, createElement(capture.Capture)),
),
);
// Named preset replaces base knobs in cascade mode
expect(capture.result?.preset.knobs.borderRadius).toBe("large");
// Parent override is carried forward
expect(capture.result?.preset.knobs.fillStyle).toBe("outlined");
});
// -------------------------------------------------------------------------
// 9. Unknown preset name falls back to defaults
// -------------------------------------------------------------------------
it("falls back to default knobs when preset name is not registered", () => {
const capture = createPresetContextCapture();
renderToString(
createElement(Preset, { preset: "nonexistent" }, createElement(capture.Capture)),
);
expect(capture.result?.preset.knobs).toEqual(defaultKnobs);
});
// -------------------------------------------------------------------------
// 10. scope prop does not exist on the type
// -------------------------------------------------------------------------
it("does not accept scope prop", () => {
// TypeScript-level check: `scope` should not be part of PresetProps.
// If this file compiles cleanly, the type check passes.
// We verify at runtime that passing an unknown prop doesn't break anything.
const capture = createPresetContextCapture();
renderToString(
createElement(
Preset,
// @ts-expect-error — scope is intentionally removed from PresetProps
{ scope: "Form" },
createElement(capture.Capture),
),
);
expect(capture.result?.preset.knobs).toEqual(defaultKnobs);
});
});