Files
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

113 lines
3.4 KiB
TypeScript

import { act, render } from "@testing-library/react";
import FilmGrain from "./FilmGrain";
/* IntersectionObserver controlado: o teste decide quando cada tela de projeto entra ou sai da janela. */
type Observer = {
callback: IntersectionObserverCallback;
observed: Set<Element>;
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(), disconnect: jest.fn() };
observers.push(this.record);
}
observe(target: Element) {
this.record.observed.add(target);
}
unobserve(target: Element) {
this.record.observed.delete(target);
}
disconnect() {
this.record.disconnect();
}
takeRecords() {
return [];
}
} as unknown as typeof IntersectionObserver;
});
afterEach(() => {
global.IntersectionObserver = original;
document.body.innerHTML = "";
});
function addScreen(className: string) {
const element = document.createElement("div");
element.className = className;
document.body.append(element);
return element;
}
/** O navegador avisa que `target` entrou (ou saiu) da janela; `height` 0 é uma tela escondida por CSS. */
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,
);
});
}
describe("static cinematic effects", () => {
it("renders grain without a canvas", () => {
const grain = render(<FilmGrain />);
expect(grain.container.firstElementChild).toHaveAttribute(
"aria-hidden",
"true",
);
expect(grain.container.querySelector("canvas")).not.toBeInTheDocument();
});
});
describe("film grain over the project screens", () => {
it("steps aside while a project screen is in view and comes back when it leaves", () => {
const reel = addScreen("ds-reel");
const { container } = render(<FilmGrain />);
const grain = container.firstElementChild as HTMLElement;
expect(grain).not.toHaveAttribute("data-over-projects");
intersect(reel, true); // a vitrine entrou na janela
expect(grain).toHaveAttribute("data-over-projects");
intersect(reel, false); // passou
expect(grain).not.toHaveAttribute("data-over-projects");
});
it("also covers the browser and phone frames and the card screens", () => {
for (const className of ["ds-bframe", "ds-phone", "ds-frame"]) {
const screen = addScreen(className);
const { container, unmount } = render(<FilmGrain />);
intersect(screen, true);
expect(container.firstElementChild).toHaveAttribute("data-over-projects");
unmount();
document.body.innerHTML = "";
}
});
it("stays on when there is no project screen on the page", () => {
const { container } = render(<FilmGrain />);
expect(observers[0].observed.size).toBe(0);
expect(container.firstElementChild).not.toHaveAttribute(
"data-over-projects",
);
});
});