// @vitest-environment jsdom // // Contract of the shared horizontal rail. jsdom lays out nothing (scrollWidth/clientWidth are 0), so // the overflow-driven scroll buttons only appear once we feed the scroller fake metrics and fire a // scroll — which is exactly the affordance under test: a button shows ONLY on the clipped edge, and // clicking it pages the scroller. i18n is key-passthrough, so asserted names are the i18n keys. import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (k: string) => k }) }; }); import { Carousel } from "./Carousel"; beforeAll(() => { // useHScroll wires a ResizeObserver (watch-the-children); jsdom ships none. class Stub { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; }); afterEach(cleanup); function setMetrics( el: HTMLElement, m: { scrollWidth: number; clientWidth: number; scrollLeft: number }, ) { Object.defineProperty(el, "scrollWidth", { configurable: true, value: m.scrollWidth }); Object.defineProperty(el, "clientWidth", { configurable: true, value: m.clientWidth }); Object.defineProperty(el, "scrollLeft", { configurable: true, writable: true, value: m.scrollLeft, }); } describe("Carousel", () => { it("renders children and an optional header with a see-all action", () => { const onSeeAll = vi.fn(); render(
card-a
card-b
, ); expect(screen.getByText("card-a")).toBeTruthy(); expect(screen.getByRole("heading", { name: "Recently added" })).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "common.seeAll" })); expect(onSeeAll).toHaveBeenCalledOnce(); }); it("omits the header when no title is given and names the scroll region", () => { render(
x
, ); expect(screen.queryByRole("heading")).toBeNull(); expect(screen.getByRole("group", { name: "Cast" })).toBeTruthy(); }); it("shows only the clipped edge's button and pages the scroller on click", () => { render(
x
, ); const scroller = screen.getByRole("group", { name: "rail" }); const scrollBy = vi.fn(); scroller.scrollBy = scrollBy as unknown as typeof scroller.scrollBy; // At the left boundary with more content to the right → only the right button. setMetrics(scroller, { scrollWidth: 1000, clientWidth: 300, scrollLeft: 0 }); fireEvent.scroll(scroller); expect(screen.queryByRole("button", { name: "common.scrollLeft" })).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "common.scrollRight" })); expect(scrollBy).toHaveBeenCalled(); // Scrolled into the middle → both edges clipped → both buttons. setMetrics(scroller, { scrollWidth: 1000, clientWidth: 300, scrollLeft: 350 }); fireEvent.scroll(scroller); expect(screen.getByRole("button", { name: "common.scrollLeft" })).toBeTruthy(); expect(screen.getByRole("button", { name: "common.scrollRight" })).toBeTruthy(); }); });