React Testing Library - testing in isolation and userEvent error - reactjs

I'm writing tests using Jest and React Testing Library. I had failing tests and I realized that if I change the order of the tests, it will work. I'm guessing this is because the tests aren't properly isolated and one test might affect the other.
I am calling:
afterEach(() => {
cleanup()
jest.resetAllMocks()
})
I have a test that looks like this:
it('calls API when submitted', async () => {
render(<SignUp />)
fillAllVerificationFieldsWithTestData()
validateUser.mockResolvedValueOnce({ id: 123 })
const signupButton = screen.getByTestId(
'sign-up-verification-button',
)
userEvent.click(signupButton)
await waitFor(() => expect(validateUser).toHaveBeenCalledTimes(1))
})
If I create the exact same test or run a similar test after this one with a userEvent.click, I get an error:
Unable to fire a "mouseMove" event - please provide a DOM element.
I looked in the #testing-library/user-event library and I see this code:
const userEvent = {
click(element) {
const focusedElement = element.ownerDocument.activeElement;
const wasAnotherElementFocused =
focusedElement !== element.ownerDocument.body &&
focusedElement !== element;
if (wasAnotherElementFocused) {
fireEvent.mouseMove(focusedElement);
fireEvent.mouseLeave(focusedElement);
}
I noticed that element.ownerDocument.activeElement is null wasAnotherElementFocused is true and so it throws the error.
The first time I run the test it isn't null so it works.
Do I need some extra clean up between tests? If I use fireEvent:
fireEvent(signupButton,
new MouseEvent('click', {
bubbles: true,
}),
)
It works but I'm afraid I'm doing something wrong and not isolating my tests correctly.
EDIT:
Here is the code for the fillAllVerificationFieldsWithTestData:
export const fillAllVerificationFieldsWithTestData = () => {
const { given_name, family_name, zip, social, loanNumber } = {
given_name: screen.getByTestId('given_name'),
family_name: screen.getByTestId('family_name'),
zip: screen.getByTestId('zip'),
social: screen.getByTestId('last4ssn'),
loanNumber: screen.getByTestId('loan_number'),
}
userEvent.type(given_name, 'FirstName')
userEvent.type(family_name, 'LastName')
userEvent.type(zip, '77025')
userEvent.type(social, '1234')
userEvent.type(loanNumber, '1112223333')
}
and screen is imported from #testing-library/react and I import validate user like this:
import { validateUser } from '../../../services/auth'
jest.mock('../../../services/auth')

So, I just faced this problem, and what fixed it for me was wrapping any code that causes a React state change (which your userEvent code presumably does) in act. So, your initial test would look like this:
it('calls API when submitted', async () => {
render(<SignUp />)
fillAllVerificationFieldsWithTestData()
validateUser.mockResolvedValueOnce({ id: 123 })
const signupButton = screen.getByTestId(
'sign-up-verification-button',
)
await act(() => userEvent.click(signupButton))
await waitFor(() => expect(validateUser).toHaveBeenCalledTimes(1))
})
Can you give this a try, applying similar changes to your fillAllVerificationFieldsWithTestData function, and let us know how it turned out?

This is a bug that was reported in the testing-library/user-event GitHub and it should be fixed as of v11.0.1. It's specific to userEvent, which is why fireEvent works.
Note that you shouldn't need to call cleanup if you're using Jest because it will get called for you.

Related

Cypress: Not able to stub with a basic example. What might i be missing?

For some reason, I am not able to stub this here. I have reduced my code to almost exactly this here. This should work according to the docs, but I'm wondering if I'm missing a finer detail of typescript / react hooks? That doesn't feel at all like the case but who knows. Thanks ahead of time if you're reading this and taking up your time. I appreciate what you do. Here's my example:
// connection.ts (the method to stub)
export const connectFn = async () => {
return 'i should not be here'
}
// ./reactHook.ts
import { connectFn } from './connection'
export const useMyHook = () => {
const handleConnect = async () => {
const result = await connectFn()
console.log(result)
// expected: 'i have been stubbed!'
// actual: 'i should not be here
}
return {
handleConnect
}
}
// ./App.ts
export const App = () => {
const { handleConnect } = useMyHook()
return <button onClick={handleConnect}>Connect</button>
}
// ./cypress/integration/connection.spec.ts
import * as myConnectModule from '../../connection'
describe('stub test', () => {
it('stubs the connectFn', () => {
cy.stub(myConnectModule, 'connectFn').resolves('i have been stubbed!')
cy.get('[data-testid=connect-btn]').click()
// assertions about the view here...
})
})
I thought I understood the cypress and Sinon docs pretty well. I have read that cypress needs an object to a stub from and not the named export directly - hence the * as an import. I have also tried every which way to export it as well. I used their example from the docs directly to no avail. I then thought it may be a typescript issue but it doesn't seem to be the case. I have reduced my actual code to pretty much this example here. I have even removed everything but the logs for testing and I'm still not getting it.
To stub successfully you need to stub (replace) the same instance.
In reactHook.ts
import { connectFn } from './connection'
if (window.Cypress) {
window.connectFn = connectFn
}
In the test
cy.window().then(win => {
cy.stub(win, 'connectFn').resolves('i have been stubbed!')
cy.get('[data-testid=connect-btn]').click()
...
}}
BTW useMyHook implies a React hook, if so you may need a cy.wait(0) to release the main JS thread and allow the hook to run.

Should you render components / select elements in each `test()/it()` block or globally?

In react-testing-library you have to render your react component before executing some tests on its elements.
For several tests on the same component, should you avoid
rendering the component multiple times? Or do you have to render it in each
test()/it() block?
Should you select elements of the component (e.g. button) in each test()/it() block, or should you lift the selection, and select only once?
Does it have any impact on the execution time of the tests?
Is one of the approaches a best practice/antipattern?
Why does the last example fail?
For the basic component I have the following testing approaches:
function MyComponent() {
return (
<>
<button disabled>test</button>
<button disabled>another button</button>
</>
);
}
e.g.
describe("MyComponent", () => {
it("renders", async () => {
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
expect(button).toBeInTheDocument();
});
it("is disabled", async () => {
// repetetive render and select, should be avoided or adopted?
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
expect(button).toBeDisabled();
});
});
vs.
describe("MyComponent", () => {
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
it("renders", async () => {
expect(button).toBeInTheDocument();
});
it("is disabled", async () => {
expect(button).toBeDisabled();
});
});
I would expect the second approach to have a faster execution time since the component has to be rendered only once, but I don't know how to measure it and if it is an anti-pattern?
While it seems to be more DRY, if I add another toBeInTheDocument check, it fails.
Why is this the case?
describe("MyComponent", () => {
const { getByRole } = render(<MyComponent />);
const button = screen.getByRole("button", { name: /test/i });
const button2 = screen.getByRole("button", { name: /another button/i });
it("renders", async () => {
expect(button).toBeInTheDocument(); //ok
});
it("is disabled", async () => {
expect(button).toBeDisabled(); // ok
});
it("renders second button", async () => {
expect(button2).toBeInTheDocument(); // fails: element could not be found in the document
});
});
So this approach seems to be more error-prone!?
Each test should be as atomic as possible, meaning that it should not be using anything that other tests are also using and should run with a fresh state. So relating that to your examples, the first one would be the correct pattern.
When you have a test suite that contains sharable state between unit tests e.g. objects or environment variables, the test suite is very prone to errors. The reason for that is; if one of the unit tests happens to mutate one of the shared objects; all of the other unit tests will also be affected by this, causing them to exhibit unwanted behaviour. This can result in test failures where the code is technically correct or even set up landmines for future developers where the addition of new tests which are correct would still result in failures, hence causing major headaches in figuring out why this is happening.
The only exception to this rule would be immutable primitive variables (e.g. string, number, boolean with the use of const keyword) as tests will not be able to mutate them and they are useful for storing reusable ids, text etc.
Ofcourse, repeating the setup of each unit test can make them really clunky, that's why jest offers the beforeEach, beforeAll, afterEach and afterAll functions to extract the repeating logic. However, this opens up the vulnerability of shared state, so do be careful and make sure that all state is refreshed before any tests are kicked off. Ref.
For the last question as to why your last unit test in the last example is failing - it appears that you are using getByRole to look for button text. You should be using getByText instead. getByRole is used with role attributes (e.g. <button role="test">test</button>) which you don't seem to be using.

React & Enzyme/Jest: beforeEach() to group variables & use before in each unit test best practice?

I am working on my first react unit test and would like to know if there is a best practice for grouping variables used in every unit test? I have a group of unit tests for a form that uses the same variables. I grouped them in an 'describe(){}' and would like to have the variables at the beginning of each test. My approach is below, but I am receiving an error that says 'ReferenceError: input is not defined'.
If I do not wrap them in a beforeEach(), then I receive errors for the 'screen.getByText' lines that it was 'Unable to find an element with the text' even though it's wrapped in an await.
The tests run fine if I have the variables repeated in each test, but that would be a lot of duplicated code as I have 6 tests within the describe().
import { render, fireEvent, wait, cleanup, screen } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
describe('testing subscription form validation', () => {
beforeEach(() => {
let handleChange = jest.fn();
let {getByTestId} = render(<Form handleChange={handleChange}/>);
let input = getByTestId('test-input');
let button = screen.getByTestId('test-button');
});
it('[some test description]', async () => {
fireEvent.change(input, { target: { value: '[test value]' } });
fireEvent.click(button);
expect(screen.getByText('[button text]')).toBeInTheDocument();
await wait(() => expect(input).toHaveAttribute('value', 'test value'));
});
it('[another test]', async () => {
fireEvent.change(input, { target: { value: '' } });
fireEvent.click(button);
await wait(() => {
const requiredText = screen.getByText('This field is required');
expect(requiredText).toBeInTheDocument();
});
});
});
Thank you for taking the time to help! Any guidance would be great.
You should dclare the variables outside the function and assign to them inside your beforeEach(), like so:
describe('hdgsbdicg', () => {
let input;
bedoreEach(() => {
input=jest.fn(); //or whatever
}
});

When testing, code that causes React state updates should be wrapped into act

I have this test:
import {
render,
cleanup,
waitForElement
} from '#testing-library/react'
const TestApp = () => {
const { loading, data, error } = useFetch<Person>('https://example.com', { onMount: true });
return (
<>
{loading && <div data-testid="loading">loading...</div>}
{error && <div data-testid="error">{error.message}</div>}
{data &&
<div>
<div data-testid="person-name">{data.name}</div>
<div data-testid="person-age">{data.age}</div>
</div>
}
</>
);
};
describe("useFetch", () => {
const renderComponent = () => render(<TestApp/>);
it('should be initially loading', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('loading')).toBeDefined();
})
});
The test passes but I get the following warning:
Warning: An update to TestApp inside a test was not wrapped in
act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser
in TestApp
console.error
node_modules/react-dom/cjs/react-dom.development.js:506
Warning: An update to TestApp inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser
in TestApp
The key is to await act and then use async arrow function.
await act( async () => render(<TestApp/>));
Source:
https://stackoverflow.com/a/59839513/3850405
Try asserting inside 'await waitFor()' - for this your it() function should be async
it('should be initially loading', async () => {
const { getByTestId } = renderComponent();
await waitFor(() => {
expect(getByTestId('loading')).toBeDefined();
});
});
Keep calm and happy coding
I was getting the same issue which gets resolved by using async queries (findBy*) instead of getBy* or queryBy*.
expect(await screen.findByText(/textonscreen/i)).toBeInTheDocument();
Async query returns a Promise instead of element, which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms. If you need to find more than one element, use findAllBy.
https://testing-library.com/docs/dom-testing-library/api-async/
But as you know it wont work properly if something is not on screen. So for queryBy* one might need to update test case accordingly
[Note: Here there is no user event just simple render so findBy will work otherwise we need to put user Event in act ]
Try using await inside act
import { act } from 'react-dom/test-utils';
await act(async () => {
wrapper = mount(Commponent);
wrapper.find('button').simulate('click');
});
test('handles server ok', async () => {
render(
<MemoryRouter>
<Login />
</MemoryRouter>
)
await waitFor(() => fireEvent.click(screen.getByRole('register')))
let domInfo
await waitFor(() => (domInfo = screen.getByRole('infoOk')))
// expect(domInfo).toHaveTextContent('登陆成功')
})
I solved the problem in this way,you can try it.
I don't see the stack of the act error, but I guess, it is triggered by the end of the loading when this causes to change the TestApp state to change and rerender after the test finished. So waiting for the loading to disappear at the end of the test should solve this issue.
describe("useFetch", () => {
const renderComponent = () => render(<TestApp/>);
it('should be initially loading', async () => {
const { getByTestId } = renderComponent();
expect(getByTestId('loading')).toBeDefined();
await waitForElementToBeRemoved(() => queryByTestId('loading'));
});
});
React app with react testing library:
I tried a lot of things, what worked for me was to wait for something after the fireevent so that nothing happens after the test is finished.
In my case it was a calendar that opened when the input field got focus. I fireed the focus event and checked that the resulting focus event occured and finished the test. I think maybe that the calendar opened after my test was finished but before the system was done, and that triggered the warning. Waiting for the calendar to show before finishing did the trick.
fireEvent.focus(inputElement);
await waitFor(async () => {
expect(await screen.findByText('December 2022')).not.toBeNull();
});
expect(onFocusJestFunction).toHaveBeenCalledTimes(1);
// End
Hopes this helps someone, I just spent half a day on this.
This is just a warning in react-testing-library (RTL). you do not have to use act in RTL because it is already using it behind the scenes. If you are not using RTL, you have to use act
import {act} from "react-dom/test-utils"
test('',{
act(()=>{
render(<TestApp/>)
})
})
You will see that warning when your component does data fetching. Because data fetching is async, when you render the component inside act(), behing the scene all the data fetching and state update will be completed first and then act() will finish. So you will be rendering the component, with the latest state update
Easiest way to get rid of this warning in RTL, you should run async query functions findBy*
test("test", async () => {
render(
<MemoryRouter>
<TestApp />
</MemoryRouter>
);
await screen.findByRole("button");
});

Why does react hook throw the act error when used with fetch api?

I keep getting Warning: An update to App inside a test was not wrapped in act(...). in my test suite whenever I make an API request and update the state.
I'm making use of react-testing-library. I also tried using ReactDOM test utils, got the same result. One other thing I tried was wrapping the container in act, still got the same result.
Please note that: My App works and my test passes. I just need to know what I was doing wrong or if it's a bug in the react-dom package that's making that error show up. And it's bad to mock the console error and mute it.
global.fetch = require('jest-fetch-mock');
it('should clear select content item', async () => {
fetch.mockResponseOnce(JSON.stringify({ results: data }));
const { container } = render(<App />);
const content = container.querySelector('.content');
await wait();
expect(content.querySelectorAll('.content--item').length).toBe(2);
});
Here's the hook implementation:
const [data, setData] = useState([]);
const [error, setError] = useState('');
const fetchInitData = async () => {
try {
const res = await fetch(API_URL);
const data = await res.json();
if (data.fault) {
setError('Rate limit Exceeded');
} else {
setData(data.results);
}
} catch(e) {
setError(e.message);
}
};
useEffect(() => {
fetchInitData();
}, [isEqual(data)]);
It's a known problem, check this issue in Github https://github.com/kentcdodds/react-testing-library/issues/281
For anyone who stumbles upon this more than a year later as I did, the issue Giorgio mentions has since been resolved, and wait has since been replaced with waitFor, as documented here:
https://testing-library.com/docs/dom-testing-library/api-async/
That being the case, I believe the solution to the warning now should be something like this:
import { render, waitFor } from '#testing-library/react';
// ...
it('should clear select content item', async () => {
fetch.mockResponseOnce(JSON.stringify({ results: data }));
const { container } = render(<App />);
const content = container.querySelector('.content');
await waitFor(() =>
expect(content.querySelectorAll('.content--item').length).toBe(2);
);
});
In my case, I had an App component loading data asynchronously in a useEffect hook, and so I was getting this warning on every single test, using beforeEach to render App. This was the specific solution for my case:
beforeEach(async () => {
await waitFor(() => render(<App />));
});
To get rid of the act() warning you need to make sure your promises resolve synchronously. You can read here how to do this.
Summary:
The solution for this is a bit involved:
we polyfill Promise globally with an implementation that can resolve
promises 'immediately', such as promise
transpile your javascript with a custom babel setup like the one in this repo
use jest.runAllTimers(); this will also now flush the promise task queue
I had this problem and gave up using wait and async instead used jest faketimers and so on, so your code should be something like this.
global.fetch = require('jest-fetch-mock');
it('should clear select content item', /*async */ () => {
jest.useFakeTimers();
fetch.mockResponseOnce(JSON.stringify({ results: data }));
const { container } = render(<App />);
const content = container.querySelector('.content');
// await wait();
act(() => {
jest.runAllTimers();
});
expect(content.querySelectorAll('.content--item').length).toBe(2);
});

Resources