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.
146 lines
5.1 KiB
TypeScript
146 lines
5.1 KiB
TypeScript
import { act, render, screen } from "@testing-library/react";
|
|
import { getShowcaseAssets } from "@/lib/project-showcase";
|
|
import { ProjectVideo } from "./ProjectVideo";
|
|
|
|
let reducedMotion = false;
|
|
jest.mock("@/app/hooks/useReducedMotion", () => ({
|
|
useReducedMotion: () => reducedMotion,
|
|
}));
|
|
|
|
const video = getShowcaseAssets("nutriscan", "desktop");
|
|
|
|
beforeEach(() => {
|
|
reducedMotion = false;
|
|
jest.spyOn(HTMLMediaElement.prototype, "play").mockResolvedValue();
|
|
jest.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
|
|
jest.spyOn(HTMLMediaElement.prototype, "load").mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
Object.defineProperty(navigator, "connection", {
|
|
configurable: true,
|
|
value: undefined,
|
|
});
|
|
});
|
|
|
|
describe("ProjectVideo", () => {
|
|
it("renders a muted looping inline video that downloads nothing until it is active", () => {
|
|
const { container } = render(
|
|
<ProjectVideo video={video} title="Demo" active={false} />,
|
|
);
|
|
const element = container.querySelector("video");
|
|
|
|
expect(element).not.toBeNull();
|
|
expect(element).toHaveAttribute("autoplay");
|
|
expect(element).toHaveAttribute("loop");
|
|
expect(element).toHaveAttribute("playsinline");
|
|
expect(element).toHaveAttribute("preload", "metadata");
|
|
expect(element).toHaveAttribute("poster", video.poster);
|
|
expect(element?.muted).toBe(true);
|
|
expect(element).toHaveAttribute("data-ready", "false");
|
|
expect(container.querySelectorAll("source")).toHaveLength(0);
|
|
expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("attaches the sources and plays when it becomes the active card, then pauses when it stops being", () => {
|
|
const { container, rerender } = render(
|
|
<ProjectVideo video={video} title="Demo" active={false} />,
|
|
);
|
|
|
|
rerender(<ProjectVideo video={video} title="Demo" active />);
|
|
const sources = [...container.querySelectorAll("source")];
|
|
expect(sources.map((source) => source.getAttribute("src"))).toEqual(
|
|
video.sources.map((source) => source.src),
|
|
);
|
|
expect(HTMLMediaElement.prototype.load).toHaveBeenCalled();
|
|
expect(HTMLMediaElement.prototype.play).toHaveBeenCalled();
|
|
|
|
rerender(<ProjectVideo video={video} title="Demo" active={false} />);
|
|
expect(HTMLMediaElement.prototype.pause).toHaveBeenCalled();
|
|
// sai de cena, mas o arquivo já baixado fica: voltar a tocar não baixa de novo
|
|
expect(container.querySelectorAll("source")).toHaveLength(2);
|
|
});
|
|
|
|
it("fades the video in once it is playing", () => {
|
|
const { container } = render(
|
|
<ProjectVideo video={video} title="Demo" active />,
|
|
);
|
|
const element = container.querySelector("video") as HTMLVideoElement;
|
|
|
|
act(() => {
|
|
element.dispatchEvent(new Event("playing"));
|
|
});
|
|
expect(element).toHaveAttribute("data-ready", "true");
|
|
});
|
|
|
|
it("does not even request the poster while the card is far from the screen", () => {
|
|
const { container, rerender } = render(
|
|
<ProjectVideo
|
|
video={video}
|
|
title="Demo"
|
|
active={false}
|
|
showPoster={false}
|
|
/>,
|
|
);
|
|
expect(container.querySelector("video")).not.toHaveAttribute("poster");
|
|
|
|
rerender(
|
|
<ProjectVideo video={video} title="Demo" active={false} showPoster />,
|
|
);
|
|
expect(container.querySelector("video")).toHaveAttribute(
|
|
"poster",
|
|
video.poster,
|
|
);
|
|
});
|
|
|
|
it("swaps to the sources of the new device when the version changes", () => {
|
|
const mobile = getShowcaseAssets("nutriscan", "mobile");
|
|
const { container, rerender } = render(
|
|
<ProjectVideo video={video} title="Demo" active />,
|
|
);
|
|
const loads = jest.mocked(HTMLMediaElement.prototype.load).mock.calls
|
|
.length;
|
|
|
|
rerender(<ProjectVideo video={mobile} title="Demo" active />);
|
|
expect(
|
|
[...container.querySelectorAll("source")].map((source) =>
|
|
source.getAttribute("src"),
|
|
),
|
|
).toEqual(mobile.sources.map((source) => source.src));
|
|
expect(
|
|
jest.mocked(HTMLMediaElement.prototype.load).mock.calls.length,
|
|
).toBeGreaterThan(loads);
|
|
});
|
|
|
|
it("shows only the poster, with no video element, when the data saver is on", () => {
|
|
Object.defineProperty(navigator, "connection", {
|
|
configurable: true,
|
|
value: { saveData: true },
|
|
});
|
|
const { container } = render(
|
|
<ProjectVideo video={video} title="Demo" active />,
|
|
);
|
|
|
|
expect(container.querySelector("video")).toBeNull();
|
|
expect(container.querySelector("source")).toBeNull();
|
|
expect(
|
|
screen.getByRole("img", { name: "Captura de tela de Demo" }),
|
|
).toBeInTheDocument();
|
|
expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows only the poster, with no video element, when the user prefers reduced motion", () => {
|
|
reducedMotion = true;
|
|
const { container } = render(
|
|
<ProjectVideo video={video} title="Demo" active />,
|
|
);
|
|
|
|
expect(container.querySelector("video")).toBeNull();
|
|
expect(
|
|
screen.getByRole("img", { name: "Captura de tela de Demo" }),
|
|
).toBeInTheDocument();
|
|
expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled();
|
|
});
|
|
});
|