import { render, screen, waitFor } from "@testing-library/react"; import { portfolio } from "@/lib/portfolio-data"; import NotebookStory, { canUseNotebook3D, getNotebookRunwayVh, getNotebookSeekProgress, } from "./NotebookStory"; jest.mock("next/dynamic", () => () => { return function DynamicNotebookStage() { return
; }; }); jest.mock("gsap", () => ({ gsap: { registerPlugin: jest.fn(), timeline: jest.fn(() => ({ to: jest.fn().mockReturnThis() })), context: jest.fn((callback: () => void) => { callback(); return { revert: jest.fn() }; }), }, })); jest.mock("gsap/ScrollTrigger", () => ({ ScrollTrigger: { getAll: jest.fn(() => []) }, })); const projects = portfolio.projects; function mockMatchMedia({ reduced = false, coarse = false, hover = true, }: { reduced?: boolean; coarse?: boolean; hover?: boolean; } = {}) { const matcher = jest.isMockFunction(window.matchMedia) ? window.matchMedia : jest.spyOn(window, "matchMedia"); matcher.mockImplementation((query: string) => ({ matches: (query === "(prefers-reduced-motion: reduce)" && reduced) || (query === "(pointer: coarse)" && coarse) || (query === "(hover: hover)" && hover), media: query, onchange: null, addEventListener: jest.fn(), removeEventListener: jest.fn(), addListener: jest.fn(), removeListener: jest.fn(), dispatchEvent: jest.fn(), })); } describe("NotebookStory", () => { beforeEach(() => { mockMatchMedia(); Object.defineProperty(window, "innerWidth", { configurable: true, value: 1280, }); }); afterEach(() => { jest.restoreAllMocks(); }); it("renders the static fallback when reduced motion is enabled", async () => { mockMatchMedia({ reduced: true }); render(); expect(screen.getByTestId("notebook-fallback")).toBeInTheDocument(); await waitFor(() => expect(screen.queryByTestId("notebook-stage")).not.toBeInTheDocument(), ); }); it("does not initialize the 3D stage on coarse pointers", async () => { mockMatchMedia({ coarse: true }); render(); expect(screen.getByTestId("notebook-fallback")).toBeInTheDocument(); await waitFor(() => expect(screen.queryByTestId("notebook-stage")).not.toBeInTheDocument(), ); }); it("initializes the 3D stage only on a desktop hover-capable viewport", async () => { render(); await waitFor(() => expect(screen.getByTestId("notebook-stage")).toBeInTheDocument(), ); expect( screen.getByLabelText("Projetos em produção no notebook"), ).toHaveAttribute("data-notebook-runway-vh", "520"); }); it("returns to the static fallback when the viewport loses desktop eligibility", async () => { render(); await waitFor(() => expect(screen.getByTestId("notebook-stage")).toBeInTheDocument(), ); Object.defineProperty(window, "innerWidth", { configurable: true, value: 800, }); window.dispatchEvent(new Event("resize")); await waitFor(() => expect(screen.queryByTestId("notebook-stage")).not.toBeInTheDocument(), ); expect(screen.getByTestId("notebook-fallback")).toBeInTheDocument(); }); it("keeps the static fallback when no mapped projects are available", async () => { mockMatchMedia(); render(); expect(screen.getByTestId("notebook-fallback")).toBeInTheDocument(); expect( screen.getByLabelText("Projetos em produção no notebook"), ).toBeInTheDocument(); await waitFor(() => expect(screen.queryByTestId("notebook-stage")).not.toBeInTheDocument(), ); }); it("scales the runway from the number of projects", () => { expect(getNotebookRunwayVh(1)).toBe(400); expect(getNotebookRunwayVh(4)).toBe(520); expect(getNotebookRunwayVh(99)).toBe(4320); }); it("requires desktop hover input without reduced motion", () => { const matchMedia = (query: string) => ({ matches: query === "(hover: hover)", }) as MediaQueryList; expect(canUseNotebook3D({ innerWidth: 1280, matchMedia })).toBe(true); expect(canUseNotebook3D({ innerWidth: 1024, matchMedia })).toBe(false); expect( canUseNotebook3D({ innerWidth: 1280, matchMedia: (query) => ({ matches: query === "(hover: hover)" || query === "(prefers-reduced-motion: reduce)", }) as MediaQueryList, }), ).toBe(false); }); it("passes only mapped projects into the fallback browser", () => { mockMatchMedia({ reduced: true }); render(); expect(screen.getByRole("tab", { name: /NutriScan/i })).toBeInTheDocument(); expect( screen.queryByRole("tab", { name: /Twins Lab/i }), ).not.toBeInTheDocument(); }); it("maps tab seeks inside the browse window without a second scroll engine", () => { const first = getNotebookSeekProgress(0, 4); const middle = getNotebookSeekProgress(2, 4); const last = getNotebookSeekProgress(3, 4); expect(first).toBeGreaterThanOrEqual(0.44); expect(first).toBeLessThan(0.78); expect(middle).toBeGreaterThan(first); expect(last).toBeLessThan(0.78); expect(getNotebookSeekProgress(99, 4)).toBeLessThan(0.78); }); it("does not register a global scroll listener", () => { const addEventListener = jest.spyOn(window, "addEventListener"); mockMatchMedia({ reduced: true }); render(); expect( addEventListener.mock.calls.some(([type]) => type === "scroll"), ).toBe(false); }); });