Files
portifolio-evandrodevbr/app/components/showcase/ProjectMedia.test.tsx
T
Evandro Fonseca Junior c9a0b80656 perf(movimento): uma transição, 3D leve e vídeo certo por aparelho
Passagens com fade e 32 px, cena anterior termina em opacidade 0, sem visibility nem clip-path. 3D do hero como fundo ambiente, sob demanda e desligado em toque, aparelho fraco, saveData e movimento reduzido. Lenis fora do toque e salto direto em âncoras distantes. Vitrine só de clientes, até 3 capítulos, fixa só no desktop e carrossel no celular; vídeo só do projeto em cena com preload metadata. Rótulos mono a partir de 12 px e limão só no contato.
2026-10-14 03:32:23 -03:00

258 lines
7.8 KiB
TypeScript

import { act, fireEvent, render, screen } from "@testing-library/react";
import { renderToString } from "react-dom/server";
import { MOBILE_QUERY } from "@/lib/device";
import { getFeaturedProjects } from "@/lib/portfolio-data";
import { ProjectMedia } from "./ProjectMedia";
import { ShowcaseProvider } from "./ShowcaseProvider";
let reducedMotion = false;
jest.mock("@/app/hooks/useReducedMotion", () => ({
useReducedMotion: () => reducedMotion,
}));
jest.mock("./ProjectLightbox", () => ({
ProjectLightbox: () => <div role="dialog" aria-label="ampliado" />,
}));
const projects = getFeaturedProjects(null);
const nutriscan = projects.find((project) => project.id === "nutriscan");
if (!nutriscan) throw new Error("projeto ausente");
type Callback = IntersectionObserverCallback;
const callbacks: Callback[] = [];
const Original = global.IntersectionObserver;
const originalMatchMedia = window.matchMedia;
function mockPhone() {
window.matchMedia = jest.fn().mockImplementation((query: string) => ({
matches: query === MOBILE_QUERY,
media: query,
addEventListener() {},
removeEventListener() {},
}));
}
beforeEach(() => {
jest.useFakeTimers();
reducedMotion = false;
callbacks.length = 0;
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(() => {
jest.useRealTimers();
jest.restoreAllMocks();
global.IntersectionObserver = Original;
window.matchMedia = originalMatchMedia;
Object.defineProperty(navigator, "connection", {
configurable: true,
value: undefined,
});
});
function renderCard() {
const view = render(
<ShowcaseProvider projects={projects}>
<article className="ds-card">
<ProjectMedia project={nutriscan as NonNullable<typeof nutriscan>} />
</article>
</ShowcaseProvider>,
);
const frame = view.container.querySelector(".ds-frame") as HTMLElement;
frame.getBoundingClientRect = () =>
({
top: 300,
height: 300,
bottom: 600,
left: 0,
right: 400,
width: 400,
x: 0,
y: 300,
}) as DOMRect;
return { ...view, frame };
}
function show(frame: HTMLElement) {
act(() => {
for (const callback of callbacks)
callback(
[
{
target: frame,
intersectionRatio: 1,
isIntersecting: true,
} as unknown as IntersectionObserverEntry,
],
{} as IntersectionObserver,
);
jest.advanceTimersByTime(40);
});
}
describe("ProjectMedia", () => {
it("frames the video with the project's address and a button that enlarges it", () => {
renderCard();
expect(screen.getByText("nutriscan.evandro.dev.br")).toBeInTheDocument();
const expand = screen.getByRole("button", {
name: "Ampliar o vídeo de NutriScan",
});
expect(expand).toHaveAttribute("aria-haspopup", "dialog");
fireEvent.click(expand);
expect(
screen.getByRole("dialog", { name: "ampliado" }),
).toBeInTheDocument();
});
it("plays only while the card is the active one", () => {
const { frame, container } = renderCard();
const video = container.querySelector("video") as HTMLVideoElement;
expect(container.querySelector(".ds-frame")).toHaveAttribute(
"data-active",
"false",
);
expect(video.querySelectorAll("source")).toHaveLength(0);
show(frame);
expect(container.querySelector(".ds-frame")).toHaveAttribute(
"data-active",
"true",
);
expect(video.querySelectorAll("source")).toHaveLength(2);
});
it("lets the visitor pause the videos that play on their own, and resume them", () => {
const { frame, container } = renderCard();
show(frame);
expect(container.querySelector(".ds-frame")).toHaveAttribute(
"data-active",
"true",
);
fireEvent.click(
screen.getByRole("button", { name: "Pausar os vídeos dos projetos" }),
);
act(() => {
jest.advanceTimersByTime(40);
});
expect(container.querySelector(".ds-frame")).toHaveAttribute(
"data-active",
"false",
);
fireEvent.click(
screen.getByRole("button", { name: "Retomar os vídeos dos projetos" }),
);
act(() => {
jest.advanceTimersByTime(40);
});
expect(container.querySelector(".ds-frame")).toHaveAttribute(
"data-active",
"true",
);
});
it("has no pause button when nothing plays on its own (reduced motion)", () => {
reducedMotion = true;
renderCard();
expect(
screen.queryByRole("button", { name: /vídeos dos projetos/ }),
).toBeNull();
});
it("sends no media at all in the server HTML: the device is only known in the browser", () => {
const html = renderToString(
<ShowcaseProvider projects={projects}>
<ProjectMedia project={nutriscan as NonNullable<typeof nutriscan>} />
</ShowcaseProvider>,
);
expect(html).toContain("ds-frame-screen");
expect(html).not.toContain("<video");
expect(html).not.toContain("/videos/");
});
it("asks for the poster only once the cover is within about a screen of the window", () => {
const { frame, container } = renderCard();
const screenElement = container.querySelector(
".ds-frame-screen",
) as HTMLElement;
const video = container.querySelector("video") as HTMLVideoElement;
expect(screenElement.style.backgroundImage).toBe("");
expect(video).not.toHaveAttribute("poster");
expect(video).toHaveAttribute("preload", "metadata");
show(frame);
expect(screenElement.style.backgroundImage).toContain(
"/videos/nutriscan/desktop.webp",
);
expect(video).toHaveAttribute("poster", "/videos/nutriscan/desktop.webp");
});
it("uses the phone version of the poster and the video below 768 px", () => {
mockPhone();
const { frame, container } = renderCard();
show(frame);
const screenElement = container.querySelector(
".ds-frame-screen",
) as HTMLElement;
expect(screenElement.style.backgroundImage).toContain(
"/videos/nutriscan/mobile.webp",
);
const sources = [...container.querySelectorAll("video source")].map(
(source) => source.getAttribute("src"),
);
expect(sources).toEqual([
"/videos/nutriscan/mobile.webm",
"/videos/nutriscan/mobile.mp4",
]);
});
it("keeps the poster and never attaches a video when the data saver is on", () => {
Object.defineProperty(navigator, "connection", {
configurable: true,
value: { saveData: true },
});
const { frame, container } = renderCard();
show(frame);
expect(container.querySelector("video")).toBeNull();
expect(container.querySelector("source")).toBeNull();
expect(
screen.getByRole("img", { name: "Captura de tela de NutriScan" }),
).toBeInTheDocument();
});
it("renders nothing for a project without a recorded video", () => {
const { container } = render(
<ShowcaseProvider projects={projects}>
<ProjectMedia project={{ id: "twins-lab", title: "Twins Lab" }} />
</ShowcaseProvider>,
);
expect(container.querySelector(".ds-frame")).toBeNull();
});
it("does nothing, without crashing, when used outside the provider", () => {
const { container } = render(
<ProjectMedia project={nutriscan as NonNullable<typeof nutriscan>} />,
);
fireEvent.click(
screen.getByRole("button", { name: "Ampliar o vídeo de NutriScan" }),
);
expect(container.querySelector(".ds-frame")).not.toBeNull();
});
});