// @vitest-environment happy-dom
import { act, forwardRef, type ComponentProps } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ToolkitProvider } from "../provider.js";
import { Button } from "../ui/button.js";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu.js";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
import { ActionButton, IconButton } from "./components.js";
import { defaultDesignSystemComponents } from "./default-adapter.js";
import { defineDesignSystem } from "./definition.js";
import { defineTheme } from "./theme.js";
import { DESIGN_SYSTEM_CONTRACT_VERSION } from "./types.js";
describe("design-system contract", () => {
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("locks the seventeen semantic component names", () => {
expect(DESIGN_SYSTEM_CONTRACT_VERSION).toBe(1);
expect(Object.keys(defaultDesignSystemComponents).sort()).toEqual(
[
"ActionButton",
"Avatar",
"Checkbox",
"Dialog",
"IconButton",
"Menu",
"Picker",
"Popover",
"Skeleton",
"Spinner",
"Status",
"Surface",
"Switch",
"Tabs",
"TextArea",
"TextField",
"Tooltip",
].sort(),
);
});
it("preserves typed design-system and theme definitions", () => {
const theme = defineTheme({
colors: {
light: { primary: "oklch(55% 0.2 260)" },
},
radius: "0.75rem",
});
const definition = defineDesignSystem({
name: "Acme",
theme,
components: {},
});
expect(definition).toEqual({ name: "Acme", theme, components: {} });
});
it("prefers ActionButton over the legacy Button override", () => {
const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
const LegacyButton = () => ;
const CustomActionButton = () => ;
act(() => {
root.render(
,
);
});
expect(container.querySelector("[data-adapter=semantic]")).not.toBeNull();
expect(container.querySelector("[data-adapter=legacy]")).toBeNull();
expect(warning).toHaveBeenCalledOnce();
});
it("warns when nested providers combine the effective ActionButton APIs", () => {
const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
const LegacyButton = () => ;
const CustomActionButton = () => ;
act(() => {
root.render(
,
);
});
expect(container.querySelector("[data-adapter=semantic]")).not.toBeNull();
expect(warning).toHaveBeenCalledOnce();
});
it("forwards semantic intent independently from the legacy visual variant", () => {
const received = vi.fn();
const CustomActionButton = (props: ComponentProps) => {
received(props.intent, props.emphasis);
return ;
};
act(() => {
root.render(
,
);
});
expect(received).toHaveBeenCalledWith("danger", "outline");
});
it("uses legacy Button as the lowest-precedence ActionButton adapter", () => {
const LegacyButton = (props: ComponentProps<"button">) => (
);
act(() => {
root.render(
Save
,
);
});
expect(container.querySelector("[data-adapter=legacy]")?.textContent).toBe(
"Save",
);
});
it.each(["pointer", "Enter", "Space", "ArrowDown"])(
"preserves Radix menu trigger behavior through a legacy Button override with %s activation",
async (activation) => {
const onOpenChange = vi.fn();
let buttonRef: HTMLButtonElement | null = null;
const LegacyButton = forwardRef<
HTMLButtonElement,
ComponentProps
>(
(
{
asChild: _asChild,
emphasis: _emphasis,
intent: _intent,
size: _size,
variant: _variant,
...props
},
ref,
) => ,
);
await act(async () => {
root.render(
First item
,
);
});
const trigger = container.querySelector("button");
expect(trigger).not.toBeNull();
expect(buttonRef).toBe(trigger);
expect(trigger?.getAttribute("aria-haspopup")).toBe("menu");
expect(trigger?.getAttribute("aria-expanded")).toBe("false");
expect(trigger?.dataset.state).toBe("closed");
await act(async () => {
trigger?.focus();
if (activation === "pointer") {
trigger?.dispatchEvent(
new PointerEvent("pointerdown", {
bubbles: true,
button: 0,
pointerType: "mouse",
}),
);
} else {
const key = activation === "Space" ? " " : activation;
trigger?.dispatchEvent(
new KeyboardEvent("keydown", { bubbles: true, key }),
);
}
await Promise.resolve();
});
expect(onOpenChange).toHaveBeenCalledTimes(1);
expect(onOpenChange).toHaveBeenLastCalledWith(true);
expect(trigger?.getAttribute("aria-expanded")).toBe("true");
expect(trigger?.dataset.state).toBe("open");
expect(document.querySelector("[role=menu]")).not.toBeNull();
await act(async () => {
document.activeElement?.dispatchEvent(
new KeyboardEvent("keydown", { bubbles: true, key: "Escape" }),
);
await Promise.resolve();
});
expect(onOpenChange).toHaveBeenCalledTimes(2);
expect(onOpenChange).toHaveBeenLastCalledWith(false);
expect(trigger?.getAttribute("aria-expanded")).toBe("false");
expect(trigger?.dataset.state).toBe("closed");
},
);
it("forwards a native ref passed to ActionButton to the real DOM button node", () => {
let node: HTMLButtonElement | null = null;
act(() => {
root.render(
{
node = el;
}}
>
Save
,
);
});
const button = container.querySelector("button");
expect(button).not.toBeNull();
expect(node).toBe(button);
});
it("resolves a Radix asChild Popover trigger's ref to the real IconButton DOM node", async () => {
// Radix positions a popover by measuring the DOM node its `asChild`
// Slot clones a ref onto. If that ref is dropped, Radix has nothing to
// measure and falls back to an unpositioned, off-screen placement.
let node: HTMLButtonElement | null = null;
await act(async () => {
root.render(
}
ref={(el) => {
node = el;
}}
/>
Content
,
);
});
const button = container.querySelector("button");
expect(button).not.toBeNull();
expect(node).toBe(button);
});
it.each([
["IconButton", } />],
["ActionButton", Manage],
])(
"opens a Radix asChild Popover trigger built on %s when clicked",
async (_name, trigger) => {
// Radix passes its toggle handler down as `onClick`. An adapter that
// spreads props and then sets its own `onClick` silently drops it, so
// the trigger renders correctly and never opens.
await act(async () => {
root.render(
{trigger}
Content
,
);
});
const button = container.querySelector("button");
await act(async () => {
button?.click();
});
expect(button?.dataset.state).toBe("open");
},
);
it("isolates a broken customer component and renders the default control", () => {
vi.spyOn(console, "error").mockImplementation(() => {});
const BrokenActionButton = () => {
throw new Error("broken adapter");
};
act(() => {
root.render(
Fallback action
,
);
});
expect(container.querySelector("button")?.textContent).toBe(
"Fallback action",
);
expect(console.error).toHaveBeenCalled();
});
});