import { act, fireEvent, render, screen } from "@testing-library/react"; import { getFeaturedProjects } from "@/lib/portfolio-data"; import { buildShowcaseItems, ShowcaseProvider, useShowcaseCard, useShowcaseOpen, } from "./ShowcaseProvider"; let reducedMotion = false; jest.mock("@/app/hooks/useReducedMotion", () => ({ useReducedMotion: () => reducedMotion, })); jest.mock("./ProjectLightbox", () => ({ ProjectLightbox: ({ index, onClose, }: { index: number; onClose: () => void; }) => (
aberto:{index}
), })); type FakeObserver = { callback: IntersectionObserverCallback; targets: Set; }; let observers: FakeObserver[] = []; const OriginalObserver = global.IntersectionObserver; class FakeIntersectionObserver { private readonly state: FakeObserver; constructor(callback: IntersectionObserverCallback) { this.state = { callback, targets: new Set() }; observers.push(this.state); } observe(element: Element) { this.state.targets.add(element); } unobserve(element: Element) { this.state.targets.delete(element); } disconnect() { this.state.targets.clear(); } takeRecords() { return []; } } function setVisibility(element: Element, ratio: number) { for (const observer of observers) { if (!observer.targets.has(element)) continue; act(() => { observer.callback( [ { target: element, intersectionRatio: ratio, isIntersecting: ratio > 0, } as IntersectionObserverEntry, ], observer as unknown as IntersectionObserver, ); }); } } function place(element: Element, top: number) { element.getBoundingClientRect = () => ({ top, height: 300, bottom: top + 300, left: 0, right: 400, width: 400, x: 0, y: top, }) as DOMRect; } function flush() { act(() => { jest.advanceTimersByTime(40); }); } function Card({ id }: { id: string }) { const { frameRef, active, open } = useShowcaseCard(id); return (
); } const projects = getFeaturedProjects(null); function setup() { const view = render( , ); const frames = { sev: screen.getByTestId("sev-modaintima"), lavacao: screen.getByTestId("lavacao-047"), nutri: screen.getByTestId("nutriscan"), }; // tela de 768px de altura (jsdom): o meio fica em 384 place(frames.sev, 100); // centro 250 place(frames.lavacao, 230); // centro 380 place(frames.nutri, 900); // fora da tela return { view, frames }; } beforeEach(() => { jest.useFakeTimers(); reducedMotion = false; observers = []; global.IntersectionObserver = FakeIntersectionObserver as unknown as typeof IntersectionObserver; }); afterEach(() => { jest.useRealTimers(); global.IntersectionObserver = OriginalObserver; }); const isActive = (element: Element) => element.getAttribute("data-active") === "true"; describe("ShowcaseProvider", () => { it("lists only projects that have a recorded showcase, in showcase order", () => { const items = buildShowcaseItems(projects); expect(items.map((item) => item.config.id)).toEqual([ "sev-modaintima", "lavacao-047", "nutriscan", "sc-plus", "as-servicos", ]); expect( buildShowcaseItems(projects.filter((p) => p.id === "twins-lab")), ).toEqual([]); }); it("serves the reel with only the client projects the home passes in, in showcase order", () => { const clients = projects.filter((project) => ["as-servicos", "sc-plus", "lavacao-047", "sev-modaintima"].includes( project.id, ), ); expect(buildShowcaseItems(clients).map((item) => item.config.id)).toEqual([ "sev-modaintima", "lavacao-047", "sc-plus", "as-servicos", ]); }); it("works with no cards at all: no per-scroll work, and the enlarged window still opens and navigates", () => { const clients = projects.filter((project) => ["sev-modaintima", "lavacao-047", "sc-plus", "as-servicos"].includes( project.id, ), ); function Opener() { const open = useShowcaseOpen(); return ( ); } const frames = jest.spyOn(window, "requestAnimationFrame"); render( , ); flush(); frames.mockClear(); act(() => { window.dispatchEvent(new Event("scroll")); window.dispatchEvent(new Event("resize")); }); flush(); expect(frames).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole("button", { name: "abrir sc-plus" })); // posição 3 de 4 na lista que a home passou (o NutriScan não está nela) expect(screen.getByRole("dialog", { name: "ampliado" })).toHaveTextContent( "aberto:2", ); fireEvent.click(screen.getByRole("button", { name: "fechar" })); expect(screen.queryByRole("dialog")).toBeNull(); frames.mockRestore(); }); it("plays nothing until a card is on screen, then the one closest to the middle", () => { const { frames } = setup(); flush(); expect(Object.values(frames).some(isActive)).toBe(false); setVisibility(frames.sev, 0.8); setVisibility(frames.lavacao, 0.8); flush(); expect(isActive(frames.lavacao)).toBe(true); expect(isActive(frames.sev)).toBe(false); }); it("does not count a card with less than a third on screen", () => { const { frames } = setup(); setVisibility(frames.sev, 0.2); setVisibility(frames.lavacao, 0.1); flush(); expect(Object.values(frames).some(isActive)).toBe(false); }); it("hands the video to the card under the mouse and takes it back when the mouse leaves", () => { const { frames } = setup(); setVisibility(frames.sev, 0.8); setVisibility(frames.lavacao, 0.8); flush(); const sevCard = frames.sev.closest(".ds-card") as HTMLElement; fireEvent.pointerEnter(sevCard, { pointerType: "mouse" }); flush(); expect(isActive(frames.sev)).toBe(true); expect(isActive(frames.lavacao)).toBe(false); fireEvent.pointerLeave(sevCard, { pointerType: "mouse" }); flush(); expect(isActive(frames.lavacao)).toBe(true); }); it("ignores touch pointers: there is no hover on a phone", () => { const { frames } = setup(); setVisibility(frames.sev, 0.8); setVisibility(frames.lavacao, 0.8); flush(); fireEvent.pointerEnter(frames.sev.closest(".ds-card") as HTMLElement, { pointerType: "touch", }); flush(); expect(isActive(frames.sev)).toBe(false); expect(isActive(frames.lavacao)).toBe(true); }); it("follows keyboard focus inside a card", () => { const { frames } = setup(); setVisibility(frames.sev, 0.8); setVisibility(frames.lavacao, 0.8); flush(); fireEvent.focusIn( screen.getByRole("button", { name: "ampliar sev-modaintima" }), ); flush(); expect(isActive(frames.sev)).toBe(true); }); it("re-evaluates on scroll", () => { const { frames } = setup(); setVisibility(frames.sev, 0.8); setVisibility(frames.lavacao, 0.8); flush(); expect(isActive(frames.lavacao)).toBe(true); place(frames.sev, 230); place(frames.lavacao, 600); act(() => { window.dispatchEvent(new Event("scroll")); }); flush(); expect(isActive(frames.sev)).toBe(true); }); it("pauses every card while the enlarged window is open, and resumes after", () => { const { frames } = setup(); setVisibility(frames.lavacao, 0.8); flush(); expect(isActive(frames.lavacao)).toBe(true); fireEvent.click( screen.getByRole("button", { name: "ampliar lavacao-047" }), ); flush(); expect(screen.getByRole("dialog", { name: "ampliado" })).toHaveTextContent( "aberto:1", ); expect(isActive(frames.lavacao)).toBe(false); fireEvent.click(screen.getByRole("button", { name: "fechar" })); flush(); expect(screen.queryByRole("dialog")).toBeNull(); expect(isActive(frames.lavacao)).toBe(true); }); it("never plays a card for users who prefer reduced motion", () => { reducedMotion = true; const { frames } = setup(); setVisibility(frames.lavacao, 1); flush(); expect(isActive(frames.lavacao)).toBe(false); }); it("stops listening when it unmounts", () => { const { view, frames } = setup(); setVisibility(frames.lavacao, 1); view.unmount(); expect(observers.every((observer) => observer.targets.size === 0)).toBe( true, ); }); });