I have two components in my react app:
Component A
Performs a lazy fetch of users. This looks like:
const ComponentA = () => {
const [trigger, {data}] = useLazyLoadUsers({
fixedCacheKey: fixedLoadUsersKey,
});
useEffect(() => {
trigger();
}, []);
return <div>{data.map(user => user.id)}</div>
}
Component B
Wants to render a loading indicator while useLazyLoadUsers's isLoading property equals true. This component looks like this:
const ComponentB = () => {
const [, {isLoading}] = useLazyLoadUsers({
fixedCacheKey: fixedLoadUsersKey,
});
if (!isLoading) {
return <div>Users loaded</div>
}
return <div>Loading users</div>
}
The issue
While this works well (the states are in sync via the fixedLoadUsersKey), I'm struggling to find documentation or examples on how to test Component B.
Testing Component A is well documented here https://redux.js.org/usage/writing-tests#ui-and-network-testing-tools.
I already have an overwritten react testing library render method that provides a real store (which includes all my auto-generated queries).
What I would like to do is testing that Component B loading indicator renders - or not - based on a mocked isLoading value. I want to keep my current or similar implementation, not duplicating the isLoading state into another slice.
So far, I have tried mocking useLazyLoadUsers without success. I also tried dispatching an initiator before rendering the test, something like
it('should render the loading indicator', async () => {
const store = makeMockedStore();
store.dispatch(myApi.endpoints.loadUsers.initiate());
render(<ComponentB />, {store});
expect(await screen.findByText('Loading users')).toBeVisible();
})
This didn't work either.
Does someone have a hint on how to proceed here or suggestions on best practices?
Related
I have several components where I added a analyticsProvider to track usage. In each one of these, I'm using an useEffect hook and importing useAnalytics:
import { useAnalytics } from '#framework/component-analytics';
const { trackView } = useAnalytics();
useEffect(() => {
trackView(`/liveTraffic`, 'liveTrafficPage');
}, [trackView]);
I also have a AnalyticsProvider at the root of the application that passes all the necessary config to the child consumers (ie. useAnalytics).
I'm running into an issue when running tests: 'useAnalytics' must be called from inside an '<AnalyticsProvider>', or must be provided with an 'eventManagerConfiguration' argument.
What is the best way to pass the component needed and related configuration in my tests? I'm fairly new to React and struggling to figure out what to use. I've looked into wrapping the components but it's not working.
describe('has no liveTraffic list items', () => {
const component = shallow(analyticsWrapper(<LiveTrafficPage {...props} />));
it('should NOT render LiveTrafficListing', () => {
expect(component.update().find(LiveTrafficListing).length).toEqual(0);
});
});
I have a React component that doesn't render until fetchUser status is not loading. My test fails because the component is not rendered the first time and therefore can not find page loaded text. How can I mock my const [fetchStatusLoading, setFetchStatusLoading] = useState(false) on my test so that the page would get rendered instead of loading text?
const fetchUser = useSelector((state) => (state.fetchUser));
const [fetchStatusLoading, setFetchStatusLoading] = useState(true);
useEffect(() => {
setFetchStatusLoading(fetchUser .status === 'loading');
}, [fetchStatusLoading, fetch.status]);
useEffect(() => {
// this is a redux thunk dispatch that sets fetch.status to loading/succeeded
dispatch(fetchAPI({ username }));
}, []);
if(fetchStatusLoading) return 'loading..';
return (<>page loaded</>)
// test case fails
expect(getByText('page loaded')).toBeInTheDocument();
If you were able to mock the loading state you would only be testing the code on the "render" part and not all the component logic you are supposed to test, as this is the way Testing Library means to shine.
If this is just an asynchronous problem, you could do something like:
test('test page loaded', async () => {
render(<MyComponent />);
expect(await screen.findByText('page loaded')).toBeInTheDocument();
});
But it seems that your component contains an API request, so if the component accepts a function you could mock it and return the value, or you could mock fetch. Here are some reasons why you should not do that.
Using nock or mock-service-worker you can have a fake server that responds to your API requests and therefore run your component test without having to mock any internal state.
Testing Library just renders the component in a browser-like environment and does not provide an API to modify any of the props or the state. It was created with this purpose, as opposite of Enzyme, which provides an API to access the component props and state.
Check out this similar question: check state of a component using react testing library
I'm using a few third-party React hook libraries that aren't required for the initial render. E.g. react-use-gesture, react-spring, and react-hook-form. They all provide interactivity, which can wait until after the UI is rendered. I want to dynamically load these using Webpack's codesplitting (i.e. import()) after I render my component.
However, I can't stub out a React hook because it's essentially a conditional hook, which React doesn't support.
The 2 solutions that I can think of are:
Somehow extract the hook into a component and use composition
Force React to reconstruct a component after the hook loads
Both solutions seem hacky and it's likely that future engineers will mess it up. Are there better solutions for this?
As you say it, there are two ways to go about using lazy loaded hooks:
Load library in a Parent Component, conditionally render Component using library when available
Something along the lines of
let lib
const loadLib = () => {...}
const Component = () => {
const {...hooks} = lib
...
}
const Parent = () => {
const [loaded, setLoaded] = useState(false)
useEffect(() => loadComponent().then(() => setLoaded(true)), [])
return loaded && <Component/>
}
This method is indeed a little hacky and a lot of manual work for each library
Start loading a component using the hook, fail, reconstruct the component when the hook is loaded
This can be streamlined with the help of React.Suspense
<Suspense fallback={"Loading..."}>
<ComponentWithLazyHook/>
</Suspense>
Suspense works similar to Error Boundary like follows:
Component throws a Promise during rendering (via React.lazy or manually)
Suspense catches that Promise and renders Fallback
Promise resolves
Suspense re-renders the component
This way is likely to get more popular when Suspense for Data Fetching matures from experimental phase.
But for our purposes of loading a library once, and likely caching the result, a simple implementation of data fetching can do the trick
const cache = {}
const errorsCache = {}
// <Suspense> catches the thrown promise
// and rerenders children when promise resolves
export const useSuspense = (importPromise, cacheKey) => {
const cachedModule = cache[cacheKey]
// already loaded previously
if (cachedModule) return cachedModule
//prevents import() loop on failed imports
if (errorsCache[cacheKey]) throw errorsCache[cacheKey]
// gets caught by Suspense
throw importPromise
.then((mod) => (cache[cacheKey] = mod))
.catch((err) => {
errorsCache[cacheKey] = err
})
};
const SuspendedComp = () => {
const { useForm } = useSuspense(import("react-hook-form"), "react-hook-form")
const { register, handleSubmit, watch, errors } = useForm()
...
}
...
<Suspense fallback={null}>
<SuspendedComp/>
</Suspense>
You can see a sample implementation here.
Edit:
As I was writing the example in codesandbox, it completely escaped me that dependency resolution will behave differently than locally in webpack.
Webpack import() can't handle completely dynamic paths like import(importPath). It must have import('react-hook-form') somewhere statically, to create a chunk at build time.
So we must write import('react-hook-form') ourselves and also provide the importPath = 'react-hook-form' to use as a cache key.
I updated the codesanbox example to one that works with webpack, the old example, which won't work locally, can be found here
Have you considered stubbing the hooks? We used something similar to async load a large lib, but it was not a hook, so YMMV.
// init with stub
let _useDrag = () => undefined;
// load the actual implementation asynchronously
import('react-use-gesture').then(({useDrag}) => _useDrag = useDrag);
export asyncUseDrag = (cb) => _useDrag(cb)
I'm doing some testing with jest and react-testing-library
I've boiled this down so it's obviously not my full test.
Why does this code:
test('Share renders', () => {
const { getByText } = render(<Share />)
})
Give me this console error:
When testing, code that causes React state updates should be wrapped into act
But this code:
test('Share renders', async () => {
const { getByText } = render(<Share />)
const share = await waitForElement(() => getByText(/Share/i))
expect(share).toBeVisible()
})
Does not?
My Share component makes a call to get some user info inside a useEffect hook and set it to state. This causes a rerender the first time the component is mounted. The first example then complains at me due to the state update. That makes sense. My question is, what is waitForElement doing that suddenly makes it NOT give me the error with the second example? Nothing has changed with the component being rendered and then immediately updating...but now the error goes away???
I have a container component where I'm fetching the data via ajax operator from rxjs
const data = ajax(someUrl).pipe(map(r => r.response));
And in my componentDidMount I have
data.subscribe((data) => {
this.setState({ data });
});
// test.js
import React from 'react';
import { mount } from 'enzyme';
import { ajax } from 'rxjs/ajax'
import App from '../src/App';
describe('<App />', () => {
const wrap = mount(<App />);
const data = [{ 1: 'a' }];
const mock = ajax('http://url.com').pipe(map(() => data));
it('renders', () => {
console.log(mock.subscribe(x => x));
expect(wrap.find(App).exists()).toBe(true);
});
});
How do I go about mocking the response so that when I run the test it I can pass that data on to other components and check if they render?
All the testing examples I've found have been redux-Observable ones which I'm not using.
Thanks a lot!
First you need to understand that you should be testing one thing at a time.
Meaning that testing your async method execution should be separated from testing your components rendering proper content.
To test async methods you can mock your data and than mock timers in Jest.
https://jestjs.io/docs/en/tutorial-async
https://jestjs.io/docs/en/asynchronous
https://jestjs.io/docs/en/timer-mocks.html
with jest.useFakeTimers() and techniques mentioned above.
For testing component proper rendering use jest snapshots and e2e testing (can be done with ex. TestCafe)
To connect those approaches you need to design you app in a way that will allow you to:
The API you call in your component, should be external to component and be called from that external source (different file, different class, however you design it), so you can replace it in test.
Whole API should be modular, so you can take one module and test it without initializing whole API just for this case.
If you design your app in such manner, you can initialize part of the API with mock data, than render your component in test and as it will call mocked API, you can check if it renders what you expect it to.