Mock intersections
Drive deterministic observer callbacks and thresholds in Vitest, Jest, or another DOM-capable test runner.
Use react-intersection-observer/test-utils when the assertion is about a callback, state transition, or threshold branch rather than real layout. The utilities replace window.IntersectionObserver, remember which elements each mock observer watches, and let a helper deliver controlled entries to the registered callback.
Set up the mock
Importing test-utils in a DOM test environment can set the mock up for you. When the module finds global beforeEach, afterEach, and either vi or jest, it registers its own hooks. The beforeEach hook installs the observer mock with that runner’s fn. The afterEach hook clears both the mock calls and the observed elements, so nothing carries between tests. Put the import in a setup file so every relevant test gets the same lifecycle.
Install a DOM emulator first. happy-dom is usually faster, and jsdom may fit an existing suite better. Neither one proves layout, scrolling, or native observer delivery.
pnpm add -D happy-dom
# or: pnpm add -D jsdom
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "happy-dom", // "jsdom" works too
setupFiles: ["./test/setup.ts"],
},
});
// test/setup.ts
import "react-intersection-observer/test-utils";// jest.config.js
module.exports = {
testEnvironment: "jsdom",
setupFilesAfterEnv: ["react-intersection-observer/test-utils"],
};The mock does not care which runner you use. Jest is not tied to jsdom, and Vitest works with either jsdom or happy-dom. In a mixed Vitest suite, keep this setup in a dom project so it never reaches the Browser Mode tests.
Manual setup without globals
If your runner does not expose global test APIs, or you prefer importing them, register the lifecycle yourself. setupIntersectionMocking takes the runner’s mock factory, and resetIntersectionMocking clears it after every test.
import { afterEach, beforeEach, vi } from "vitest";
import {
resetIntersectionMocking,
setupIntersectionMocking,
} from "react-intersection-observer/test-utils";
beforeEach(() => setupIntersectionMocking(vi.fn));
afterEach(resetIntersectionMocking);import { afterEach, beforeEach, jest } from "@jest/globals";
import {
resetIntersectionMocking,
setupIntersectionMocking,
} from "react-intersection-observer/test-utils";
beforeEach(() => setupIntersectionMocking(jest.fn));
afterEach(resetIntersectionMocking);Drive a transition
With either setup in place, import a helper and trigger the observer state you need:
import { render } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { useOnInView } from "react-intersection-observer";
import { mockAllIsIntersecting } from "react-intersection-observer/test-utils";
function Probe({ onSeen }: { onSeen: (inView: boolean) => void }) {
const ref = useOnInView((inView) => onSeen(inView));
return <div ref={ref}>Observed</div>;
}
test("ignores the initial false notification", () => {
const onSeen = vi.fn();
render(<Probe onSeen={onSeen} />);
mockAllIsIntersecting(false);
expect(onSeen).not.toHaveBeenCalled();
mockAllIsIntersecting(true);
expect(onSeen).toHaveBeenCalledWith(true);
});
Pass a boolean to enter or leave. A number selects which configured thresholds the observer has crossed. It is not a layout ratio: the mock reports the highest crossed threshold as entry.intersectionRatio. Assert threshold behavior, not raw geometry or an arbitrary percentage.
These examples work the same in Vitest and Jest. Switch to Browser Mode when a test has to prove real browser behavior.
Utilities
| Utility | Use |
|---|---|
mockAllIsIntersecting(value) |
Trigger every observed element. |
mockIsIntersecting(element, value) |
Trigger one observed element. |
intersectionMockInstance(element) |
Inspect the mock observer for an element. |
setupIntersectionMocking(mockFn) |
Install the mock yourself with vi.fn, jest.fn, or a compatible factory. |
resetIntersectionMocking() |
Clear observed elements and mock calls between tests. |
destroyIntersectionMocking() |
Restore the native observer after mocking it. |