Test lazy loaded components in Enzyme - javascript

Given a simple App containing multiple lazy loaded routes,
import React, { lazy, Suspense } from "react";
import { Route } from "react-router-dom";
import "./styles.css";
const Component = lazy(() => import("./Component"));
const PageNotFound = lazy(() => import("./PageNotFound"));
export default function App() {
return (
<div className="App">
<Route
path="/component"
exact
render={() => (
<Suspense fallback={<div>Loading..</div>}>
<Component />
</Suspense>
)}
/>
<Route
path="*"
render={() => (
<Suspense fallback={<div>Loading..</div>}>
<PageNotFound />
</Suspense>
)}
/>
</div>
);
}
How can tests be made to check if those components are being rendered on that specific route?
Here's the App.test with what I tried:
import { configure, shallow, mount } from "enzyme";
import Adapter from "#wojtekmaj/enzyme-adapter-react-17";
import React from "react";
import { MemoryRouter } from "react-router-dom";
import App from "./App";
import Component from "./Component";
import PageNotFound from "./PageNotFound";
configure({ adapter: new Adapter() });
describe("App", () => {
it("renders without crashing", () => {
shallow(<App />);
});
it("renders lazy loaded PageNotFound route", () => {
// Act
const wrapper = mount(
<MemoryRouter initialEntries={["/random"]}>
<App />
</MemoryRouter>
);
// Assert
// expect(wrapper.containsMatchingElement(<PageNotFound />)).toEqual(true);
// expect(wrapper.find(PageNotFound)).toHaveLength(1);
expect(wrapper.exists(PageNotFound)).toEqual(true);
});
});
All 3 assertions don't seem to be working due to Suspense; A working snippet can be found at codesandbox here - Make sure to go on the 'tests' tab in order to see the failing tests.
Any suggestion is highly appreciated, thank you in advance!

This is an interesting question which is hard to have a best way to mock since the lazy(() => import('path/to/file')) takes a function as argument so we can't detect the value of anonymous function.
But I think I have a solution for you but it's not best to test all cases but a specific case it would work. You would mock as following:
jest.mock('react', () => {
const React = jest.requireActual('react');
// Always render children as our lazy mock component
const Suspense = ({ children }) => {
return children;
};
const lazy = () => {
// `require` component directly as we want to see
// Why? Above reason
return require('./PageNotFound').default;
}
return {
...React,
lazy,
Suspense
};
});
Update a new way to mock lazy function
I think I have a better idea to invoke the lazy argument then return as a component as following:
jest.mock('react', () => {
const React = jest.requireActual('react');
const Suspense = ({ children }) => {
return children;
};
const lazy = jest.fn().mockImplementation((fn) => {
const Component = (props) => {
const [C, setC] = React.useState();
React.useEffect(() => {
fn().then(v => {
setC(v)
});
}, []);
return C ? <C.default {...props} /> : null;
}
return Component;
})
return {
...React,
lazy,
Suspense
};
});
Then you have to wait the component updated which is returned in mock lazy so we wait component to re-paint as following:
// keep warning `act` removed
import { act } from 'react-dom/test-utils';
// A helper to update wrapper
const waitForComponentToPaint = async (wrapper) => {
await act(async () => {
await new Promise(resolve => setTimeout(resolve));
wrapper.update();
});
};
it("renders PageNotFound", async () => {
const wrapper = mount(
<MemoryRouter initialEntries={["/random"]}>
<App />
</MemoryRouter>
);
await waitForComponentToPaint(wrapper);
expect(wrapper.exists(PageNotFound)).toEqual(true);
});
it("renders Component", async () => {
const wrapper = mount(
<MemoryRouter initialEntries={["/component"]}>
<App />
</MemoryRouter>
);
await waitForComponentToPaint(wrapper);
expect(wrapper.exists(Component)).toEqual(true);
});
Another update for link
I've created a repl.it link for you to check how it works: https://repl.it/#tmhao2005/js-cra
You can run the test: yarn test -- lazy. And browse the code under src/Lazy.

Below is my working version :
import { act, } from 'react-dom/test-utils';
const waitForComponentToPaint = async (wrapper) => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve));
wrapper.update();
});
};
jest.mock('react', () => {
const ReactActual = jest.requireActual('react');
// Always render children as our lazy mock component
const Suspense = ({
children,
}) => children;
const lazyImport = jest.fn().mockImplementation(() => {
class SpyComponent extends ReactActual.Component {
componentDidMount() {}
render() {
const {
path,
} = this.props;
const LazyComponent = require(path).default;
return (
<>
{LazyComponent ? <LazyComponent {...this.props} /> : null}
</>
);
}
}
return SpyComponent;
});
return {
...ReactActual,
lazy: lazyImport,
Suspense,
};
});
describe('Render <Header />', () => {
it('should render a Header', async () => {
const wrapper = mount(
<Header />
);
await waitForComponentToPaint(wrapper);
expect(wrapper.find('XXXXXX')).to.have.length(1);
});
});
And I added a path props when calling the lazy component :
<CustomLazyComponent
path="./CustomLazyComponent"
/>

Related

Check whether React.Suspense is ready

I'm using the Intersection Observer API in React to add some animations. I am adding as Intersection Entries some elements.
The problem is that I have the app in multiple languages, and due to the implementation that the tool I am using to translate has, I need to wrap all my components into React.Suspense to wait for languages to load.
When useEffect queries for the elements, they aren't still in the DOM, and therefore they are not assigned as entries.
This is my custom hook:
hooks/useObserver.js
import { useState } from "react";
import { useEffect, useRef } from "react";
export function useObserver(config = {}) {
const [elements, setElements] = useState([]);
const [entries, setEntries] = useState([]);
const observer = useRef(
new IntersectionObserver(observedEntries => {
setEntries(observedEntries);
}, config)
);
useEffect(() => {
const { current: currentObserver } = observer;
currentObserver.disconnect();
if (elements.length > 0) {
elements.forEach(el => currentObserver.observe(el));
}
return () => {
if (currentObserver) {
currentObserver.disconnect();
}
};
}, [elements]);
return { observer: observer.current, setElements, entries };
}
and this is my main component:
App.jsx
import Header from "./components/Header";
import Hero from "./components/Hero";
import Footer from "./components/Footer";
import { Loader } from "./components/shared/Loader";
import { useObserver } from "./hooks/useObserver";
import { useEffect, Suspense } from "react";
function App() {
const { entries, setElements } = useObserver({});
useEffect(() => {
const sections = document.querySelectorAll("section.animated-section");
setElements(sections);
};
}, [setElements]);
useEffect(() => {
entries.forEach(entry => {
entry.target.classList.toggle("section-visible", entry.isIntersecting);
});
}, [entries]);
return (
<Suspense fallback={<Loader />}>
<Header />
<Hero />
<Footer />
</Suspense>
);
}
export default App;
I tried to set a timeout to wait some seconds and then add the elements as entries, and it works correctly:
useEffect(() => {
const observeElements = () => {
const sections = document.querySelectorAll("section.animated-section");
setElements(sections);
};
const observeElementsTimeout = setTimeout(observeElements, 3000);
return () => clearTimeout(observeElementsTimeout)
}, [setElements]);
I want to know if:
There is a way to know when React.Suspense is ready
There is a better approach to solve my problem
Thanks in advance!!

queryByTestId is null after waitFor in unit test

In my unit test, I want to click an element in my wrapper component that affects the child component. The queryByTestId works before the await waitFor call, but the 2nd queryByTestID returns "null". I'm trying to test what happens in the child component when the language changes.
In my test I have the following:
const { queryByTestId, container } = render(
<TestIntlWrapper>
<MyComponent />
</TestIntlWrapper>
);
expect(queryByTestId("test-intl-wrapper")).toBeInTheDocument;
await waitFor(() => expect(mockedAxios.get).toBeCalledTimes(expectedNumOfAPICalls));
expect(mockedAxios.get).toBeCalledWith(expectedURL1);
expect(mockedAxios.get.mock.calls[1][0]).toBe(expectedURL2);
expect(mockedAxios.get.mock.calls[thirdCall][0]).toBe(expectedURL3);
expect(queryByTestId("test-intl-wrapper")).toBeInTheDocument; //queryByTestId returns null here
TestIntlWrapper.tsx
import React, { useEffect, useState } from "react";
import { IntlProvider } from "react-intl";
interface TestIntlWrapperProps {
children: JSX.Element
}
export default function TestIntlWrapper({children}: TestIntlWrapperProps) {
const languages = ["en", "es", "fr"]
const [currentLanguage, setCurrentLanguage] = useState(languages[0]);
const [clickCount, setClickCount] = useState(0);
const setClick = () => {
setClickCount(clickCount + 1)
}
useEffect(() => {
setCurrentLanguage(languages[clickCount % languages.length]);
},[clickCount] )
return (
<div data-testid="test-intl-wrapper" onClick={setClick}>
<IntlProvider locale={currentLanguage}>
{children}
</IntlProvider>
</div>
)
}
Any help is appreciated
The issue was the application was throwing an uncaught error in the waitFor which is why it was running an empty div and the data-testid was disappearing.

Jest test fails on a react component which uses react-hooks-form with sharing refs

Here is the abstract version of my component
const Test = () => {
const { register } = useFormContext();
const mRef = useThirdpartyHook(); // Third party hook returns a ref
const { ref, ...rest } = register('test');
return (
<input
type="text"
name="test"
{...rest}
ref={(e) => {
ref(e);
mRef.current = e;
}}
/>
);
};
export default Test;
Test case
import React from 'react';
import { render } from '#testing-library/react';
import { FormProvider } from 'react-hook-form';
import Test from './Test';
describe('<Test> component', () => {
it('renders default correctly', () => {
const wrapper = render(
<FormProvider
{...{ register: () => jest.fn() }}>
<Test />
</FormProvider>
);
expect(wrapper.baseElement).toMatchSnapshot();
});
});
Executing this test throws an error as given below.
<Test> component › renders default correctly
TypeError: ref is not a function
ref={(e) => {
ref(e);
^
mRef.current = e;
}}
I tried to mock the ref as function but it didn't help. It would be great if anyone can throw some insights.
register is a function that returns props the input needs. You are mocking the register function, but it still needs to return everything the component is consuming or passing on. Specifically it seems you need to return a ref function for what is returned by the useFormContext hook.
Example:
describe('<Test> component', () => {
it('renders default correctly', () => {
const registerMock = () => ({
ref: jest.fn(),
.... mock other necessary bits here
});
const wrapper = render(
<FormProvider
{...{ register: registerMock }}
>
<Test />
</FormProvider>
);
expect(wrapper.baseElement).toMatchSnapshot();
});
});

Mock different results of an api called inside react component in jest

I have a react component that calls an API that returns two different results which the default value is
{init:false}
And based on users actions, it will be true:
{init:true}
Now I want to test these two states in my app.test.tsx, It will work when I skip one of them(each working fine without another one):
import { screen } from '#testing-library/react';
import { render } from 'src/mocks/renderViaAllProviders';
import App from './app';
import * as apis from 'src/api/consul';
import { mockedRawConsul } from 'src/mocks/db/consul';
test("Show init page when 'initialized:false' in consul.", async () => {
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
});
test("Show Login page when 'initialized:true' in consul", async () => {
const initializedConsul = {
...mockedRawConsul,
...{ configs: { initialized: true } }
};
/*eslint-disable */
//#ts-ignore
apis.getConsulPublicConfig = jest.fn(() =>
Promise.resolve(initializedConsul)
);
render(<App />);
const loginButton = await screen.findByRole('button', {
name: /regularLogin/i
});
expect(loginButton).toBeInTheDocument();
});
How can I fix this?
Update
Here is the reprex and the error :
● Show Login page when 'initialized:true' in consul
Unable to find role="textbox"
console.error
TypeError: Cannot read property 'status' of undefined
at onResponseRejected (\src\api\
service\interceptors.ts:18:23)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at getLicense (\src\api\license .
ts:10:20)
I have tried to simulate the example that you are trying, I am able to mock the API which returns different results and test for the same, but since we want different results when a Component is rendered the API will be called only once(assuming the API is called on mounting) and upon some user actions the Component will be mounted again that's why called render function again not sure whether it is a good practice or not
//App.js
export default function App() {
const [value, setValue] = useState('loading');
const [show, setShow] = useState({init: false})
useEffect(() => {
setTimeout(() => {
setValue('init page')
fetchData().then(data => {
setShow(data)
}).catch((error) => {
console.log(`ERROR`)
})
},0)
},[])
const { init = false} = show
return (
<>
<p>IT S APP</p>
<h1>Value is {value}</h1>
{ init ? <button>regular Login</button> : null}
</>
);
}
//api.js
function fetchData() {
return fetch("https://jsonplaceholder.typicode.com/posts").then((response) =>
Promise.resolve({init: true})
);
}
export { fetchData };
//App.test.js
import App from "./App";
import { fetchData }from './api';
jest.mock('./api')
describe("<App />", () => {
it("check if loading, login button is present",async () => {
fetchData.mockImplementationOnce(() => Promise.resolve({init: false}))
fetchData.mockImplementationOnce(() => Promise.resolve({init: true}))
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
render(<App />);
await waitFor(() => {
expect(screen.queryByRole('button', {
name: /regular Login/i
})).toBeInTheDocument();
})
});
});

How to mock useEffect async calls with react-native-testing-library?

I'm trying to test the screen that has useEffect hook with the following:
export const SomeScreen: React.FC<Props> = ({}) => {
const [isSensorAvailable, setIsSensorAvailable] = useState(false);
useEffect(() => {
const isSensorAvailableCheck = async () => {
try {
await FingerprintScanner.isSensorAvailable();
setIsSensorAvailable(true);
} catch (error) {
console.log(error);
}
};
isSensorAvailableCheck();
}, []);
...
Here goes code that renders View if the sensor is available
...
}
And I'm trying the following test:
import {default as FingerprintScannerMock} from 'react-native-fingerprint-scanner';
jest.mock('react-native-fingerprint-scanner');
(FingerprintScannerMock.isSensorAvailable as jest.Mock).mockResolvedValue(true);
const createTestProps = (props: Object) => ({
navigation: {
navigate: jest.fn(),
},
...props,
});
describe('Testing some screen', () => {
test('testing some functionalities', async () => {
let props: any;
props = createTestProps({});
const component = render(
<Provider store={store}>
<RegisterPasswordScreen {...props} />
</Provider>,
);
const {getByText, getByTestId, getAllByTestId} = component;
const container = getByTestId('containerId');
expect(container).toBeTruthy();
});
});
But this container is never found because setIsSensorIsAvailable never sets the value to true because of the following error:
An update to SomeScreen inside a test was not wrapped in act(...).
I tried everything even like this:
const component = await waitFor(() =>
render(<Provider store={store}>
<RegisterPasswordScreen {...props} />
</Provider>,
);
);
But when I run this test it never ends. Tried also to wrap it with act(...) but that does not work either, then the error is following:
Can't access .root on unmounted test renderer.
Any help appreciated, thanks!
Had a similar issue
Wrapping the component using act(https://reactjs.org/docs/testing-recipes.html#act) solved my issue
Using create to wrap your component for react native instead of using a typical render method wrapped in act
import { act, create } from 'react-test-renderer';
it('test', () => {
await act(async () => {
create(<ComponentWithAsyncUseEffect />);
});
})
I ended up using findByTestId rather than getByTestId and now it works well.

Categories