Files
portifolio-evandrodevbr/app/components/cinematic/FilmGrain.paths.test.tsx
T
Evandro Fonseca Junior 318a764190 perf(movimento): sem tela vazia, passagens em CSS e vitrine leve
Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
2026-10-14 14:15:50 -03:00

222 lines
6.5 KiB
TypeScript

import { act, render } from "@testing-library/react";
import FilmGrain from "./FilmGrain";
/**
* Testes de caminhos-base de FilmGrain (complemento de CinematicEffects.test.tsx): a medição é do
* IntersectionObserver (nada ouve a rolagem nem mede a página a cada quadro), uma tela escondida por CSS não
* conta, várias telas, telas que a página cria ou remove depois e a limpeza ao desmontar.
*/
type Observer = {
callback: IntersectionObserverCallback;
observed: Set<Element>;
/** quantas vezes observe() foi chamado (um conjunto esconderia repetições) */
calls: number;
disconnect: jest.Mock;
};
let observers: Observer[] = [];
const original = global.IntersectionObserver;
beforeEach(() => {
observers = [];
global.IntersectionObserver = class {
private readonly record: Observer;
constructor(callback: IntersectionObserverCallback) {
this.record = {
callback,
observed: new Set(),
calls: 0,
disconnect: jest.fn(),
};
observers.push(this.record);
}
observe(target: Element) {
this.record.observed.add(target);
this.record.calls += 1;
}
unobserve(target: Element) {
this.record.observed.delete(target);
}
disconnect() {
this.record.disconnect();
}
takeRecords() {
return [];
}
} as unknown as typeof IntersectionObserver;
});
afterEach(() => {
global.IntersectionObserver = original;
jest.restoreAllMocks();
document.body.innerHTML = "";
});
function addScreen(className: string) {
const element = document.createElement("div");
element.className = className;
document.body.append(element);
return element;
}
function intersect(target: Element, isIntersecting: boolean, height = 600) {
act(() => {
for (const observer of observers)
if (observer.observed.has(target))
observer.callback(
[
{
target,
isIntersecting,
boundingClientRect: { height },
} as unknown as IntersectionObserverEntry,
],
{} as IntersectionObserver,
);
});
}
function renderGrain() {
const view = render(<FilmGrain />);
return { ...view, grain: view.container.firstElementChild as HTMLElement };
}
/** Deixa o MutationObserver (microtarefa) avisar. */
const flush = () =>
act(async () => {
await Promise.resolve();
});
describe("FilmGrain: is a project screen in view?", () => {
it("path: a visible screen -> the grain steps aside", () => {
const screen = addScreen("ds-phone");
const { grain } = renderGrain();
intersect(screen, true);
expect(grain).toHaveAttribute("data-over-projects");
});
it("path: a screen with no height (hidden by CSS) -> does not count even if the browser says it intersects", () => {
const screen = addScreen("ds-bframe");
const { grain } = renderGrain();
intersect(screen, true, 0);
expect(grain).not.toHaveAttribute("data-over-projects");
});
it("path: a screen that is off the window -> does not count", () => {
const screen = addScreen("ds-frame");
const { grain } = renderGrain();
intersect(screen, false);
expect(grain).not.toHaveAttribute("data-over-projects");
});
it("path: one screen off the window and another in view -> counts (any is enough), and leaves only when the last one leaves", () => {
const far = addScreen("ds-reel");
const near = addScreen("ds-phone");
const { grain } = renderGrain();
intersect(far, false);
intersect(near, true);
expect(grain).toHaveAttribute("data-over-projects");
intersect(far, true);
intersect(near, false);
expect(grain).toHaveAttribute("data-over-projects");
intersect(far, false);
expect(grain).not.toHaveAttribute("data-over-projects");
});
it("path: no project screen on the page -> the grain stays", () => {
const { grain } = renderGrain();
expect(grain).not.toHaveAttribute("data-over-projects");
});
});
describe("FilmGrain: screens that appear and disappear", () => {
it("path: a screen the page creates after the grain mounted (the pinned reel swapping the list) is observed too", async () => {
const { grain } = renderGrain();
expect(observers[0].observed.size).toBe(0);
const reel = addScreen("ds-reel");
await flush();
expect(observers[0].observed.has(reel)).toBe(true);
intersect(reel, true);
expect(grain).toHaveAttribute("data-over-projects");
});
it("path: a screen that is removed while in view stops counting", async () => {
const reel = addScreen("ds-reel");
const { grain } = renderGrain();
intersect(reel, true);
expect(grain).toHaveAttribute("data-over-projects");
reel.remove();
await flush();
expect(observers[0].observed.has(reel)).toBe(false);
expect(grain).not.toHaveAttribute("data-over-projects");
});
it("path: the same screen is never observed twice", async () => {
addScreen("ds-reel");
renderGrain();
expect(observers[0].calls).toBe(1);
addScreen("ds-phone");
await flush();
// só a tela nova foi pedida ao observador; a primeira não se repete
expect(observers[0].calls).toBe(2);
expect(observers[0].observed.size).toBe(2);
});
});
describe("FilmGrain: no work per frame, cleanup", () => {
it("path: nothing listens to the scroll or the window size, and the page is never measured", () => {
const screen = addScreen("ds-reel");
const measure = jest.spyOn(screen, "getBoundingClientRect");
const listen = jest.spyOn(window, "addEventListener");
renderGrain();
act(() => {
window.dispatchEvent(new Event("scroll"));
window.dispatchEvent(new Event("resize"));
});
expect(measure).not.toHaveBeenCalled();
const types = listen.mock.calls.map(([type]) => type);
expect(types).not.toContain("scroll");
expect(types).not.toContain("resize");
});
it("path: unmount -> the observers are disconnected and later screens are ignored", async () => {
const { unmount } = renderGrain();
unmount();
expect(observers[0].disconnect).toHaveBeenCalledTimes(1);
addScreen("ds-reel");
await flush();
expect(observers[0].observed.size).toBe(0);
});
it("path: a browser without IntersectionObserver -> the grain renders and simply stays", () => {
// biome-ignore lint/suspicious/noExplicitAny: simula um navegador sem a API
(global as any).IntersectionObserver = undefined;
addScreen("ds-reel");
const { grain } = renderGrain();
expect(grain).toHaveClass("film-grain");
expect(grain).not.toHaveAttribute("data-over-projects");
});
});