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="/ponent"
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 ponents 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!
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="/ponent"
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 ponents 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 ponent
const Suspense = ({ children }) => {
return children;
};
const lazy = () => {
// `require` ponent directly as we want to see
// Why? Above reason
return require('./PageNotFound').default;
}
return {
...React,
lazy,
Suspense
};
});
lazy
functionI think I have a better idea to invoke the lazy
argument then return as a ponent 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 ponent updated which is returned in mock lazy
so we wait ponent 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={["/ponent"]}>
<App />
</MemoryRouter>
);
await waitForComponentToPaint(wrapper);
expect(wrapper.exists(Component)).toEqual(true);
});
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 ponent
const Suspense = ({
children,
}) => children;
const lazyImport = jest.fn().mockImplementation(() => {
class SpyComponent extends ReactActual.Component {
ponentDidMount() {}
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 ponent :
<CustomLazyComponent
path="./CustomLazyComponent"
/>