reactjsreact-testing-libraryjsdomvitest

Test `window.onerror` call with vitest and React


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?


Solution

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

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