skip to Main Content

I am unable to reach window.onerror in React tests with vitest, jsdom, and React Testing Library when throwing inside an event handler. The same test passes in Jest.

Goal: Assert that window.onerror is called when an error is thrown in an event handler (e.g., onClick). It seems that vitest with jsdom doesn’t propagate JS errors to window.onerror?

Vitest code

import { render, screen } from "@testing-library/react";
import ReactTestUtils from "react-dom/test-utils";
import { vi, it, expect } from "vitest";
import React from "react";

it("window.onerror is called on unhandled error in event handler", async () => {
  const spy = vi.spyOn(console, "error");
  spy.mockImplementation(() => undefined);

  const caught = vi.fn();
  const App = () => {
    return (
      <button
        onClick={() => {
          throw new Error("ahhhh");
        }}
      >
        Error
      </button>
    );
  };

  class Tracker extends React.Component<any, any> {
    componentDidMount() {
      window.onerror = (message, source, lineno, colno, error) => {
        caught();
        return true;
      };
    }

    render() {
      return this.props.children;
    }
  }

  render(
    <Tracker>
      <App />
    </Tracker>
  );

  expect(caught).toHaveBeenCalledTimes(0);
  const button = await screen.findByText("Error");

  try {
    // using ReactTestUtils here since it allows me to catch the error
    ReactTestUtils.Simulate.click(button);
  } catch (e) {
    // do nothing
  }

  expect(caught).toHaveBeenCalledTimes(1);
});

Dependencies

    "react": "^18.2.0",
    "react-dom": "^18.2.0"

    "@testing-library/react": "^14.1.2",
    "@vitejs/plugin-react": "^4.2.0",
    "jsdom": "^23.0.1",
    "vite": "^5.0.4",
    "vitest": "^0.34.6"

The test fails with the following error message:

 FAIL  index.test.tsx > window.onerror is called on unhandled error in event handler
AssertionError: expected "spy" to be called 1 times, but got 0 times
 ❯ index.test.tsx:52:18
     50|   }
     51| 
     52|   expect(caught).toHaveBeenCalledTimes(1);
       |                  ^
     53| });
     54| 

The same test passes with Jest (Jest code below). Note that the code is very much the same. I only replaced jest with vi.

Jest code

import { render, screen } from "@testing-library/react";
import ReactTestUtils from "react-dom/test-utils";
import { jest, it, expect } from "@jest/globals";
import React from "react";

it("window.onerror is called on unhandled error in event handler", async () => {
  const spy = jest.spyOn(console, "error");
  spy.mockImplementation(() => undefined);

  const caught = jest.fn();
  const App = () => {
    return (
      <button
        onClick={() => {
          throw new Error("ahhhh");
        }}
      >
        Error
      </button>
    );
  };

  class Tracker extends React.Component {
    componentDidMount() {
      window.onerror = (message, source, lineno, colno, error) => {
        caught();
        return true;
      };
    }

    render() {
      return this.props.children;
    }
  }

  render(
    <Tracker>
      <App />
    </Tracker>
  );

  expect(caught).toHaveBeenCalledTimes(0);
  const button = await screen.findByText("Error");

  try {
    // using ReactTestUtils here since it allows me to catch the error
    ReactTestUtils.Simulate.click(button);
  } catch (e) {
    // do nothing
  }

  expect(caught).toHaveBeenCalledTimes(1);
});

Dependencies

    "react": "^18.2.0",
    "react-dom": "^18.2.0"

    "@babel/preset-env": "^7.23.5",
    "@babel/preset-react": "^7.23.3",
    "@testing-library/react": "^14.1.2",
    "babel-jest": "^29.7.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "react-test-renderer": "^18.2.0"

Looking for ways to work around this. Any way I can reach window.onerror in vitest?

2

Answers


  1. Chosen as BEST ANSWER

    I was able to fix the vitest test by replacing window.onerror = with window.addEventListener('error', ...).

    componentDidMount() {
      window.addEventListener("error", (event) => {
        caught();
      });
    }
    

  2. In your vitest setup, you can manually mock window.onerror before the test and then assert whether it was called:

    it("window.onerror is called on unhandled error in event handler", async () => {
      // ... other setup ...
    
      // Mock window.onerror
      const originalOnError = window.onerror;
      window.onerror = vi.fn();
    
      // ... rendering and event simulation ...
    
      // Check if window.onerror was called
      expect(window.onerror).toHaveBeenCalledTimes(1);
    
      // Restore original window.onerror
      window.onerror = originalOnError;
    });
    

    Create an error boundary component and use it to capture errors:

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        return { hasError: true };
      }
    
      componentDidCatch(error, errorInfo) {
        // You can log the error here
        this.props.onError();
      }
    
      render() {
        if (this.state.hasError) {
          // Render fallback UI
          return <h1>Something went wrong.</h1>;
        }
    
        return this.props.children;
      }
    }
    
    it("error boundary catches error", async () => {
      const onError = vi.fn();
    
      render(
        <ErrorBoundary onError={onError}>
          <App />
        </ErrorBoundary>
      );
    
      // ... simulate the click ...
    
      expect(onError).toHaveBeenCalledTimes(1);
    });
    

    If you have control over the error-throwing logic, you can dispatch a custom event and listen for that in your tests:

    // Inside your component
    const handleClick = () => {
      try {
        throw new Error("ahhhh");
      } catch (e) {
        const event = new CustomEvent("customError", { detail: e });
        window.dispatchEvent(event);
      }
    };
    
    // In your test
    window.addEventListener("customError", caught);
    
    // ... simulate the click ...
    
    expect(caught).toHaveBeenCalledTimes(1);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search