How to mock a third party React component using Jest? - reactjs

TLDR; what's the proper way to mock a React component imported from a third-party library?
I'm testing a component called <App/>. It consumes a 3rd part component called <Localize/> provided by a library called localize-toolkit.
I'm having some trouble mocking <Localize/> using Jest.
Here is how I've tried mocking it.
jest.mock('localize-toolkit', () => ({
// Normally you pass in a key that represents the translated caption.
// For the sake of testing, I just want to return the key.
Localize: () => (key:string) => (<span>{key}</span>)
}));
And I've written a unit test for <App/> that looks like this.
it('Test', () => {
const component = render(<App/>);
expect(component).toMatchSnapshot();
}
)
It will pass, however this is the warning message returned.
Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render.
And when I look at the snapshot, I get a series of periods "..." where the localized caption should appear.
Am I not mocking the Localize component properly?

Here's how I ended up doing it.
Note how the third-party component Localize needs to be returned as a function.
jest.mock('localize-toolkit', () => ({
Localize: ({t}) => (<>{t}</>)
}));
and in case there are multiple components, and you only want to mock one of them, you can do this:
jest.mock("localize-toolkit", () => {
const lib = jest.requireActual("localize-toolkit");
return {
...lib,
Localize: ({t}) => (<>{t}</>),
};
});

We can mock the 3rd party library for example in my case i need to mock react-lazyload
Component.tsx
import LazyLoad from 'react-lazyload';
render() {
<LazyLoad><img/></LazyLoad>
}
In jest.config.js
module.exports = {
moduleNameMapper: {
'react-lazyload': '/jest/__mocks__/react-lazyload.js',
}
}
In jest/mocks/react-lazyload.js
import * as React from 'react';
jest.genMockFromModule('react-lazyload');
const LazyLoad = ({children}) => <>{children}</>;
module.exports = { default: LazyLoad };

Related

Adding functions to lit web components in react with typescript

I have a web component i created in lit, which takes in a function as input prop. but the function is not being triggered from the react component.
import React, { FC } from 'react';
import '#webcomponents/widgets'
declare global {
namespace JSX {
interface IntrinsicElements {
'webcomponents-widgets': WidgetProps
}
}
}
interface WidgetProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
var1: string,
successCallback: Function,
}
const App = () =>{
const onSuccessCallback = () =>{
console.log("add some logic here");
}
return(<webcomponents-widgets var1="test" successCallBack={onSuccessCallback}></webcomponents-widgets>)
}
How can i trigger the function in react component? I have tried this is vue 3 and is working as expected.
Am i missing something?
As pointed out in this answer, React does not handle function props for web components properly at this time.
While it's possible to use a ref to add the function property imperatively, I would suggest the more idiomatic way of doing things in web components is to not take a function as a prop but rather have the web component dispatch an event on "success" and the consumer to write an event handler.
So the implementation of <webcomponents-widgets>, instead of calling
this.successCallBack();
would instead do
const event = new Event('success', {bubbles: true, composed: true});
this.dispatch(event);
Then, in your React component you can add the event listener.
const App = () => {
const widgetRef = useRef();
const onSuccessCallback = () => {
console.log("add some logic here");
}
useEffect(() => {
widgetRef.current?.addEventListener('success', onSuccessCallback);
return () => {
widgetRef.current?.removeEventListener('success', onSuccessCallback);
}
}, []);
return(<webcomponents-widgets var1="test" ref={widgetRef}></webcomponents-widgets>);
}
The #lit-labs/react package let's you wrap the web component, turning it into a React component so you can do this kind of event handling declaratively.
React does not handle Web Components as well as other frameworks (but it is planned to be improved in the future).
What is happening here is that your successCallBack parameter gets converted to a string. You need to setup a ref on your web component and set successCallBack from a useEffect:
const App = () => {
const widgetRef = useRef();
const onSuccessCallback = () =>{
console.log("add some logic here");
}
useEffect(() => {
if (widgetRef.current) {
widgetRef.current.successCallBack = onSuccessCallback;
}
}, []);
return(<webcomponents-widgets var1="test" ref={widgetRef}></webcomponents-widgets>)
}

Jest Test Failed - TypeError: window.matchMedia is not a function

I try to make a snapshot testing using react-test-renderer, I need to render a component which use the code below (in a separate file) to conditionally render different component.
let mql = window.matchMedia("(max-width: 600px)");
let mobileView = mql.matches;
export default mobileView;
This is the snapshot test
test("Your test case", () => {
const component = renderer.create(<CalendarTitle month={8} year={2021} />);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
I have tried using jest-matchmedia-mock package but it can't solve the problem, maybe it need to mock in a different file but i am not sure what to write in matchMedia.mock.ts file and what is the myMethod from "file-to-test" and how to write to complete test method.
import matchMedia from './matchMedia.mock.ts'; // Must be imported before the tested file
import { myMethod } from './file-to-test';
describe('myMethod()', () => {
// Test the method here...
});

Testing react-lazyload in React testing library

I am unit testing a component that makes use of react-lazyload to lazyload stuff. While rendering in my unit test, I noticed that the Lazyload is not rendering the content and its placeholder is being shown. I tried using waitFor with async/await to wait for lazily loaded content to render in next tick cycle but it still fails and screen.debug() still shows the placeholder in the dom.
Is there any way I can test such a component? Here is what I tried:
render(<WrapperComponent />)
await waitFor(() => {
expect(screen.getByText('Lazily loaded text!')).toBeInTheDocument()
});
// source code
import LazyLoad from 'react-lazyload';
const WrapperComponent = () => {
return (
<div>
<LazyLoad placeholder="Loading..." >
<div>Lazily loaded text!</div>
</LazyLoad>
</div>
);
}
Lazily loaded content has a div with the text being expected above.
We can mock the LazyLoad component from react-lazyload library at the top of our test file so that the LazyLoad component behave just like a wrapper around our actual component that we need to test for.
jest.mock(
'react-lazyload',
() =>
function LazyLoad({ children }) {
return <>{children}</>
}
)
test('component wrapped with Lazyload', () => {
const {screen} = render(< WrapperComponent />)
expect(screen.getByText('Lazily loaded text!')).toBeInTheDocument()
})
Test does not actually trigger real data fetch, you have to mock it.
Probably the function that requests for data should be the one you mock.
You can utilize forceVisible:
import { forceVisible } from 'react-lazyload';
it('Testing a component using react-lazyload', async () => {
const screen = render(<WrapperComponent />)
forceVisible();
await waitFor(() => {
expect(screen.getByText('Lazily loaded text!')).toBeInTheDocument()
});
});

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()
})

loading CKEditor5 components with React.lazy

In my project I want to use CKEditor5. Problem is, this version is not compatible with IE11 so my goal is to lazy load CKEditor5 components and when IE11 is detected, I dont want to simply load those components.
When component is imported like this import CKEditor from "#ckeditor/ckeditor5-react"; it just importing class, however with React.lazy import component is wrapped with React.LazyExoticComponent.
I need to create instance of GFMDataProcessor according to documentation https://ckeditor.com/docs/ckeditor5/latest/features/markdown.html
But with dynamic import I am not able to do that, since this line of code throws an error and I dont know what argument should I pass, since GFMDataProcessor is React.LazyExoticComponent and not GFMDataProcessor class.
//Expected 1 arguments, but got 0
const markdownPlugin = (editor) => { editor.data.processor = new GFMDataProcessor() }
Other problem is with my configuration for CKEditor, it has to be lazy loaded also and here is the same problem as above, ClassicEditor is again React.LazyExoticComponent instead of class and I have to pass to editor property imported component, not the wrapped one with React.LazyExoticComponent. Is there some way how I can extract dynamically imported component from wrapped one or any other way how can this be solved?
const ckeditorProps = {
data: data,
editor: ClassicEditor,
onChange: (event, editor) => {
const data2 = editor.getData();
if (data2 !== data) {
this.onChange(data2, this.state.selectedCultureCode, true);
}
},
config: editorConfiguration
}
Here are my dynamic imports
const CKEditor = React.lazy(() => import("#ckeditor/ckeditor5-react"));
const ClassicEditor = React.lazy(() => import("#ckeditor/ckeditor5-build-classic"));
const GFMDataProcessor = React.lazy(() => import("#ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor"));
Usage in DOM structure
<React.Suspense fallback={<div>Loading...</div>}>
<CKEditor {...ckeditorProps} />
</React.Suspense>
I don't know why you wrap anything you want to be fetched asynchronously into the React.lazy components. You should just fetch them normally when they're needed. Maybe something like the following will work for you:
let ClassicEditor, GFMDataProcessor;
const LazyCKEditor = React.lazy(() => {
return Promise.all([
import("#ckeditor/ckeditor5-build-classic"),
import("#ckeditor/ckeditor5-react"),
import("#ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor")
]).then(([ _ClassicEditor, _CKEditor, _GFMDataProcessor ]) => {
ClassicEditor = _ClassicEditor;
GFMDataProcessor = _GFMDataProcessor;
return _CKEditor;
});
});

Resources