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(null); const inView = useInView(ref, { amount, once }); return
; } 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(); 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(); 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(); 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(); 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(); 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(); expect(state(container)).toBe("true"); }); it("disconnects the observer when the component unmounts", () => { const { unmount } = render(); unmount(); expect(observers[0].disconnect).toHaveBeenCalled(); }); });