CI / verify (push) Failing after 5m9s
Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
222 lines
6.6 KiB
TypeScript
222 lines
6.6 KiB
TypeScript
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
import { getFeaturedProjects } from "@/lib/portfolio-data";
|
|
import NotebookStory, { canUseNotebook3D } from "./NotebookStory";
|
|
|
|
const mockNotebookStage = jest.fn(({ isVisible }: { isVisible: boolean }) => (
|
|
<div data-is-visible={String(isVisible)} data-testid="notebook-stage" />
|
|
));
|
|
|
|
jest.mock("next/dynamic", () => () => {
|
|
return function DynamicNotebookStage(props: { isVisible: boolean }) {
|
|
return mockNotebookStage(props);
|
|
};
|
|
});
|
|
|
|
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 = getFeaturedProjects(null);
|
|
|
|
let intersectionCallback: IntersectionObserverCallback | undefined;
|
|
const mockObserve = jest.fn();
|
|
const mockDisconnect = jest.fn();
|
|
|
|
function setDocumentHidden(hidden: boolean) {
|
|
Object.defineProperty(document, "hidden", {
|
|
configurable: true,
|
|
value: hidden,
|
|
});
|
|
}
|
|
|
|
function setSectionIntersects(isIntersecting: boolean) {
|
|
const entry = { isIntersecting } as IntersectionObserverEntry;
|
|
act(() => intersectionCallback?.([entry], {} as IntersectionObserver));
|
|
}
|
|
|
|
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,
|
|
});
|
|
setDocumentHidden(false);
|
|
intersectionCallback = undefined;
|
|
mockObserve.mockReset();
|
|
mockDisconnect.mockReset();
|
|
class MockIntersectionObserver {
|
|
readonly root = null;
|
|
readonly rootMargin = "";
|
|
readonly thresholds = [];
|
|
|
|
constructor(callback: IntersectionObserverCallback) {
|
|
intersectionCallback = callback;
|
|
}
|
|
|
|
disconnect = mockDisconnect;
|
|
observe = mockObserve;
|
|
takeRecords = () => [];
|
|
unobserve = jest.fn();
|
|
}
|
|
global.IntersectionObserver =
|
|
MockIntersectionObserver as unknown as typeof IntersectionObserver;
|
|
});
|
|
|
|
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("stays paused when the tab returns while the section is offscreen", async () => {
|
|
render(<NotebookStory projects={projects} />);
|
|
|
|
const stage = await screen.findByTestId("notebook-stage");
|
|
expect(stage).toHaveAttribute("data-is-visible", "false");
|
|
|
|
setSectionIntersects(true);
|
|
await waitFor(() =>
|
|
expect(stage).toHaveAttribute("data-is-visible", "true"),
|
|
);
|
|
|
|
setDocumentHidden(true);
|
|
act(() => document.dispatchEvent(new Event("visibilitychange")));
|
|
await waitFor(() =>
|
|
expect(stage).toHaveAttribute("data-is-visible", "false"),
|
|
);
|
|
|
|
setSectionIntersects(false);
|
|
setDocumentHidden(false);
|
|
act(() => document.dispatchEvent(new Event("visibilitychange")));
|
|
expect(stage).toHaveAttribute("data-is-visible", "false");
|
|
});
|
|
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("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();
|
|
});
|
|
});
|