Files
portifolio-evandrodevbr/app/components/cinematic/HeroScene.paths.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

207 lines
5.8 KiB
TypeScript

import { act, render } from "@testing-library/react";
import HeroScene from "./HeroScene";
/**
* Testes de caminhos-base de HeroScene (fundo 3D das páginas internas): quando o desenho está ligado
* (visível E à vista), a câmera que segue o mouse (CameraRig) e o relógio do chão em grade (GridFloor).
* O @react-three/fiber é trocado por um dublê que renderiza os filhos e entrega os callbacks do useFrame.
*/
const mockFrames: Array<(state: unknown, delta: number) => void> = [];
const mockCamera = {
position: { x: 0, y: 0.5, z: 8 },
lookAt: jest.fn(),
};
const mockPulse = jest.fn();
jest.mock("@react-three/fiber", () => ({
Canvas: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
useFrame: (callback: (state: unknown, delta: number) => void) => {
mockFrames.push(callback);
},
useThree: () => ({ camera: mockCamera }),
}));
jest.mock("./FramePulse", () => ({
__esModule: true,
default: (props: { active: boolean }) => {
mockPulse(props.active);
return null;
},
}));
type Observer = {
callback: IntersectionObserverCallback;
disconnect: jest.Mock;
};
let observers: Observer[] = [];
const OriginalObserver = global.IntersectionObserver;
beforeEach(() => {
mockFrames.length = 0;
mockPulse.mockClear();
mockCamera.position = { x: 0, y: 0.5, z: 8 };
mockCamera.lookAt.mockClear();
observers = [];
global.IntersectionObserver = class {
private readonly record: Observer;
constructor(callback: IntersectionObserverCallback) {
this.record = { callback, disconnect: jest.fn() };
observers.push(this.record);
}
observe() {}
unobserve() {}
disconnect() {
this.record.disconnect();
}
takeRecords() {
return [];
}
} as unknown as typeof IntersectionObserver;
// as tags do R3F (fog, mesh, ...) são elementos desconhecidos no DOM do jsdom
jest.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
global.IntersectionObserver = OriginalObserver;
jest.restoreAllMocks();
});
const lastPulse = () =>
mockPulse.mock.calls[mockPulse.mock.calls.length - 1][0];
function report(isIntersecting: boolean) {
act(() => {
observers[0].callback(
[{ isIntersecting } as IntersectionObserverEntry],
{} as IntersectionObserver,
);
});
}
describe("HeroScene: quando desenha", () => {
it("path: visible by prop and in view (default) -> the pulse is on", () => {
render(<HeroScene />);
expect(lastPulse()).toBe(true);
});
it("path: isVisible=false -> the pulse is off even though the scene is in view", () => {
render(<HeroScene isVisible={false} />);
expect(lastPulse()).toBe(false);
});
it("path: scrolled out of view -> off; back in view -> on", () => {
render(<HeroScene />);
report(false);
expect(lastPulse()).toBe(false);
report(true);
expect(lastPulse()).toBe(true);
});
it("path: out of view and isVisible=false -> off", () => {
render(<HeroScene isVisible={false} />);
report(false);
expect(lastPulse()).toBe(false);
});
it("path: unmount -> the observer is disconnected", () => {
const view = render(<HeroScene />);
view.unmount();
expect(observers[0].disconnect).toHaveBeenCalledTimes(1);
});
});
describe("HeroScene: câmera que segue o mouse", () => {
// a cada renderização CameraRig e GridFloor chamam useFrame, nessa ordem: valem os dois últimos
const rigFrame = () => mockFrames[mockFrames.length - 2];
it("path: mouse at the center -> the camera eases toward the resting height and looks at the origin", () => {
render(<HeroScene />);
act(() => {
rigFrame()({}, 0.016);
});
expect(mockCamera.position.x).toBeCloseTo(0, 9);
expect(mockCamera.position.y).toBeCloseTo(0.5, 9);
expect(mockCamera.lookAt).toHaveBeenCalledWith(0, 0, 0);
});
it("path: mouse moved to the top-right corner -> the camera drifts right and up, a little per frame", () => {
Object.defineProperty(window, "innerWidth", {
configurable: true,
value: 1000,
});
Object.defineProperty(window, "innerHeight", {
configurable: true,
value: 800,
});
render(<HeroScene />);
act(() => {
window.dispatchEvent(
new MouseEvent("mousemove", { clientX: 1000, clientY: 0 }),
);
rigFrame()({}, 0.016);
});
// alvo x = 1 * 0.5; alvo y = 1 * 0.3 + 0.5; passo de 2% por quadro
expect(mockCamera.position.x).toBeCloseTo(0.5 * 0.02, 9);
expect(mockCamera.position.y).toBeCloseTo(0.5 + (0.8 - 0.5) * 0.02, 9);
});
it("path: unmount -> the mouse listener is removed", () => {
const remove = jest.spyOn(window, "removeEventListener");
const view = render(<HeroScene />);
view.unmount();
expect(remove.mock.calls.map(([type]) => type)).toContain("mousemove");
});
});
describe("HeroScene: relógio do chão em grade", () => {
const gridFrame = () => mockFrames[mockFrames.length - 1];
function attachMaterial(material: unknown) {
const mesh = document.querySelector("mesh") as HTMLElement & {
material?: unknown;
};
mesh.material = material;
}
it("path: the mesh has shader uniforms -> uTime follows the clock", () => {
render(<HeroScene />);
const uniforms = { uTime: { value: 0 } };
attachMaterial({ uniforms });
gridFrame()({ clock: { elapsedTime: 12.5 } }, 0.016);
expect(uniforms.uTime.value).toBe(12.5);
});
it("path: the material has no uniforms yet -> nothing happens", () => {
render(<HeroScene />);
attachMaterial({});
expect(() =>
gridFrame()({ clock: { elapsedTime: 3 } }, 0.016),
).not.toThrow();
});
it("path: the mesh is gone (unmounted) -> the frame callback does nothing", () => {
const view = render(<HeroScene />);
const frame = gridFrame();
view.unmount();
expect(() => frame({ clock: { elapsedTime: 3 } }, 0.016)).not.toThrow();
});
});