165 lines
4.7 KiB
TypeScript
165 lines
4.7 KiB
TypeScript
import { render, screen, waitFor } from "@testing-library/react";
|
|
import { portfolio } from "@/lib/portfolio-data";
|
|
import NotebookStory, {
|
|
canUseNotebook3D,
|
|
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(),
|
|
);
|
|
});
|
|
it("keeps the static fallback when no mapped projects are available", async () => {
|
|
mockMatchMedia();
|
|
|
|
render(<NotebookStory projects={[]} />);
|
|
|
|
expect(screen.getByTestId("notebook-fallback")).toBeInTheDocument();
|
|
await waitFor(() =>
|
|
expect(screen.queryByTestId("notebook-stage")).not.toBeInTheDocument(),
|
|
);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|