import { existsSync, statSync } from "node:fs"; import { join } from "node:path"; import { act, render, screen } from "@testing-library/react"; import { getProjectVideo, PROJECT_VIDEO_IDS, type ProjectVideoAsset, } from "@/lib/project-videos"; import { ProjectVideo } from "./ProjectVideo"; let reducedMotion = false; jest.mock("@/app/hooks/useReducedMotion", () => ({ useReducedMotion: () => reducedMotion, })); const video: ProjectVideoAsset = { poster: "/videos/demo/desktop.webp", width: 1440, height: 900, sources: [ { src: "/videos/demo/desktop.webm", type: 'video/webm; codecs="vp9"' }, { src: "/videos/demo/desktop.mp4", type: "video/mp4" }, ], }; type Callback = IntersectionObserverCallback; let callbacks: Callback[] = []; const originalObserver = global.IntersectionObserver; beforeEach(() => { reducedMotion = false; callbacks = []; // observador controlável: o teste decide quando o card entra e sai da tela global.IntersectionObserver = class { constructor(callback: Callback) { callbacks.push(callback); } observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } as unknown as typeof IntersectionObserver; jest.spyOn(HTMLMediaElement.prototype, "play").mockResolvedValue(); jest.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {}); jest.spyOn(HTMLMediaElement.prototype, "load").mockImplementation(() => {}); }); afterEach(() => { global.IntersectionObserver = originalObserver; jest.restoreAllMocks(); }); function setVisible(isIntersecting: boolean) { act(() => { for (const callback of callbacks) { callback( [{ isIntersecting } as IntersectionObserverEntry], {} as IntersectionObserver, ); } }); } describe("ProjectVideo", () => { it("renders a muted looping inline video that downloads nothing before it is visible", () => { const { container } = render(); const element = container.querySelector("video"); expect(element).not.toBeNull(); expect(element).toHaveAttribute("autoplay"); expect(element).toHaveAttribute("loop"); expect(element).toHaveAttribute("playsinline"); expect(element).toHaveAttribute("preload", "none"); expect(element).toHaveAttribute("poster", video.poster); expect(element?.muted).toBe(true); expect(container.querySelectorAll("source")).toHaveLength(0); expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled(); }); it("attaches the sources and plays once the card enters the viewport, then pauses when it leaves", () => { const { container } = render(); setVisible(true); const sources = [...container.querySelectorAll("source")]; expect(sources.map((source) => source.getAttribute("src"))).toEqual([ "/videos/demo/desktop.webm", "/videos/demo/desktop.mp4", ]); expect(HTMLMediaElement.prototype.load).toHaveBeenCalled(); expect(HTMLMediaElement.prototype.play).toHaveBeenCalled(); setVisible(false); expect(HTMLMediaElement.prototype.pause).toHaveBeenCalled(); }); it("shows only the poster, with no video element, when the user prefers reduced motion", () => { reducedMotion = true; const { container } = render(); expect(container.querySelector("video")).toBeNull(); expect( screen.getByRole("img", { name: "Captura de tela de Demo" }), ).toBeInTheDocument(); expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled(); }); }); describe("getProjectVideo", () => { it("returns undefined for projects without a recorded video", () => { expect(getProjectVideo("reddit-explorer")).toBeUndefined(); }); it.each(PROJECT_VIDEO_IDS)( "has webm, mp4 and poster on disk for %s, each within 3 MB", (id) => { const asset = getProjectVideo(id); expect(asset).toBeDefined(); const files = [ asset?.poster, ...(asset?.sources.map((s) => s.src) ?? []), ]; for (const file of files) { const path = join(process.cwd(), "public", file ?? ""); expect(existsSync(path)).toBe(true); expect(statSync(path).size).toBeLessThanOrEqual(3 * 1024 * 1024); } }, ); });