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

142 lines
4.0 KiB
TypeScript

import { act, render } from "@testing-library/react";
import { useRef } from "react";
import { useInView } from "./useInView";
type Observer = {
callback: IntersectionObserverCallback;
options?: IntersectionObserverInit;
observed: Element[];
disconnect: jest.Mock;
};
let observers: Observer[] = [];
const original = global.IntersectionObserver;
beforeEach(() => {
observers = [];
Object.defineProperty(window, "innerHeight", {
configurable: true,
value: 800,
});
global.IntersectionObserver = class {
private readonly record: Observer;
constructor(
callback: IntersectionObserverCallback,
options?: IntersectionObserverInit,
) {
this.record = { callback, options, observed: [], disconnect: jest.fn() };
observers.push(this.record);
}
observe(target: Element) {
this.record.observed.push(target);
}
unobserve() {}
disconnect() {
this.record.disconnect();
}
takeRecords() {
return [];
}
} as unknown as typeof IntersectionObserver;
});
afterEach(() => {
global.IntersectionObserver = original;
});
function report(ratio: number, visibleHeight: number) {
act(() => {
for (const observer of observers)
observer.callback(
[
{
isIntersecting: ratio > 0,
intersectionRatio: ratio,
intersectionRect: { height: visibleHeight },
target: observer.observed[0],
} as unknown as IntersectionObserverEntry,
],
{} as IntersectionObserver,
);
});
}
function Probe({ amount, once }: { amount?: number; once?: boolean }) {
const ref = useRef<HTMLDivElement>(null);
const inView = useInView(ref, { amount, once });
return <div ref={ref} data-in-view={String(inView)} />;
}
const state = (container: HTMLElement) =>
container.firstElementChild?.getAttribute("data-in-view");
describe("useInView", () => {
it("is false until enough of the element is on screen (20 % by default)", () => {
const { container } = render(<Probe />);
expect(state(container)).toBe("false");
expect(observers).toHaveLength(1);
expect(observers[0].observed).toHaveLength(1);
report(0.1, 40); // pouco à vista: ainda não
expect(state(container)).toBe("false");
report(0.25, 100);
expect(state(container)).toBe("true");
});
it("stays true after the element leaves (once), and stops observing", () => {
const { container } = render(<Probe />);
report(1, 300);
expect(state(container)).toBe("true");
expect(observers[0].disconnect).toHaveBeenCalled();
report(0, 0);
expect(state(container)).toBe("true");
});
it("follows the element in and out when once is false", () => {
const { container } = render(<Probe once={false} />);
report(1, 300);
expect(state(container)).toBe("true");
report(0, 0);
expect(state(container)).toBe("false");
report(0.5, 200);
expect(state(container)).toBe("true");
});
it("counts a very tall element once a fifth of the window is showing, even if that is a tiny share of it", () => {
const { container } = render(<Probe />);
report(0.02, 100); // 100 px de um bloco de 5000: menos de 20% da janela (800 px)
expect(state(container)).toBe("false");
report(0.04, 200); // 200 px >= 20% de 800
expect(state(container)).toBe("true");
});
it("asks the observer for the amount it was given", () => {
render(<Probe amount={0.7} />);
expect(observers[0].options?.threshold).toEqual([0, 0.7]);
});
it("is true at once when the browser has no IntersectionObserver (content is never left hidden)", () => {
// biome-ignore lint/suspicious/noExplicitAny: simula um navegador sem a API
(global as any).IntersectionObserver = undefined;
const { container } = render(<Probe />);
expect(state(container)).toBe("true");
});
it("disconnects the observer when the component unmounts", () => {
const { unmount } = render(<Probe />);
unmount();
expect(observers[0].disconnect).toHaveBeenCalled();
});
});