React testing library, how to test history.push - reactjs

In a React App, I have a button that goes to a new url programatically:
<ComposeButton
onClick={() => {
history.push('some-link'))
}}
>
In my test, I render my component as mentioned in the react-testing-library documentation for React Router:
const renderComponent = (page: Pages) => {
const history = createMemoryHistory()
return renderWithState(
<MockedProvider
mocks={mocks}
addTypename={false}
defaultOptions={{
watchQuery: { fetchPolicy: 'no-cache' },
query: { fetchPolicy: 'no-cache' }
}}
>
<Router history={history}>
<ComponentToRender />
</Router>
</MockedProvider>
)
}
How can I wait for this change page in react-testing-library?
it('sends invitations', async () => {
const { getByTestId, queryByTestId, debug, getByText } = renderComponent(
Pages.contacts
)
await new Promise((resolve) => setTimeout(resolve))
const writeMessageBtn = getByTestId('write-message-btn')
waitFor(() => fireEvent.click(writeMessageBtn))
await new Promise((resolve) => setTimeout(resolve))
waitFor(() => debug()) // <- expect to see new page
getByText('EV_305_NEW_INVITATION_SEND') // <- this is in the second page, never get here
})
I can never see the content of the new page (after clicking the button) when using debug

I'm not sure this will make everything work, but there are a few issues with the code sample you shared I can highlight:
You are rendering a Router but no Routes. Use MemoryRouter instead and provide the same routes as your actual application - at least provide the route you are pushing into history
You are not using react-testing-library correctly. Instead of using getByTestId, use findByTestId. e.g. const writeMessageBtn = await findByTestId('write-message-btn'). The difference is that get* queries are synchronous while find* are async. You don't need to wait for arbitrary timeouts, the testing library will try to find the element for a few seconds.
The same applies to the other places where you use get*

Related

Unit testing useLoaderData() react-router v6 loader functions

A project uses react-router v6 and in some components I call useLoaderData(). For instance:
const routes = [
{ path: "widgets",
loader: () => fetchAndGetJSON("/api/widgets"),
element: <ListWidgets/> }
];
function ListWidgets() {
const widgets = useLoaderData();
return <>
<p>Here is a list of {widgets.length} widgets:
<...>
</>;
}
When testing I do not want to execute the fetch, I want to supply the list of widgets in the test.
How can I create a unit test for ListWidgets that uses data that I supply in the test?
I haven't used React Router's data APIs myself, but as I understand it, there are three main alternatives.
Use Jest's standard mocking functionality to mock fetch, mock fetchAndGetJSON, or similar.
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(FAKE_TEST_DATA),
})
);
beforeEach(() => fetch.mockClear());
Use createMemoryRouter with a testing version of your route that renders <ListWidgets/> with a custom loader that returns the test data that you want.
test(ListWidgets, async () => {
const routes = [{
path: "widgets",
element: <ListWidgets />,
loader: () => FAKE_TEST_DATA,
}];
const router = createMemoryRouter(routes, { initialEntries: ["widgets"] });
render(<RouterProvider router={router} />);
// ...testing...
}
Use Mock Service Worker (msw.js) to create a mock back-end. Functionally, this is pretty similar to mocking fetch yourself, but MSW is very good at streamlining and consolidating things.
const worker = setupWorker(
rest.get('/api/widgets', async (req, res, ctx) => {
const { username } = await req.json();
return res(ctx.json(FAKE_TEST_DATA))
}),
);
worker.start();
I'm a big fan of msw.js, but any of the options should work. (It's something of a trade-off: overriding loader results in more narrowly target unit tests, while msw.js lets you write less invasive tests closer to the integration testing end of the spectrum.)

How to make an automated test which checks that no components in a React component tree re-rendered?

I want to automate the following test scenario:
I render some arbitrary React component tree.
I make some action (scroll some container / click a button / ...)
I assert if any components have re-rendered since taking action 2.
What would be a good way to do this? We currently use Jest, Cypress and react-test-renderer in our project - it would be great to find a way to this using those. But this is not strictly necessary.
I need this to catch improperly memoized useSelector calls high up the component tree, which result in most of the app re-rendering - we keep running into this problem over and over.
As mentioned by #morganney, welldone-software/why-did-you-render has a bunch of Cypress test examples already in the project.
Here is one example:
Cypress.Commands.add('visitAndSpyConsole', (url, cb) => {
const context = {};
cy.visit(url, {
onBeforeLoad: win => {
cy.spy(win.console, 'log');
cy.spy(win.console, 'group');
},
onLoad: win => context.win = win,
});
cy.waitFor(context.win)
.then(() => cb(context.win.console));
});
it('Child of Pure Component', () => {
cy.visitAndSpyConsole('/#childOfPureComponent', console => {
cy.contains('button', 'clicks:').click();
cy.contains('button', 'clicks:').click();
cy.contains('button', 'clicks:').should('contain', '2');
expect(console.group).to.be.calledWithMatches([
{ match: 'PureFather', times: 2 },
{ match: /props.*children\W/, times: 2 },
]);
expect(console.log).to.be.calledWithMatches([
{ match: 'syntax always produces a *NEW* immutable React element', times: 2 },
]);
});
});
If you add more details to the question, I can give you a specific example.
Generally re-render checks'/optimisation's are done at development time using Reacts Profiler API.
https://reactjs.org/docs/profiler.html
this tells you why something re-rendered, times re-rendered.
EDIT#2
I was reading a blog and i found a better way to integrate it into a test.
In my last approach you would have to have a callback to log and a babel-plugin to wrap your desired component for test.
with approach 2 you can just use -
import TestRenderer from 'react-test-renderer';
const testRenderer = TestRenderer.create(
<Link page="https://www.facebook.com/">Facebook</Link>
);
console.log(testRenderer.toJSON());
// { type: 'a',
// props: { href: 'https://www.facebook.com/' },
// children: [ 'Facebook' ] }
it prints out host(in your case react) component tree in json, You can use Jest’s snapshot testing feature to automatically save a copy of the JSON tree and check if anything has changes and compute your rerenders.
I have 2 solutions for your problem.
The first one is implemented with Puppeteer only
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const navigationPromise = page.waitForNavigation();
await page.goto('http://localhost:3000/path-to-your-page')
await page.setViewport({ width: 1276, height: 689 });
await navigationPromise;
// Begin profiling...
await page.tracing.start({ path: 'profiling-results.json' });
// do action
await page.$eval('.class_name:last-child', e => {
e.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'end' });
});
// Stop profliling
await page.tracing.stop();
await browser.close();
})()
The second one is implemented with Puppeteer and the use of React Profiler API directly in your app:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const navigationPromise = page.waitForNavigation();
await page.goto('http://localhost:3000/path-to-your-page')
await page.setViewport({ width: 1276, height: 689 });
await navigationPromise;
page
.on('console', message =>
console.log(`${message.type().substr(0, 3).toUpperCase()} ${message.text()}`))
// do action
await page.$eval('.class_name:last-child or any other CSS selector/id/etc', e => {
e.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'end' });
});
await browser.close();
})()
And for the second solution to work add this code to your App.jsx:
<Profiler
id="profile-all"
onRender={
(id, phase, actualDuration) => {
console.log({id, phase, actualDuration})
}
}>
{/*all of the markdown should go inside*/}
</Profiler>

React-testing-library with connected react router together

I am trying to test a workflow with a React app. When all fields are filled withing a workflow step, user is able to click to the "next" button. This action registers a state in a reducer and changes the URL to go to the next workflow step.
According to the RTL documentation, I wrap my component under test in a store provider and a connected router using this function:
export const renderWithRedux = (ui: JSX.Element, initialState: any = {}, route: string = "/") => {
// #ts-ignore
const root = reducer({}, { type: "##INIT" })
const state = mergeDeepRight(root, initialState)
const store = createStoreWithMiddleWare(reducer, state)
const history = createMemoryHistory({ initialEntries: [route]})
const Wrapper = ({ children }: any) => (
<Provider store={store}>
<ConnectedRouter history={history}>{children}</ConnectedRouter>
</Provider>
)
return {
...render(ui, { wrapper: Wrapper }),
// adding `store` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
store
}
}
Unlike in the documentation, I amm using connected-react-router, not react-router-dom, but I've seen some people using connected-react-router with RTL on the web so I don't think the problem come from here.
The component under test is wrapped in a withRouter function, and I refresh the URL via the connected react router push function, dispatching via the redux connectfunction:
export default withRouter(
connect<any, any, any>(mapStateToProps, mapDispatchToProps, mergeProps)(View)
)
Everything work well in production, but the page doesn't refresh when I fire a click event on the "next" button. Here is the code of my test (to make it easier to read for you, I have filled all field and enable the "next" button):
const {
container,
queryByText,
getAllByPlaceholderText,
getByText,
debug,
getAllByText
} = renderWithRedux(<Wrapper />, getInitialState(), "/workflow/EXAC")
await waitForElement(
() => [getByText("supplierColumnHeader"), getByText("nextButton")],
{ container }
)
fireEvent.click(getByText("nextButton"))
await waitForElement(
() => [getByText("INTERNAL PARENT"), getByText("EXTERNAL PARENT")],
{ container }
)
Any clue of what is going wrong here?

Jest test case for UseEffect hooks in react JS

I am trying to write the Jest-enzyme test case for useEffect react hooks, and I am really lost, I want to write test case for 2 react hooks, one making the async call and another sorting the data and setting the data using usestate hooks, my file is here.
export const DatasetTable: React.FC<DatasetTableProps> = ({id, dataset, setDataset, datasetError, setDataSetError}) => {
const [sortedDataset, setSortedDataset] = useState<Dataset[]>();
useEffect(() => {
fetchRegistryFunction({
route:`/dataset/report?${constructQueryParams({id})}`,
setData: setDataset,
setError: setDataSetError
})();
}, [id, setDataset, setDataSetError]});
useEffect(() => {
if(dataset) {
const sortedDatasetVal = [...dataset];
sortedDatasetVal.sort(a, b) => {
const dateA: any = new Date(a.date);
const dateA: any = new Date(a.date);
return dataA - dateB;
}
setSortedDataset(sortedDatasetVal);
}
}, [dataset])
return (
<div>
<DatasetTable
origin="Datasets"
tableData={sortedDataset}
displayColumns={datasetColumns}
errorMessage={datasetError}
/>
</div>
);
}
Enzyme isn't the right library for this kind of testing.
https://react-hooks-testing-library.com/ is what you need.
In your case I would extract all the data fetching to a 'custom hook' and then test this independently from your UI presentation layer.
In doing so you have better separation of concerns and your custom hook can be used in other similar react components.
I managed to get enzyme to work with a data fetching useEffect hook. It does however require that you allow your dataFetching functions to be passed as props to the component.
Here's how I would go about testing your component, considering it now accepts fetchRegistryFunction as a prop:
const someDataSet = DataSet[] // mock your response object here.
describe('DatasetTable', () => {
let fetchRegistryFunction;
let wrapper;
beforeEach(async () => {
fetchRegistryFunction = jest.fn()
.mockImplementation(() => Promise.resolve(someDataSet));
await act(async () => {
wrapper = mount(
<DatasetTable
fetchRegistryFunction={fetchRegistryFunction}
// ... other props here
/>,
);
});
// The wrapper.update call changes everything,
// act seems to not automatically update the wrapper,
// which lets you validate your old rendered state
// before updating it.
wrapper.update();
});
afterEach(() => {
wrapper.unmount();
jest.restoreAllMocks();
});
it('should display fetched data', () => {
expect(wrapper.find(DatasetTable).props().tableData)
.toEqual(someDataSet);
});
});
Hope this helps!

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

Resources