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.
258 lines
7.8 KiB
TypeScript
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();
|
|
});
|
|
});
|