/** * 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;