import { cleanup, render, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; import SwatchGroup, { type SwatchData } from "./SwatchGroup"; /** * Fixed swatch dimensions are authoritative (free renderer). * * The swatch box is `box-sizing: border-box` with an inline width/height, but * padding never shrinks in CSS: a 24px box with the default 8/16 padding used * to paint 32x24, and 48px padding everywhere painted 96x96. The painted * padding is now fitted inside the box (borders reserved) while the settings * themselves stay untouched. jsdom has no layout, so these assert the inline * declarations; the browser agent's dimension-padding-boundaries spec checks * the used CSS dimensions. */ const color: Record = { red: { type: "color", color: "#ff0000", label: "Red" }, blue: { type: "color", color: "#0000ff", label: "Blue" }, }; const image: Record = { red: { type: "image", color: "", label: "Red", image: "https://x/r.png" }, blue: { type: "image", color: "", label: "Blue", image: "https://x/b.png" }, }; const number: Record = { red: { type: "number", color: "", label: "10", number: "10" }, blue: { type: "number", color: "", label: "12", number: "12" }, }; const button: Record = { red: { type: "button", color: "", label: "Extra Large Label" }, blue: { type: "button", color: "", label: "Small" }, }; function createSelect(): HTMLSelectElement { const select = document.createElement("select"); select.id = "attribute_pa_color"; select.name = "attribute_pa_color"; for (const value of ["", "red", "blue"]) { const option = document.createElement("option"); option.value = value; option.textContent = value || "Choose"; select.append(option); } document.body.append(select); return select; } const padding = (t: number, r: number, b: number, l: number) => ({ padding_top: t, padding_right: r, padding_bottom: b, padding_left: l, }); const DEFAULT = padding(8, 16, 8, 16); const MAX = padding(48, 48, 48, 48); const NONE = padding(0, 0, 0, 0); async function renderGroup( swatches: Record, settingsOverride: Record, ) { const select = createSelect(); const result = render( , ); await waitFor(() => expect(result.container.querySelectorAll('[role="radio"]')).toHaveLength(2), ); return result; } function getRadio(container: HTMLElement, value: string): HTMLElement { const radio = container.querySelector( `[data-b3-wvs-radio-value="${value}"]`, ); if (!radio) throw new Error(`Missing radio for ${value}`); return radio; } afterEach(() => { cleanup(); document.body.innerHTML = ""; }); describe.each([ ["color", color], ["image", image], ])("free %s swatch fixed dimensions", (_type, swatches) => { it("fits default padding inside a 24px box", async () => { const { container } = await renderGroup(swatches, { swatch_width: 24, swatch_height: 24, ...DEFAULT, }); const radio = getRadio(container, "red"); expect(radio.style.width).toBe("24px"); expect(radio.style.height).toBe("24px"); expect(radio.style.boxSizing).toBe("border-box"); // 22px of room beside the 1px borders; vertical 8+8 already fits. expect(radio.style.padding).toBe("8px 11px"); expect(radio.style.getPropertyValue("--b3-wvs-swatch-padding")).toBe( "8px 11px 8px 11px", ); }); it("keeps default padding inside a 60px box", async () => { const { container } = await renderGroup(swatches, { swatch_width: 60, swatch_height: 60, ...DEFAULT, }); const radio = getRadio(container, "red"); expect(radio.style.width).toBe("60px"); expect(radio.style.height).toBe("60px"); expect(radio.style.padding).toBe("8px 16px"); }); it.each([ [24, "11px"], [60, "29px"], ])("fits 48px padding on every side inside a %spx box", async (size, fit) => { const { container } = await renderGroup(swatches, { swatch_width: size, swatch_height: size, ...MAX, }); const radio = getRadio(container, "red"); expect(radio.style.width).toBe(`${size}px`); expect(radio.style.height).toBe(`${size}px`); expect(radio.style.padding).toBe(fit); }); it("leaves zero padding alone", async () => { const { container } = await renderGroup(swatches, { swatch_width: 24, swatch_height: 24, ...NONE, }); expect(getRadio(container, "red").style.padding).toBe("0px"); }); }); describe("free number swatch fixed dimensions", () => { it("fits padding inside a 24px box while reserving label room", async () => { const { container } = await renderGroup(number, { swatch_width: 24, swatch_height: 24, label_font_size: 14, ...DEFAULT, }); const radio = getRadio(container, "red"); expect(radio.style.width).toBe("24px"); expect(radio.style.height).toBe("24px"); // 24 - 2px border - 14px content room = 8px for padding on each axis. expect(radio.style.padding).toBe("4px"); }); it("honors custom number sizing at 60px with max padding", async () => { const { container } = await renderGroup(number, { swatch_width: 24, swatch_height: 24, number_custom_sizing: true, number_swatch_width: 60, number_swatch_height: 60, label_font_size: 14, ...MAX, }); const radio = getRadio(container, "red"); expect(radio.style.width).toBe("60px"); expect(radio.style.height).toBe("60px"); // 60 - 2 - 14 = 44px of room: 48+48 scales to 22+22. expect(radio.style.padding).toBe("22px"); }); }); describe("free text button sizing policy", () => { it("fits only the vertical padding and keeps natural width (minWidth)", async () => { const { container } = await renderGroup(button, { swatch_width: 24, swatch_height: 24, label_font_size: 14, ...DEFAULT, }); const radio = getRadio(container, "red"); expect(radio.style.width).toBe(""); expect(radio.style.minWidth).toBe("24px"); expect(radio.style.height).toBe("24px"); // Underline style keeps its 4px bottom bar + 14px label inside 24px. expect(radio.style.padding).toBe("3px 16px"); expect(radio.textContent).toContain("Extra Large Label"); }); it("keeps a 60px button's default padding untouched", async () => { const { container } = await renderGroup(button, { swatch_width: 60, swatch_height: 60, ...DEFAULT, }); expect(getRadio(container, "red").style.padding).toBe("8px 16px"); }); it("fits pill segments vertically and keeps minWidth", async () => { const { container } = await renderGroup(button, { layout: "pill", swatch_width: 24, swatch_height: 24, label_font_size: 14, ...MAX, }); const radio = getRadio(container, "red"); expect(radio.style.width).toBe(""); expect(radio.style.minWidth).toBe("24px"); expect(radio.style.height).toBe("24px"); // No vertical border on a segment: 24 - 14 = 10px of room. expect(radio.style.padding).toBe("5px 48px"); }); });