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.
This commit is contained in:
1 parent
8d07b8c6bb
commit
c9a0b80656
48 files changed
+7210
-1241
No files matched your search
@@ -0,0 +1,193 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { getShowcaseAssets } from "@/lib/project-showcase";
|
||||
import { ProjectVideo } from "./ProjectVideo";
|
||||
|
||||
/**
|
||||
* Testes de caminhos-base de ProjectVideo (complemento de ProjectVideo.test.tsx): modo só pôster
|
||||
* (movimento reduzido ou "economizar dados") com e sem pôster pedido, inativo x ativo, vídeo sem fontes,
|
||||
* falha do play() e a combinação de ativo com modo só pôster.
|
||||
*/
|
||||
|
||||
let reducedMotion = false;
|
||||
jest.mock("@/app/hooks/useReducedMotion", () => ({
|
||||
useReducedMotion: () => reducedMotion,
|
||||
}));
|
||||
|
||||
const video = getShowcaseAssets("nutriscan", "desktop");
|
||||
|
||||
function setSaveData(value: boolean | undefined) {
|
||||
Object.defineProperty(navigator, "connection", {
|
||||
configurable: true,
|
||||
value: value === undefined ? undefined : { saveData: value },
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
reducedMotion = false;
|
||||
setSaveData(undefined);
|
||||
jest.spyOn(HTMLMediaElement.prototype, "play").mockResolvedValue();
|
||||
jest.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
|
||||
jest.spyOn(HTMLMediaElement.prototype, "load").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
setSaveData(undefined);
|
||||
});
|
||||
|
||||
describe("ProjectVideo: modo só pôster", () => {
|
||||
it.each([
|
||||
[
|
||||
"reduced motion",
|
||||
() => {
|
||||
reducedMotion = true;
|
||||
},
|
||||
],
|
||||
["the data saver", () => setSaveData(true)],
|
||||
])(
|
||||
"path: %s and the poster is not wanted yet -> renders nothing at all",
|
||||
(_l, arrange) => {
|
||||
arrange();
|
||||
|
||||
const { container } = render(
|
||||
<ProjectVideo video={video} title="Demo" active showPoster={false} />,
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"reduced motion",
|
||||
() => {
|
||||
reducedMotion = true;
|
||||
},
|
||||
],
|
||||
["the data saver", () => setSaveData(true)],
|
||||
])(
|
||||
"path: %s and the poster is wanted -> only the image, with the project name in its alt",
|
||||
(_l, arrange) => {
|
||||
arrange();
|
||||
|
||||
const { container } = render(
|
||||
<ProjectVideo video={video} title="Demo" active={false} />,
|
||||
);
|
||||
|
||||
expect(container.querySelector("video")).toBeNull();
|
||||
const image = screen.getByRole("img", {
|
||||
name: "Captura de tela de Demo",
|
||||
});
|
||||
expect(image).toHaveAttribute("width", String(video.width));
|
||||
expect(image).toHaveAttribute("height", String(video.height));
|
||||
},
|
||||
);
|
||||
|
||||
it("path: data saver off and motion allowed -> the video element, never the image", () => {
|
||||
setSaveData(false);
|
||||
|
||||
const { container } = render(
|
||||
<ProjectVideo video={video} title="Demo" active={false} />,
|
||||
);
|
||||
|
||||
expect(container.querySelector("video")).not.toBeNull();
|
||||
expect(screen.queryByRole("img")).toBeNull();
|
||||
});
|
||||
|
||||
it("path: active card but poster-only mode -> nothing is attached and nothing plays", () => {
|
||||
reducedMotion = true;
|
||||
|
||||
render(<ProjectVideo video={video} title="Demo" active />);
|
||||
|
||||
expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled();
|
||||
expect(HTMLMediaElement.prototype.load).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectVideo: vídeo", () => {
|
||||
it("path: far card (poster not wanted) -> the video has no poster attribute", () => {
|
||||
const { container } = render(
|
||||
<ProjectVideo
|
||||
video={video}
|
||||
title="Demo"
|
||||
active={false}
|
||||
showPoster={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector("video")).not.toHaveAttribute("poster");
|
||||
});
|
||||
|
||||
it("path: inactive and never activated -> no source, no load, no play, no pause", () => {
|
||||
render(<ProjectVideo video={video} title="Demo" active={false} />);
|
||||
|
||||
expect(HTMLMediaElement.prototype.load).not.toHaveBeenCalled();
|
||||
expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled();
|
||||
expect(HTMLMediaElement.prototype.pause).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("path: becomes active -> muted is forced on the element before play()", () => {
|
||||
const { container, rerender } = render(
|
||||
<ProjectVideo video={video} title="Demo" active={false} />,
|
||||
);
|
||||
const element = container.querySelector("video") as HTMLVideoElement;
|
||||
element.muted = false;
|
||||
let mutedAtPlay: boolean | undefined;
|
||||
jest.mocked(HTMLMediaElement.prototype.play).mockImplementation(function (
|
||||
this: HTMLMediaElement,
|
||||
) {
|
||||
mutedAtPlay = this.muted;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
rerender(<ProjectVideo video={video} title="Demo" active />);
|
||||
|
||||
expect(mutedAtPlay).toBe(true);
|
||||
});
|
||||
|
||||
it("path: play() is rejected by the browser -> swallowed, the video stays mounted", async () => {
|
||||
jest
|
||||
.mocked(HTMLMediaElement.prototype.play)
|
||||
.mockRejectedValue(new DOMException("blocked", "NotAllowedError"));
|
||||
|
||||
const { container } = render(
|
||||
<ProjectVideo video={video} title="Demo" active />,
|
||||
);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(container.querySelector("video")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("path: no sources in the assets -> becomes active without calling load()", () => {
|
||||
const empty = { ...video, sources: [] };
|
||||
|
||||
const { container } = render(
|
||||
<ProjectVideo video={empty} title="Demo" active />,
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll("source")).toHaveLength(0);
|
||||
expect(HTMLMediaElement.prototype.load).not.toHaveBeenCalled();
|
||||
expect(HTMLMediaElement.prototype.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("path: active -> inactive -> active again -> plays again without reloading the sources", () => {
|
||||
const { container, rerender } = render(
|
||||
<ProjectVideo video={video} title="Demo" active />,
|
||||
);
|
||||
const loadsAfterFirst = jest.mocked(HTMLMediaElement.prototype.load).mock
|
||||
.calls.length;
|
||||
const playsAfterFirst = jest.mocked(HTMLMediaElement.prototype.play).mock
|
||||
.calls.length;
|
||||
|
||||
rerender(<ProjectVideo video={video} title="Demo" active={false} />);
|
||||
rerender(<ProjectVideo video={video} title="Demo" active />);
|
||||
|
||||
expect(jest.mocked(HTMLMediaElement.prototype.load).mock.calls.length).toBe(
|
||||
loadsAfterFirst,
|
||||
);
|
||||
expect(jest.mocked(HTMLMediaElement.prototype.play).mock.calls.length).toBe(
|
||||
playsAfterFirst + 1,
|
||||
);
|
||||
expect(container.querySelectorAll("source")).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user