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>;
Related
I'm developing react native app, in that i'm using two packages for exiting and restarting my app.
Whenever I tries to mock these functions in getting error. I'm not able to cover test cases for exit and restart methods
Could anybody know how do we mock these functions
import RNRestart from 'react-native-restart';
import RNExitApp from 'react-native-exit-app';
if (configNotThere) {
RNExitApp.exitApp();
}
if(configFound){
RNRestart.Restart();
}
Jest code
jest.mock('react-native-exit-app', () => ({
RNExitApp: {
exitApp: () => jest.fn(),
},
}));
expect(exitApp).toHaveBeenCalledTimes(1);
// expect(RNExitApp.exitApp()).toHaveBeenCalledTimes(1);
// beforeEach(() => {
// const exitAppMockValue = jest.fn();
// exSpy.mockReturnValue(exitAppMockValue);
// });
// const dummyExit = jest.fn();
// rnExitMock.mockReturnValue(dummyExit);
// RNExitApp.exitApp().mockResolvedValueOnce();
// const component = shallow(<KfcAppError />);
// component.find(TouchableOpacity).forEach((element) => {
// element.simulate('press');
// });
// RNExitApp.exitApp().mockResolvedValueOnce(true);
tried with all the way BUT no luck for always some of the getting errors
How to mock npm package methods in jest ?
Please anybody help me on this
react native exit app npm package
react native restart app npm package
I have come up with below answer for mocking npm module methods
In setup.js file we need to declare the methods like below then it will detect the methods in any of test.js files
jest.mock('react-native-exit-app', () => ({
exitApp: () => jest.fn(),
}));
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.
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()
})
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',
]
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);
});