/**
* Mock implementation of IntersectionObserver for jsdom testing environment.
*
* jsdom does not implement IntersectionObserver, so this mock allows testing
* components that rely on intersection detection (e.g., lazy loading, view tracking).
*
* @example
* ```typescript
* import {
* setupIntersectionObserverMock,
* teardownIntersectionObserverMock,
* resetIntersectionObserverMock,
* triggerIntersection,
* triggerAllIntersections,
* } from '../test-utils/IntersectionObserverMock';
*
* describe('MyComponent', () => {
* beforeAll(() => {
* setupIntersectionObserverMock();
* });
*
* afterAll(() => {
* teardownIntersectionObserverMock();
* });
*
* beforeEach(() => {
* resetIntersectionObserverMock();
* });
*
* it('tracks element visibility', async () => {
* render();
* const element = screen.getByTestId('tracked-element');
*
* triggerIntersection(element);
*
* expect(onVisibleSpy).toHaveBeenCalled();
* });
* });
* ```
*/
// Track all observer instances so we can trigger intersections on any of them
const observerInstances: Set = new Set();
class MockIntersectionObserverInstance implements IntersectionObserver {
root: Element | Document | null = null;
rootMargin: string = '';
thresholds: readonly number[] = [];
private callback: IntersectionObserverCallback;
private observedElements: Set = new Set();
constructor(callback: IntersectionObserverCallback) {
this.callback = callback;
observerInstances.add(this);
}
observe(target: Element) {
this.observedElements.add(target);
}
unobserve(target: Element) {
this.observedElements.delete(target);
}
disconnect() {
this.observedElements.clear();
observerInstances.delete(this);
}
takeRecords(): IntersectionObserverEntry[] {
return [];
}
/**
* Test helper: trigger intersection for a specific element on this observer.
* Calls the observer callback with isIntersecting: true for the given element.
*/
triggerIntersection(element: Element) {
if (!this.observedElements.has(element)) {
return;
}
const entry = {
target: element,
isIntersecting: true,
intersectionRatio: 1,
boundingClientRect: element.getBoundingClientRect(),
intersectionRect: element.getBoundingClientRect(),
rootBounds: null,
time: Date.now(),
} as IntersectionObserverEntry;
this.callback([entry], this);
}
/**
* Test helper: trigger intersection for all observed elements on this observer.
*/
triggerAllIntersections() {
this.observedElements.forEach((element) =>
this.triggerIntersection(element),
);
}
}
/**
* Trigger intersection for a specific element across all observer instances.
* Use this when you know which element you want to mark as visible.
*/
export const triggerIntersection = (element: Element) => {
observerInstances.forEach((observer) =>
observer.triggerIntersection(element),
);
};
/**
* Trigger intersection for all observed elements across all observer instances.
* Use this to simulate all tracked elements becoming visible at once.
*/
export const triggerAllIntersections = () => {
observerInstances.forEach((observer) => observer.triggerAllIntersections());
};
/**
* Set up the IntersectionObserver mock on the global object.
* Call this in beforeAll() before your tests run.
*/
export const setupIntersectionObserverMock = () => {
observerInstances.clear();
global.IntersectionObserver = MockIntersectionObserverInstance;
};
/**
* Tear down the IntersectionObserver mock from the global object.
* Call this in afterAll() after your tests complete.
*/
export const teardownIntersectionObserverMock = () => {
observerInstances.clear();
delete (global as any).IntersectionObserver;
};
/**
* Reset all observer instances between tests.
* Call this in beforeEach() to ensure test isolation.
*/
export const resetIntersectionObserverMock = () => {
observerInstances.clear();
};
/**
* Get all currently active observer instances.
* Useful for debugging or advanced test scenarios.
*/
export const getObserverInstances = () => observerInstances;