diff --git a/README.md b/README.md index 2b350f6d..242c9201 100644 --- a/README.md +++ b/README.md @@ -251,12 +251,13 @@ will emulate the real IntersectionObserver, allowing you to validate that your components are behaving as expected. | Method | Description | -| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `mockAllIsIntersecting(isIntersecting)` | Set `isIntersecting` on all current Intersection Observer instances. The value of `isIntersecting` should be either a `boolean` or a threshold between 0 and 1. | | `mockIsIntersecting(element, isIntersecting)` | Set `isIntersecting` for the Intersection Observer of a specific `element`. The value of `isIntersecting` should be either a `boolean` or a threshold between 0 and 1. | | `intersectionMockInstance(element)` | Call the `intersectionMockInstance` method with an element, to get the (mocked) `IntersectionObserver` instance. You can use this to spy on the `observe` and`unobserve` methods. | | `setupIntersectionMocking(mockFn)` | Mock the `IntersectionObserver`, so we can interact with them in tests - Should be called in `beforeEach`. (**Done automatically in Jest environment**) | -| `resetIntersectionMocking()` | Reset the mocks on `IntersectionObserver` - Should be called in `afterEach`. (**Done automatically in Jest environment**) | +| `resetIntersectionMocking()` | Reset the mocks on `IntersectionObserver` - Should be called in `afterEach`. (**Done automatically in Jest/Vitest environment**) | +| `destroyIntersectionMocking()` | Destroy the mocked `IntersectionObserver` function, and return `window.IntersectionObserver` to the original browser implementation | ### Testing Libraries diff --git a/src/__tests__/hooks.test.tsx b/src/__tests__/hooks.test.tsx index 6c3c5107..8d307b35 100644 --- a/src/__tests__/hooks.test.tsx +++ b/src/__tests__/hooks.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react"; import React, { useCallback } from "react"; import { type IntersectionOptions, defaultFallbackInView } from "../index"; import { + destroyIntersectionMocking, intersectionMockInstance, mockAllIsIntersecting, mockIsIntersecting, @@ -342,6 +343,7 @@ test("should set intersection ratio as the largest threshold smaller than trigge }); test("should handle fallback if unsupported", () => { + destroyIntersectionMocking(); // @ts-ignore window.IntersectionObserver = undefined; const { rerender } = render( @@ -363,6 +365,7 @@ test("should handle fallback if unsupported", () => { }); test("should handle defaultFallbackInView if unsupported", () => { + destroyIntersectionMocking(); // @ts-ignore window.IntersectionObserver = undefined; defaultFallbackInView(true); @@ -383,3 +386,12 @@ test("should handle defaultFallbackInView if unsupported", () => { `[TypeError: IntersectionObserver is not a constructor]`, ); }); + +test("should restore the browser IntersectingObserver", () => { + expect(vi.isMockFunction(window.IntersectionObserver)).toBe(true); + destroyIntersectionMocking(); + + // This should restore the original IntersectionObserver + expect(window.IntersectionObserver).toBeDefined(); + expect(vi.isMockFunction(window.IntersectionObserver)).toBe(false); +}); diff --git a/src/test-utils.ts b/src/test-utils.ts index 71339511..3be8ee9b 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -11,26 +11,32 @@ let isMocking = false; const observers = new Map(); +// Store a reference to the original `IntersectionObserver` so we can restore it later. +// This can be relevant if testing in a browser environment, where you actually have a native `IntersectionObserver`. +const originalIntersectionObserver = + typeof window !== "undefined" ? window.IntersectionObserver : undefined; + /* ** If we are running in a valid testing environment, we can automate mocking the IntersectionObserver. */ if ( typeof window !== "undefined" && typeof beforeAll !== "undefined" && + typeof beforeEach !== "undefined" && typeof afterEach !== "undefined" ) { - beforeAll(() => { - // Use the exposed mock function. Currently, only supports Jest (`jest.fn`) and Vitest with globals (`vi.fn`). + const initMocking = () => { + // Use the exposed mock function. Currently, it supports Jest (`jest.fn`) and Vitest with globals (`vi.fn`). // @ts-ignore if (typeof jest !== "undefined") setupIntersectionMocking(jest.fn); else if (typeof vi !== "undefined") { setupIntersectionMocking(vi.fn); } - }); + }; - afterEach(() => { - resetIntersectionMocking(); - }); + beforeAll(initMocking); + beforeEach(initMocking); + afterEach(resetIntersectionMocking); } function getActFn() { @@ -76,6 +82,7 @@ afterEach(() => { * @param mockFn The mock function to use. Defaults to `vi.fn`. */ export function setupIntersectionMocking(mockFn: typeof vi.fn) { + if (isMocking) return; window.IntersectionObserver = mockFn((cb, options = {}) => { const item = { callback: cb, @@ -122,6 +129,17 @@ export function resetIntersectionMocking() { observers.clear(); } +/** + * Destroy the IntersectionObserver mock function, and restore the original browser implementation of `IntersectionObserver`. + * You can use this to opt of mocking in a specific test. + **/ +export function destroyIntersectionMocking() { + resetIntersectionMocking(); + // @ts-ignore + window.IntersectionObserver = originalIntersectionObserver; + isMocking = false; +} + function triggerIntersection( elements: Element[], trigger: boolean | number,