Jest + React Loadable error Not supported - reactjs

I'm trying to test a component that renders a couple of asynchronously imported children with React Loadable like a Modal for example. My test looks like this
// Using React Testing Library
import { fireEvent, getByTestId, wait } from 'react-testing-library';
test('with RTL', async () => {
// There is a portal. I leave it in the code sample in case it gives any hints
const portalNode = document.getElementById('overlay');
const { container, getByLabelText } = render(<SearchFormComposed {...props} />);
expect(portalNode.children.length).toBe(0);
fireEvent.click(getByLabelText('MyButton'));
const list = await wait(() => getByTestId(portalNode, 'myList'));
console.log(list);
expect(portalNode.children.length).toBe(1);
});
The test gives a not very helpful error shown bellow
I can find no information about this error at all.
Anyone could shed some light here please?
Thanks in advance for your time!

I had the same issue when i used 'plugin-syntax-dynamic-import' for dynamic import. switch it to 'babel-plugin-dynamic-import-node' helped me to resolve that.
bablerc.js
plugins: [
// 'syntax-dynamic-import',
'dynamic-import-node',
]

Related

React Jest & React Testing Library, Writing test for Video player (play and pause)

I am using <ReactPlayer/> as part of my project, it gives you <video /> in HTML
I want to write the test cases for this.
To write test case, without using autoPlay I play the video manually so I have to write a test case for play and pause event of <ReactPlayer/> or <video />
Is there any way to write test case for event of "Play"
Thanks in advance
I coincidentally was working through the a similar challenge with this library. For me, this kind of ended up being a pain in the ass to work out because I was trying to test custom code using their on events (i.e. onPlay or onStart).
Given the information you provided, you shouldn't need to test the player itself. It's a third party library and they have their own test coverage that handles the functionality of the player.
As for me, trying to navigate how it all worked from an implementation standpoint seemed like a bad idea, so I ended up mocking the whole player out. I was trying to test that my code was being called on an onStart event, so I made sure my mock would handle that from the props:
// mock.jsx
jest.mock('react-player/file', () => (p) => {
const props = { ...p }
delete props.onStart
return (<div {...props} playing={p.playing.toString()} onClick={p.onStart} />)
});
// component.jsx
import React from 'react';
import tracking from 'tracking';
import ReactPlayer from 'react-player/file';
const TheVideoPlayer = () => {
const trackTheVideoPlayer = () => {
tracking.trackEvent('foo', { bar: 'baz' })
}
return (
<ReactPlayer
className={styles.theVideoPlayer}
data-testid="the-video-player"
light={backgroundSrc}
url={theVideoUrl()}
onStart={trackTheVideoPlayer}
playing
/>
)
}
// test.jsx
import { fireEvent, render, screen } from 'app/utils/test-utils';
import React from 'react';
import TheVideoPlayer from './TheVideoPlayer';
import tracking from 'tracking';
const mockTracking = { trackEvent: jest.fn() };
jest.spyOn(tracking, 'track').mockReturnValue(mockTracking);
describe('the video player', () => {
const createComponent = () => render(<TheVideoPlayer />);
describe('on start', () => {
it('Fires tracking Event', async () => {
createComponent();
const mockTrackProperties = { bar: 'baz' }
fireEvent.click(screen.getByTestId('video-player'))
expect(trackEvent).toBeCalledWith('foo', mockTrackProperties);
});
});
Since js-dom doesn't have a onStart event handler, I ended up using onClick and simulating a click. I also had to type cast the "playing" boolean to a string because jsDOM was giving a console error (but tests were passing). This doesn't feel particularly good or clean, but it does ensure that my code is being tested in an "onStart" event, however that is implemented in the library.

Using addDecorator in Storybook's preview.ts throws Rendered more hooks than during the previous render

Reading through the resource loading documentation from Chromatic, the Solution B: Check fonts have loaded in a decorator section.
Mainly would like to load our fonts before rendering the stories. The solution suggest to use addDecorator where with a simple FC we can preload the fonts and once they are loaded it can render the stories with story().
See the suggested decorator for preview.ts:
import isChromatic from "chromatic/isChromatic";
if (isChromatic() && document.fonts) {
addDecorator((story) => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true);
useEffect(() => {
Promise.all([document.fonts.load("1em Roboto")]).then(() =>
setIsLoadingFonts(false)
);
}, []);
return isLoadingFonts ? null : story();
});
}
For some reason this throws the usual error when violating the Rules of Hooks:
Rendered more hooks than during the previous render
What I've tried so far:
Mainly I tried to remove the useEffect which renders the stories:
if (isChromatic() && document.fonts) {
addDecorator((story) => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true);
return story();
});
}
Also the error disappeared but the fonts are causing inconsistent changes our screenshot tests as before.
Question:
I don't really see any issues which would violate the Rules of Hooks in the added FC for the addDecorator.
Is there anything what can make this error disappear? I'm open to any suggestions. Maybe I missed something here, thank you!
Mainly what solved the issue on our end is removing from main.ts one of the addons called #storybook/addon-knobs.
Also renamed from preview.ts to preview.tsx and used the decorator a bit differently as the following:
export const decorators = [
Story => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true)
useEffect(() => {
const call = async () => {
await document.fonts.load('1em Roboto')
setIsLoadingFonts(false)
}
call()
}, [])
return isLoadingFonts ? <>Fonts are loading...</> : <Story />
},
]
We dropped using the addDecorator and used as the exported const decorators as above.

React Testing Library's waitFor not working

I am using React Testing Library to unit test my ReactJS code. There are several async events in the UI, like fetching data and displaying a new page on click of button. The React code is somewhat like this:
// Inside ParentComponent.tsx
const [isChildVisible, setChildVisibility] = useState(false);
const showChild = () => setChildVisibility(true);
return(
<>
<button data-testid="show-child" onClick={showChild}>Show Child</button>
{isChildVisible && <ChildComponent {..childProps}/>}
</>
)
Where ChildComponent mounts, it fetches some data and then re-renders itself with the hydrated data. My unit test looks like:
jest.mock('../../../src/service'); // mock the fetch functions used by ChildComponent to fetch its data
describe('ParentComponent', () => {
test('renders ChildComponent on button click', async () => {
const screen = render(<ParentComponent />);
userEvent.click(screen.getByTestId('show-child'));
await (waitFor(() => screen.getByText('text rendered by child')));
});
});
When I run this test, I get the error "TestingLibraryElementError: Unable to find an element with the text: text rendered by child. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.".
I am not sure why it's happening, but one of the reason maybe that it's taking more than one second to hydrate and render the child component. Thus I want to change the default wait time for waitFor, but I can't find a way to do it from the docs (the default wait time is one second). So is it possible to change the default wait time?
EDIT: Increasing the wait time is still causing the same error. So the issue is something else.
I found the answer here: React Testing Library - using 'await wait()' after fireEvent
TLDR: "You can not use wait with getBy*. getBy is not async and will not wait." Better is to use findBy*. This is the async version of getBy.
It's specified within the documentation. waitFor Documentation
function waitFor<T>(
callback: () => T | Promise<T>,
options?: {
container?: HTMLElement
timeout?: number //This is 1000ms. Change timeout here.
interval?: number
onTimeout?: (error: Error) => Error
mutationObserverOptions?: MutationObserverInit
}
): Promise<T>
//For 3 seconds.
await (waitFor(() => screen.getByText('text rendered by child'),{timeout:3000}));
The default timeout is 1000ms which will keep you under Jest's default timeout of 5000ms.
I had an issue similar to this when I was setting up testing for a test application. The way I fixed this issue was to force re-render the component.
In this case your code would look something like:
import {render, screen} from "#testing-library/react";
describe('ParentComponent', () => {
test('renders ChildComponent on button click', async () => {
const {rerender} = render(<ParentComponent />);
userEvent.click(screen.getByTestId('show-child'));
rerender(<ParentComponent />)
await (waitFor(() => screen.getByText('text rendered by child')));
});
});
I hope this works for you. Also to be noted that you can use the screen export from the react testing library. It seems like there should be a way to do this automatically, but I haven't been able to find it.
Adding link to the rerender docs: https://testing-library.com/docs/react-testing-library/api/#rerender
For those who are using jest-expo preset which breaks this functionality you need to modify the jest-expo preset to include the code from testing-library/react-native
/* eslint-disable #typescript-eslint/no-var-requires */
const { mergeDeepRight } = require("ramda");
const jestExpoPreset = require("jest-expo/jest-preset");
const testingLibraryPreset = require("#testing-library/react-native/jest-preset");
/*
* Modify the existing jest preset to implement the fix of #testing-library/react-native to get the
* async waitFor working with modern timers.
*/
jestExpoPreset.setupFiles = [
testingLibraryPreset.setupFiles[0],
...jestExpoPreset.setupFiles,
testingLibraryPreset.setupFiles[testingLibraryPreset.setupFiles.length - 1],
];
module.exports = mergeDeepRight(jestExpoPreset, {
testResultsProcessor: "jest-sonar-reporter",
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "yml"],
modulePathIgnorePatterns: ["<rootDir>/lib/"],
globals: {
"ts-jest": {
babelConfig: "./babel.config.js",
},
},
});

How to test react-toastify with jest and react-testing-library

I have a screen with some form, and on submission, I send the request to back-end with axios. After successfully receiving the response, I show a toast with react-toastify. Pretty straight forward screen. However, when I try to test this behavior with an integration test using jest and react testing library, I can't seem to make the toast appear on DOM.
I have a utility renderer like that to render the component that I'm testing with toast container:
import {render} from "#testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";
export const renderWithToastify = (component) => (
render(
<div>
{component}
<ToastContainer/>
</div>
)
);
In the test itself, I fill the form with react-testing-library, pressing the submit button, and waiting for the toast to show up. I'm using mock service worker to mock the response. I confirmed that the response is returned OK, but for some reason, the toast refuses to show up. My current test is as follows:
expect(await screen.findByRole("alert")).toBeInTheDocument();
I'm looking for an element with role alert. But this seems to be not working.
Also, I tried doing something like this:
...
beforeAll(() => {
jest.useFakeTimers();
}
...
it("test", () => {
...
act(() =>
jest.runAllTimers();
)
expect(await screen.findByRole("alert")).toBeInTheDocument();
}
I'm kind of new to JS, and the problem is probably due to asynch nature of both axios and react-toastify, but I don't know how to test this behavior. I tried a lot of things, including mocking timers and running them, mocking timers and advancing them, not mocking them and waiting etc. I even tried to mock the call to toast, but I couldn't get it working properly. Plus this seems like an implementation detail, so I don't think I should be mocking that.
I think the problem is I show the toast after the axios promise is resolved, so timers gets confused somehow.
I tried to search many places, but failed to find an answer.
Thanks in advance.
Thank you #Estus Flask, but the problem was much much more stupid :) I had to render ToastContainer before my component, like this:
import {render} from "#testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";
export const renderWithToastify = (component) => {
return (
render(
<div>
<ToastContainer/>
{component}
</div>
)
);
};
Then, the test was very simple, I just had to await on the title of the toast:
expect(await screen.findByText("alert text")).toBeInTheDocument();
The findByRole doesn't seem to work for some reason, but I'm too tired to dig deeper :)
I didn't have to use any fake timers or flush the promises. Apperently, RTL already does those when you use await and finBy* queries, only the order of rendering was wrong.
In order to use a mock when you don't have access to the DOM (like a Redux side effect) you can do:
import { toast } from 'react-toastify'
jest.mock('react-toastify', () => ({
toast: {
success: jest.fn(),
},
}))
expect(toast.success).toHaveBeenCalled()
What I would do is mock the method from react-toastify to spy on that method to see what is gets called it, but not the actual component appearing on screen:
// setupTests.js
jest.mock('react-toastify', () => {
const actual = jest.requireActual('react-toastify');
Object.assign(actual, {toast: jest.fn()});
return actual;
});
and then in the actual test:
// test.spec.js
import {toast} from 'react-toastify';
const toastCalls = []
const spy = toast.mockImplementation((...args) => {
toastCalls.push(args)
}
)
describe('...', () => {
it('should ...', () => {
// do something that calls the toast
...
// then
expect(toastCalls).toEqual(...)
}
}
)
Another recommendation would be to put this mockImplementation into a separate helper function which you can easily call for the tests you need it for. This is a bear bones approach:
function startMonitoring() {
const monitor = {toast: [], log: [], api: [], navigation: []};
toast.mockImplementation((...args) => {
monitor.toast.push(args);
});
log.mockImplementation((...args) => {
monitor.log.push(args);
});
api.mockImplementation((...args) => {
monitor.api.push(args);
});
navigation.mockImplementation((...args) => {
monitor.navigation.push(args);
});
return () => monitor;
}
it('should...', () => {
const getSpyCalls = startMonitoring();
// do something
expect(getSpyCalls()).toEqual({
toast: [...],
log: [...],
api: [...],
navigation: [...]
});
});
Here, the solution was use getByText:
await waitFor(() => {
expect(screen.getByText(/Logged!/i)).toBeTruthy()
})

Not Supported Error when testing suspense

I'm getting a strange error when trying to use react-testing-library to test React.Suspense. The error just says "Not Supported" but doesn't give any real insight into the problem. I followed the example that Kent Dodds did on youtube.
I posted the full code of my problem on github here, but here's a snapshot of the test code:
import React from "react";
import { render, waitForElement, cleanup } from "react-testing-library";
import MyOtherPackageThing from "my-package/lib/my-thing";
import LazyThing from "../src/index";
afterEach(cleanup);
test("it works", async () => {
const { getByText, debug } = render(<MyOtherPackageThing />);
await waitForElement(() => getByText("my thing"));
expect(getByText("my thing"));
});
describe("these fail with 'Not Supported'", () => {
test("it lazy loads a local component", async () => {
const LazyLocalThing = React.lazy(() => import("../src/LocalThing"));
const { getByText, debug } = render(
<React.Suspense fallback="Loading...">
<LazyLocalThing />
</React.Suspense>
);
debug();
await waitForElement(() => getByText("my local thing"));
debug();
expect(getByText("my local thing"));
});
test("it says not supported, like wtf", async () => {
const { getByText, debug } = render(<LazyThing />);
debug();
await waitForElement(() => getByText("my thing"));
debug();
expect(getByText("my thing"));
});
});
I encountered the same error recently. I noticed that Kent's working sample was using create-react-app and wondered if it was Babeling something special for Node/Jest. As a result of using CRA, his package.json uses the babel preset react-app.
Try installing and configuring the plugin babel-plugin-dynamic-import-node (which is the specific part of the react-app preset I think we need). I believe this plugin transforms dynamic imports into require statements for Node. More info: https://www.npmjs.com/package/babel-plugin-dynamic-import-node
install the plugin:
npm i --save-dev babel-plugin-dynamic-import-node
in my-consumer-pkg/babel.config.js, add the plugin:
plugins: [
...
"babel-plugin-dynamic-import-node"
]
...this should be enough to get past the Not Supported error.
As an aside, I noticed that one of your tests named "it lazy loads a local component" was subsequently failing with this error:
Element type is invalid. Received a promise that resolves to: [object Object]. Lazy element type must resolve to a class or function.
...so I made a minor change so that the LocalThing was a function
const LocalThing = () => <div>my local thing</div>;

Resources