Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow testing of errors thrown after render #828

Closed
lorensr opened this issue Nov 17, 2020 · 14 comments
Closed

Allow testing of errors thrown after render #828

lorensr opened this issue Nov 17, 2020 · 14 comments
Labels
question Further information is requested

Comments

@lorensr
Copy link
Contributor

lorensr commented Nov 17, 2020

Describe the feature you'd like:

I'd like to be able to write a passing test that verifies that when I fireEvent, an error is thrown. This test fails before it reaches the expect:

test('error is thrown', async () => {
  const { getByText } = render(<TodoApp />)
  await waitForDomChange();
  fireEvent.click(getByText('Add todo'));

  // test fails during this line
  await waitForDomChange(); 
  expect(1).toBeTruthy();
})

I'd like something to the effect of:

test('error is thrown', async () => {
  const { getByText } = render(<TodoApp />)
  await waitForDomChange();
  fireEvent.click(getByText('Add todo'));

  expect(await waitForDomChange()).toThrow();
})

Describe alternatives you've considered:

Adding an <ErrorBoundary> and testing both components together.

@kentcdodds
Copy link
Member

@lorensr,

First, I'll mention that waitForDomChange is deprecated and can be replaced by waitFor(() => {}) (keep in mind that an empty callback like that is also not recommended).

Secondly, I don't think that this is possible. To be able to catch an error, we have to have our own try/catch around it which we do not.

Your alternative is solid and that's what I'd recommend you do. Wrap it in an error boundary.

@lorensr
Copy link
Contributor Author

lorensr commented Nov 17, 2020

Thanks!

we have to have our own try/catch around it which we do not.

Is adding a try/catch inside RTL a possibility? I think it would be easier for me than wrapping in error boundary, and being able to test without the boundary component allows for better isolation. I've also found a few issues & SO questions that seem to be searching for the same thing I am.

@kentcdodds
Copy link
Member

Is adding a try/catch inside RTL a possibility?

I'm not sure where we would put it for it to be effective 🤔

I think it would be easier for me than wrapping in error boundary

I'm not sure why.

import {ErrorBoundary} from 'react-error-boundary'

test('error is thrown', async () => {
  const fallbackRender = jest.fn(() => null)
  render(
    <ErrorBoundary fallbackRender={fallbackRender}>
      <TodoApp />
    </ErrorBoundary>
  )
  fireEvent.click(await screen.findByText('Add todo'));

  await waitFor(() => expect(fallbackRender).toHaveBeenCalled())
  expect(fallbackRender.mock.calls[0].error).toMatchInlineSnapshot(/* jest will update this */)
})

Pretty sure that will work. That's how I would test it if it were me.

@kentcdodds
Copy link
Member

One other thing to consider is that it's unlikely this is desireable behavior. I'm not sure I understand the use case of expecting an error to occur in application code. I can understand it in library code though, and in this situation, this is how I'd go about it.

@nickserv nickserv added the question Further information is requested label Nov 19, 2020
@IanVS
Copy link

IanVS commented Jun 14, 2021

I'm finding that react error boundaries do not actually "catch" errors in development (or they are at least rethrown), so I still get uncaught errors which cause my tests to fail. And for whatever reason, using expect(() => {render(<Throws/>)}).toThrow() doesn't catch it either. Can anyone confirm that the approach above is working for them? Maybe it has something to do with running my tests in a real browser with @web/test-runner.

@DhrubajitPC
Copy link

@IanVS this might be a bit late but perhaps you are using create-react-app to scaffold your project. CRA shows an error overlay in development mode which you need to hide by pressing the escape key.

it("Should display error boundary when there is a rendering error", () => {
    render(
      <ErrorBoundary>
        <BuggyComponent flag={true} />
      </ErrorBoundary>
    );

    // need to disable overlay in dev mode for create-react-app
    fireEvent.keyDown(document.body, {
      key: "Escape",
      code: 27,
    });

     expect(screen.getByText(/mock test in error boundary/)).toBeInTheDocument();
  });

something like this might work

@cupcakearmy
Copy link

Cannot get it to work either, neither with expect(...).toThrow() or using error boundaries

@CoffeESIME
Copy link

@IanVS do you find a solution for that ? I'm having the same issue, i want to test for a element to thrown but it doesn't work, In every component that i want to test :c

@IanVS
Copy link

IanVS commented Mar 7, 2023

Nope, I don't have a solution, sorry. I just know some tests will fail in dev, and rely on my CI tests using a prod build.

@iambryanhaney
Copy link

It's worth noting that an ErrorBoundary only catches errors thrown from the render cycle.

If you're wanting to catch an error that is thrown from an actual event, I'm not sure how you could - something along the testing chain seems to catch them first.

@schorfES
Copy link

I believe the root cause of this issue is that render() is employing act() internally. This asynchronous rendering behavior explains why we are unable to capture errors from a rendering call using expect(() => render((<Throws/>))).toThrow(). The same issue arises when using <ErrorBoundary /> as a wrapper.

When testing errors thrown during rendering (and not involving useEffect, etc.), it might be more appropriate to treat the component as a regular function.

const TestComponent = () => {
  if (condition) {
    throw new Error('Ooooops!');
  }

  return (<div>Test component</div>);
};
it('should throw an error', () => {
  expect(() => {
    TestComponent({ ...props }, { ...state });
  }).toThrow('Ooooops!');
});

@dwjohnston
Copy link

dwjohnston commented May 28, 2024

@kentcdodds The use case I've got here from is:

I've got some page Foo, but this page should only be displayed if LaunchDarkly feature flag is enabled. If it's not enabled, we throw an error and let the error boundary catch it.
There is a brief moment that the LD client isn't initialised, in which case return a null. So the code looks like this:

function FooPage() {

    const flagsReady = useFlagsReady(); 
    const flagIsEnabled = useFlag("show-foo-page"); 

    if (!flagsReady) {
        return null; 
    }

    if(!isFlagIsEnabled){
        throw new Error("Flag is not enabled"); 
    }
    

    return <div>the rest of the component</div>
}

In this scenario the first render will be a null, and the second render should either be the thrown error, or the actual page render. But at this point I can't work how to 'wait till second render'.

@kentcdodds
Copy link
Member

Render it inside an error boundary and assert on the fallback

@eps1lon
Copy link
Member

eps1lon commented May 29, 2024

Will also be simpler in React 19 when we land #1297

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests