import { cleanup, render, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; import SwatchGroup, { type SwatchData } from "./SwatchGroup"; const SHAPES = [ "square", "soft", "rounded", "squircle", "circle", "diamond", "hexagon", ] as const; // Slider selection is rendered by its independent track/thumb contract, so it is // deliberately outside this per-swatch outline matrix. const OUTLINE_LAYOUTS = ["wrap", "grid", "flow", "pill"] as const; const OUTLINE_WIDTHS = [1, 3, 6] as const; const VALUES = ["color", "image", "number", "button"] as const; const CLIP_SHAPES = new Set(["diamond", "hexagon"]); const swatches: Record = { color: { type: "color", color: "#c00", label: "Color" }, image: { type: "image", color: "#fff", image: "data:image/svg+xml,%3Csvg/%3E", label: "Image", }, number: { type: "number", color: "#eee", label: "7" }, button: { type: "button", color: "", label: "Button" }, }; function createSelect(selected: string): HTMLSelectElement { const select = document.createElement("select"); select.name = "attribute_pa_test"; for (const value of ["", ...VALUES]) { const option = document.createElement("option"); option.value = value; option.textContent = value || "Choose"; select.append(option); } select.value = selected; document.body.append(select); return select; } function radio(container: HTMLElement, value: string): HTMLElement { const result = container.querySelector( `[data-b3-wvs-radio-value="${value}"]`, ); if (!result) throw new Error(`Missing ${value} radio`); return result; } function assertDimensions(element: HTMLElement, type: string, size: number) { if (type === "button") expect(element.style.minWidth).toBe(`${size}px`); else expect(element.style.width).toBe(`${size}px`); expect(element.style.height).toBe(`${size}px`); } const cases = OUTLINE_LAYOUTS.flatMap((layout) => SHAPES.flatMap((shape) => OUTLINE_WIDTHS.flatMap((width) => VALUES.map((selected) => ({ layout, shape, width, selected })), ), ), ); afterEach(() => { cleanup(); document.body.innerHTML = ""; }); describe("Free renderer selected outline matrix", () => { it.each( ["diamond", "hexagon"].flatMap((shape) => [ { color: "#123456", shadowColor: "#12345633", outerColor: "#1234561a" }, { color: "#abc", shadowColor: "#aabbcc33", outerColor: "#aabbcc1a" }, { color: "#33669980", shadowColor: "#3366991a", outerColor: "#3366990d", }, { color: "#33669900", shadowColor: "#33669900", outerColor: "#33669900", }, ].map((color) => ({ shape, ...color })), ), )( "paints the Background halo on the unclipped $shape wrapper with $color", async ({ shape, color, shadowColor, outerColor }) => { const select = createSelect("image"); const { container } = render( , ); await waitFor(() => expect(container.querySelectorAll('[role="radio"]')).toHaveLength(4), ); const selected = radio(container, "image"); expect(selected.style.boxShadow).not.toContain("0 0 0 3px"); expect(selected.parentElement?.style.boxShadow).toBe( `0 0 0 3px ${shadowColor}, 0 0 0 6px ${outerColor}`, ); expect(selected.style.clipPath).not.toBe(""); }, ); it.each(cases)( "$layout / $shape / $width px / selected $selected", async ({ layout, shape, width, selected }) => { const select = createSelect(selected); const { container } = render( , ); await waitFor(() => expect(container.querySelectorAll('[role="radio"]')).toHaveLength(4), ); if (layout === "pill") { expect( container.querySelector("[class~='bwvs:rounded-full']"), ).not.toBeNull(); expect( container.querySelector(".b3-wvs-clippath-outline-transition"), ).toBeNull(); } for (const value of VALUES) { const element = radio(container, value); const isSelected = value === selected; expect(element.getAttribute("aria-checked")).toBe(String(isSelected)); if (layout !== "pill") assertDimensions(element, value, 37); const underlay = element.parentElement?.querySelector( ".b3-wvs-clippath-outline-transition", ); const usesClipUnderlay = layout !== "pill" && CLIP_SHAPES.has(shape); if (isSelected && usesClipUnderlay) { expect(underlay?.style.inset).toBe(`-${width}px`); expect(element.style.boxShadow).not.toContain(`0 0 0 ${width}px`); } else if (isSelected) { expect(element.style.boxShadow).toContain(`0 0 0 ${width}px`); expect(underlay).toBeNull(); } else { expect(element.style.boxShadow).not.toContain(`0 0 0 ${width}px`); expect(underlay).toBeNull(); } } }, ); it.each( SHAPES.flatMap((shape) => OUTLINE_WIDTHS.flatMap((width) => VALUES.map((selected) => ({ shape, width, selected })), ), ), )( "uses only the slider thumb for stale $shape at $width px outline / selected $selected", async ({ shape, width, selected }) => { const select = createSelect(selected); const { container } = render( , ); await waitFor(() => expect(container.querySelectorAll('[role="radio"]')).toHaveLength(4), ); expect(radio(container, selected).getAttribute("aria-checked")).toBe( "true", ); for (const value of VALUES) { expect(radio(container, value).style.boxShadow).not.toContain( `0 0 0 ${width}px`, ); } expect( container.querySelector(".b3-wvs-clippath-outline-transition"), ).toBeNull(); const thumb = Array.from( container.querySelectorAll('[aria-hidden="true"]'), ).find((element) => element.style.backgroundColor === "rgb(18, 52, 86)"); expect(thumb).toBeDefined(); }, ); it.each([24, 60])( "honors the shared numeric dimension boundary %ipx", async (size) => { const select = createSelect("color"); const { container } = render( , ); await waitFor(() => expect(container.querySelectorAll('[role="radio"]')).toHaveLength(4), ); for (const value of VALUES) assertDimensions(radio(container, value), value, size); }, ); });