Files
portifolio-evandrodevbr/app/components/story/NotebookStory.test.tsx
T

195 lines
5.7 KiB
TypeScript

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 <div data-testid="notebook-stage" />;
};
});
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(<NotebookStory projects={projects} />);
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(<NotebookStory projects={projects} />);
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(<NotebookStory projects={projects} />);
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(<NotebookStory projects={projects} />);
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(<NotebookStory projects={[]} />);
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(<NotebookStory projects={projects} />);
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(<NotebookStory projects={projects} />);
expect(
addEventListener.mock.calls.some(([type]) => type === "scroll"),
).toBe(false);
});
});