Test `window.onerror` call with vitest and React

172 views Asked by At

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

There are 2 answers

0
Andre On BEST ANSWER

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

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

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);