import { act, cleanup, fireEvent, render, waitFor, } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; import SwatchGroup, { type SwatchData } from "./SwatchGroup"; /** * Wrap/grid/slider swatches hand Tooltip an outer visual wrapper (label and * selected indicator) around the focusable radio. Keyboard focus on the radio * must open the tooltip, and the tooltip must describe the radio itself, not * the non-focusable wrapper. */ const swatches: Record = { red: { type: "color", color: "#ff0000", label: "Red" }, linen: { type: "image", image: "https://shop.example/linen.png", color: "#000000", label: "Linen", }, }; function createSelect(): HTMLSelectElement { const select = document.createElement("select"); select.name = "attribute_pa_finish"; for (const value of ["", "red", "linen"]) { const option = document.createElement("option"); option.value = value; option.textContent = value || "Choose"; select.append(option); } document.body.append(select); return select; } async function renderGroup(layout: string) { const result = render( , ); await waitFor(() => expect(result.container.querySelectorAll('[role="radio"]')).toHaveLength(2), ); return result; } const radioFor = (container: HTMLElement, value: string): HTMLElement => container.querySelector(`[data-b3-wvs-radio-value="${value}"]`)!; afterEach(() => { cleanup(); document.body.innerHTML = ""; }); describe("free swatch tooltips describe the focusable radio", () => { for (const layout of ["wrap", "grid", "slider"]) { for (const value of ["red", "linen"]) { it(`opens on keyboard focus and describes the ${value} radio (${layout})`, async () => { const { container } = await renderGroup(layout); const radio = radioFor(container, value); const wrapper = radio.closest( ".b3-wvs-swatch-hover-target", )!; expect(wrapper).not.toBe(radio); act(() => { radio.focus(); }); const tooltip = document.querySelector('[role="tooltip"]'); expect(tooltip).not.toBeNull(); expect(radio.getAttribute("aria-describedby")).toBe(tooltip!.id); expect(wrapper.hasAttribute("aria-describedby")).toBe(false); act(() => { radio.blur(); }); expect(document.querySelector('[role="tooltip"]')).toBeNull(); expect(radio.hasAttribute("aria-describedby")).toBe(false); }); } } it("puts the hover tooltip description on the radio, not the wrapper", async () => { const { container } = await renderGroup("wrap"); const radio = radioFor(container, "red"); const wrapper = radio.closest(".b3-wvs-swatch-hover-target")!; fireEvent.pointerEnter(wrapper, { pointerType: "mouse" }); const tooltip = document.querySelector('[role="tooltip"]'); expect(tooltip?.textContent).toContain("Red"); expect(radio.getAttribute("aria-describedby")).toBe(tooltip!.id); expect(wrapper.hasAttribute("aria-describedby")).toBe(false); fireEvent.pointerLeave(wrapper, { pointerType: "mouse" }); expect(radio.hasAttribute("aria-describedby")).toBe(false); }); it("keeps an existing description on the trigger alongside the tooltip", async () => { const { container } = await renderGroup("pill"); const radio = radioFor(container, "red"); radio.setAttribute("aria-describedby", "size-guide"); fireEvent.pointerEnter(radio, { pointerType: "mouse" }); const tooltip = document.querySelector('[role="tooltip"]')!; expect(radio.getAttribute("aria-describedby")).toBe( `size-guide ${tooltip.id}`, ); fireEvent.pointerLeave(radio, { pointerType: "mouse" }); expect(radio.getAttribute("aria-describedby")).toBe("size-guide"); }); });