sodcalculator

10 Tech How-To

Testing React Components with Browser APIs

Guide 10-12297 · Updated · Step by step

This guide walks through testing React components that rely on browser APIs like window, document, localStorage, matchMedia, or IntersectionObserver. You’ll learn how to mock these APIs in Jest and Testing Library so your tests run reliably in Node.js, which lacks a real browser environment.

What You’ll Need

  • A React project with Jest and React Testing Library installed (Create React App includes these by default).
  • Basic familiarity with writing Jest tests and using render, screen, and fireEvent from Testing Library.
  • Node.js 14 or newer.

Step-by-Step

  1. Identify which browser APIs your component uses. Search your component for direct references to window, document, navigator, localStorage, sessionStorage, matchMedia, IntersectionObserver, ResizeObserver, requestAnimationFrame, or fetch. List them so you know what to mock.
  2. Set up a test file. Create a file like MyComponent.test.js next to your component. Import the component and Testing Library utilities at the top.
  3. Mock a global API with jest.spyOn. For simple methods like window.scrollTo, use jest.spyOn(window, 'scrollTo').mockImplementation(() => {}) inside a beforeEach block. This replaces the method with a no-op and restores it automatically after each test if you call jest.restoreAllMocks() in afterEach.
  4. Mock properties with Object.defineProperty. For read-only properties like window.innerWidth or navigator.clipboard, use Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1024 }). This lets you change the value per test.
  5. Mock localStorage and sessionStorage. Create a simple in-memory storage mock in a beforeEach:
const storageMock = () => {
  let store = {};
  return {
    getItem: jest.fn((key) => store[key] ?? null),
    setItem: jest.fn((key, value) => { store[key] = String(value); }),
    removeItem: jest.fn((key) => { delete store[key]; }),
    clear: jest.fn(() => { store = {}; }),
  };
};
Object.defineProperty(window, 'localStorage', { value: storageMock() });
Object.defineProperty(window, 'sessionStorage', { value: storageMock() });
  1. Mock matchMedia for responsive components. Since matchMedia is not implemented in jsdom, define it as a function that returns an object with matches, addListener, removeListener, addEventListener, and removeEventListener methods. Use jest.fn() for each method.
  2. Mock IntersectionObserver and ResizeObserver. These are also missing in jsdom. Create a class mock with a constructor that captures the callback, and methods observe, unobserve, and disconnect as jest.fn(). You can then trigger the callback manually in tests to simulate intersection changes.
  3. Mock fetch for network calls. Use jest.spyOn(global, 'fetch').mockResolvedValue({ json: async () => ({ data: 'mocked' }) }). For more complex scenarios, consider msw (Mock Service Worker) but a simple mock is fine for most cases.
  4. Write your test. Render the component, interact with it using fireEvent or userEvent, and assert on the output. For example, if the component reads localStorage on mount, set a value before rendering and check that the component displays it.
  5. Clean up after each test. In afterEach, call jest.clearAllMocks() and jest.restoreAllMocks(). Also reset any custom property values you set with Object.defineProperty to avoid leaking state between tests.

Common Mistakes

  • Forgetting to restore mocks after tests, causing tests to interfere with each other.
  • Mocking too broadly (e.g., mocking the entire window object) instead of just the specific API your component uses.
  • Not setting writable: true and configurable: true in Object.defineProperty, leading to errors when trying to reassign.
  • Assuming jsdom implements all browser APIs—it does not. Always check if the API exists in the test environment.
  • Using jest.fn() without a default implementation when the component expects a return value (e.g., matchMedia must return an object).

Tips

  • Create a setupTests.js file (CRA uses this) to define global mocks for APIs used across many components, like matchMedia or IntersectionObserver.
  • Use jest.spyOn instead of directly assigning to window.method—it gives you automatic access to the original implementation and easier restoration.
  • For matchMedia, make the matches property configurable per test by storing it in a variable that you can change before rendering.
  • When testing IntersectionObserver, expose the callback via a mock class and call it manually to simulate an element becoming visible.
  • Keep your mocks minimal—only mock what the component actually uses. Over-mocking can hide real integration issues.

Troubleshooting

Test fails with “TypeError: Cannot read property ‘addEventListener’ of undefined�. This usually means you forgot to mock matchMedia or IntersectionObserver. Add the mock in beforeEach.

Test fails with “ReferenceError: localStorage is not defined�. jsdom does provide localStorage, but if you’re using a custom environment or have cleared it, define it as shown in step 5.

Component renders but doesn’t update when you change a mock value. Ensure you’re re-rendering the component after changing the mock. Use rerender from Testing Library or unmount/remount.

Your mock implementation is called but the component still throws. Check that the mock returns the correct shape. For example, fetch must return a promise that resolves to an object with a json method.

Tests pass in isolation but fail when run together. This is a classic sign of mock leakage. Add jest.restoreAllMocks() and reset any Object.defineProperty values in afterEach.

Frequently Asked Questions

Do I need to mock window itself?

No, only the specific properties or methods your component uses. Mocking the whole window is unnecessary and can break other things.

Can I use real browser APIs in tests?

Not in jsdom, which is the default test environment for Create React App. You must mock them. If you need real browser behavior, use a tool like Cypress for end-to-end tests.

How do I mock window.location?

Use Object.defineProperty(window, 'location', { value: { href: 'https://example.com', ... }, writable: true }). Be careful to include all properties your component reads.

What about document.querySelector?

Testing Library encourages querying rendered DOM elements via screen, so you shouldn’t need to mock querySelector. If your component uses it directly, you can spy on it, but it’s better to refactor the component to use refs or Testing Library queries.

Is it okay to mock fetch globally?

Yes, but be aware that it will affect all components in that test file. If you need different responses for different tests, use mockResolvedValueOnce or change the implementation inside each test.

How do I test a component that uses requestAnimationFrame?

Mock it with jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(cb, 0)) to run the callback synchronously, or use jest.useFakeTimers() and advance timers manually.

Related

General steps for current mainstream versions; menus can differ by device and version. See the disclaimer or report an error.

More in this bed

The whole Tech How-To bed →