ReactJS Testing - Check if component did render after async call in componentDidMount - reactjs

Sometimes my react application crashes on some pages, after I changed the API. Now I want to add tests to the application that check, if the components (re)render without crashing after an API call has been made (all calls are made inside the componentDidMount() function).
Currently my test is really simple and looks like this:
it(route.path + ' renders without crashing', async () => {
const div = document.createElement('div');
const Component = route.component;
ReactDOM.render(
<Provider store={store}>
<Component />
</Provider>
, div);
});
But this only checks, if the application renders, before the API call returned something.
How can I add something like an await, to check if the data from componentDidMountloaded?
Thanks for your help.
EDIT
I checked out RTL - but I'm still having trouble. It always says that it can't find the element. Here is my code:
import { render, waitFor } from '#testing-library/react'
import '#testing-library/jest-dom/extend-expect'
test('examples of some things', async () => {
const { getByTestId, findByTestId } = render(
<Provider store={store}>
<Page />
</Provider>
);
await waitFor(() =>
expect(getByTestId("table")).toBeInTheDocument()
);
})
EDIT 2
The <Page /> component renders a <AnotherComponent />, Inside this component, an Ajax call is made to load some data, which then should be rendered:
export default class Page {
render(
...
...
<AnotherComponent />
);
}
export default class AnotherComponent {
componentDidMount() {
  someApiCall().then(set State...);
}
render(
....
...
{
this.state.someAPIdata &&
<div data-testid="table">
...
...
</div>
}
);
}
When I run the test, the tests already returns an error, even before the API call finished

Related

How to test for a global Toast/Snackbar with React Testing Library

I have a material-ui Snackbar component in my root pages/_app file in my project. If a suitable event happens on any component in my project, it dispatches an action to redux to update the "alert" state of my store, which triggers the Snackbar to appear.
What source of approach can I use to run tests on a component level to track that relevant (and important) toast/snackbar messages appear due to events? Is this possible within the confines of react-testing-library?
tests for a given component interacting with snackbar
import { render } from "#testing-library/react";
import user from "#testing-library/user-event";
describe("if the url is invalid", () => {
beforeEach(() => {
render(<SelectMedia />);
user.click(screen.getByRole("button", { name: /search/i }));
});
test("the VideoPlayer does not activate", () => {
expect(screen.getByTestId())
});
test.todo("a warning toast message appears");
// ^ what can I do to make tests like these work?
});
root app
const myApp = () => {
...
return (
<Provider store={store}>
<StyledSnackBar />
<Component {...pageProps} />
// ^ all components (like SelectMedia) exist within <Component ... />
</Provider>
)
}
toast/snackbar does not exist within the <SelectMedia /> component
toast/snackbar exists on a root page as a sibling and uses redux to trigger behavior

Unable to find functions or node when testing a nested component in Jest/Enzyme

I am trying to test a specific component that is nested within two other components:
<Router>
<Provider store={store}>
<Howitworks />
</Provider>
</Router>
However when I try to run my test:
test("should call onTaskerClick", () => {
const spy = jest.spyOn(Howitworks.prototype, "onTaskerClick");
const wrapper = mount(
<Router>
<Provider store={store}>
<Howitworks />
</Provider>
</Router>
);
wrapper.find("#pills-tasker-tab.nav-link.tasklink").at(0).simulate("click");
expect(spy).toHaveBeenCalled();
});
I get a "spyOn on a primitive value; undefined given" error. I have tried different variations of mocking this "onTaskerClick" function when simulating a click on the link that invokes it but always get a variation of the error that the function is undefined or function was not called.
The link that invokes onTaskerClick:
<a className='nav-link tasklink' id='pills-tasker-tab' data-toggle='pill' onClick={this.onTaskerClick} role='tab' aria-controls='pills-tasker' aria-selected='false'>LINK<span></span></a>
Here is how the Howitworks component is currently being exported:
export default connect(mapStateToProps, { onNotifyClick })(Howitworks);
Unfortunately there's very limited documentation on accessing functions within a nested component in a test environment so any help would be great.
EDIT:
Updated test suite:
test("should call onTaskerClick", () => {
const wrapper = mount(
<Router>
<Provider store={store}>
<Howitworks />
</Provider>
</Router>
);
const spy =
jest.spyOn(wrapper.find(Howitworks.WrappedComponent).instance(), "onTaskerClick");
wrapper.find("#pills-tasker-tab.nav-link.tasklink").at(0).simulate("click");
expect(spy).toHaveBeenCalled();
});
mapStateToProps function and export:
let mapStateToProps = (state) => ({});
export default connect(mapStateToProps, { onNotifyClick })(Howitworks);
connect returns a higher-order component that doesn't inherit from original component, which is common in React.
In order for onTaskerClick to be spyable on a prototype, it should be prototype and not instance (arrow) method. Original component class can be either separate named export:
export class Howitworks {
onTaskerClick() {...}
...
}
export default connect(...)(Howitworks);
Or it can be accessed on connected component as WrappedComponent property.
Since onTaskerClick isn't called on component instantiation, there's no need to spy on a prototype, it's easier to do this on an instance, this doesn't limit a spy to prototype methods:
const wrapper = mount(...);
const origComp = wrapper.find(Howitworks.WrappedComponent).instance();
const spy = jest.spyOn(origComp, "onTaskerClick");
origComp.forceUpdate();
// trigger click

unit test not using url path with parameter when using reach router with react-testing-library

I am trying to create a unit test for a react component where the props to the component checks part of the url path to decide a course of action and at the end of the path takes in a parameter or value.
Using the example given in https://testing-library.com/docs/example-reach-router I created my own unit test but when I run the test my component complain that the uri value is not there. In addition, when I add a console.log(props) to my component and run the test again, props is undefined.
I have tried a variation of wrapping my component with LocationProvider adding in history as shown in https://reach.tech/router/api/LocationProvider
The relevant code snippet is -
function renderWithRouter(
ui,
{ route = '/', history = createHistory(createMemorySource(route)) } = {}
) {
return {
...render(
<LocationProvider history={history}>{ui}</LocationProvider>
)
,
history,
}
}
describe('View Order Test', () => {
describe('Order', () => {
it('Renders the Order View for a specific Order', async () => {
const { queryByText } =
renderWithRouter(
<State initialState={initialState} reducer={reducer}>
<ViewOrder />
</State>
, { route: '/order/orderView/1234', }
)
expect(queryByText('OrderID: 1234'))
}
)
})
Nothing seems to work. Is there some way of passing props to a component which are uri (#reach/router) in a unit? As I said my unit is a carbon copy of the those given above
Solved by adding Reach Router and wrapping it around the component.
I had a similar problem when testing a component using url parameters with reach-router.
Route file example:
import { Router } from "#reach/router";
import Item from "pages/Item";
const Routes = () => (
<Router>
<Item path="/item/:id" />
</Router>
);
...
In my testing code, I did this in render function:
import {
Router,
createHistory,
createMemorySource,
LocationProvider,
} from "#reach/router";
import { render } from "#testing-library/react";
export function renderWithRouter(
ui,
{ route = "/", history = createHistory(createMemorySource(route)) } = {}
) {
return {
...render(
<LocationProvider history={history}>
<Router>{ui}</Router>
</LocationProvider>
),
history,
};
}
and the test case:
test("renders the component", async () => {
renderWithRouter(<Item path="/item/:id" />, { route: "/item/1" });
...
});
In this way, I was able to get the url parameters and all tests worked well.

React mapDispatchToProps Object Action Doesn't Work

I have this Action
export const UserIsLoggedIn = isLoggedIn => (
{ type: types.USER_IS_LOGGED_IN, isLoggedIn });
this actionConstant
export const USER_IS_LOGGED_IN = "USER_IS_LOGGED_IN";
index.js like this
import { UserIsLoggedIn } from "./redux/actions";
getUser = () => {
this.authService.getUser().then(user => {
if (user) {
this.props.UserIsLoggedIn(true);
}
}
}
const mapDispatchToProps = {
UserIsLoggedIn
}
export default connect(mapStateToProps, mapDispatchToProps) (Index);
so eventually with above code I get this.props.UserIsLoggedIn is not a function error, if I do UserIsLoggedIn(true); nothing happens... I don't quite understand where the problem is..
within the redux chrome extension I can dispatch with below without an error:
{
type: "USER_IS_LOGGED_IN", isLoggedIn : true
}
below is generally how index looks like
const store = configureStore();
class Index extends Component {
componentWillMount() {
this.getUser();
}
getUser = () => {
this.authService.getUser().then(user => {
if (user) {
console.log(this.props.isUserLoggedIn);
toastr.success("Welcome " + user.profile.given_name);
} else {
this.authService.login();
}
});
};
render() {
return (
<Provider store={store}>
<BrowserRouter>
<Route path="/" exact component={App} />
<Route path="/:type" component={App} />
</BrowserRouter>
</Provider>
);
}
}
function mapStateToProps(state) {
return {
isUserLoggedIn : state.User.isLoggedIn
}
}
const mapDispatchToProps = {
UserIsLoggedIn
}
export default connect(mapStateToProps, mapDispatchToProps) (Index);
ReactDOM.render(<Index />, document.getElementById("root"));
serviceWorker.unregister();
Note: Another aspect, mapStateToProps is not working either....
<Provider store={store}> needs to be wrapped around wherever this exported component is used. It can't be within the render method of Index. The way it is now, the connect method won't have access to your store.
You need something like:
ReactDOM.render(<Provider store={store}><Index /></Provider>, document.getElementById("root"));
and then remove the Provider portion from the render method in Index:
render() {
return (
<BrowserRouter>
<Route path="/" exact component={App} />
<Route path="/:type" component={App} />
</BrowserRouter>
);
}
You need to dispatch your action
const mapDispatchToProps = dispatch => ({
UserIsLoggedIn: (value) => {
dispatch(UserIsLoggedIn(value));
}
});
Update:
If you want to use the object syntax you need to wrap that action in a funciton:
const mapDispatchToProps = {
UserIsLoggedIn: (value) => UserIsLoggedIn(value),
};
Thank you #Yusufali2205 and #RyanCogswell for your help but those didn't fix the problem..
For people looking for an answer on this:
index.js is the first file in my React to load then App.js. With this setup, I have <Provider store={store}> inside index.js and inside index.js I don't have an access to the store within render method OR any lifecycles such as willmount or even didmount or willunmount. To access store, create it with <Provider store={store}> inside index.js and access store items within App.js.
About Dispatching action error, I still get Objects are not valid as a React child (found: object with keys {type, isLoggedIn}). If you meant to render a collection of children, use an array instead. error if I try to dispatch within render method with this code {this.props.UserIsLoggedIn(true)}.. I was attempting to do this just to see if dispatching worked, I don't plan to have it actually inside render. When I wrapped it with console.log, it worked fine, like this {console.log(this.props.UserIsLoggedIn(true))}..
When I moved these dispatchers to under a lifecycle method, they worked fine without console.log wrapper... like this
componentWillMount() {
console.log(this.props.isUserLoggedIn)
this.props.UserIsLoggedIn(true)
}

How test a React Loadable component

I have this component:
import React from 'react';
const UploadAsync = Loadable({
loader: () => import('components/Upload'),
loading: () => <LoadingComponent full />
});
const Component = () => {
return <UploadAsync />
}
export default Component
And the test:
import React from 'react';
import Component from './index';
describe('Component', () => {
it('should render correctly with upload component', () => {
const tree = create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
});
});
How I can see the Upload component and not the Loading component in the snapshot?
exports[`Content should render correctly with form component 1`] = `
<div>
<Loading
full={true}
/>
</div>
`;
So far I have tried setTimeOut and Promises.
Use Loadable.preloadAll() before the tests then you can access the Component you need.
Docs
Simple example:
all imports here
Loadable.preloadAll()
describe('TestName', () => {
//tests
})
I haven't been able to figure this out either, but here are a couple of workarounds that, alone or together, may work reasonably well:
Snapshot test the components that are passed to Loadable
In your case, the test would look something like this:
import React from 'react';
import Component from './components/Upload';
describe('Component', () => {
it('should render correctly with upload component', () => {
const tree = create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
});
});
You could also test <LoadingComponent full /> in a similar fashion. No, this doesn't assure you that the Loadable component is working, but you may find it satisfactory to assume that the react-loadable library is well tested and will work as long as you pass to it your own, properly tested components.
End-to-end browser testing
Using a framework such as Selenium or TestCafe you can write tests that run against your site as it runs in a real browser.
It seems like there is no proper solution but if your test is actually rendering the component in browser inside iframe then you can get your react component by Jquery contentDocument
$('#component-iframe')[0].contentDocument
you can find for some specific element in your component by class or id using
$('#component-iframe')[0].contentDocument.getElementById('componentID')
I have a solution, which I found accidentally, but I don't understand how it works (maybe if we could figure out, we could solve this problem). For now, it's good for a workaround.
If you call mount once outside the test, the dynamic module will load magically:
function mountApp() {
return mount(
<ApolloProvider client={apolloClient}>
<MemoryRouter initialEntries={['/']}>
<App />
</MemoryRouter>
</ApolloProvider>
);
}
mountApp();
// Tests
test('react-loadable module loads', () => {
const wrapper = mountApp();
console.log(wrapper.debug());
});
Here, the App, which contains react-loadable modules loads correctly with all its content available. When I remove the first mountApp, it doesn't work anymore (it loads only the loading string).
Edit:
Actually it works inside the test too, but this way you only need to do this once for every test to work.
I was trying to test a component which had Loadable-components inside of it, and run into a similar problem. I managed to mock those components and that made the parent-component mount (enzyme) as I wanted it to.
I'm leaving out the mocked store and props, as they aren't relevant to the solution
const component = () => {
return mount(
<Provider store={store}>
<DoubleMatrix {...props} />
</Provider>
)
}
// These are the Loadable-components, import { Grid, GridColumn } from 'App/Components/Tables/Grid' in the parent component which I am testing
jest.mock('App/Components/Tables/Grid', () => ({
Grid: () => <div />, // eslint-disable-line
GridColumn: () => <div />, // eslint-disable-line
}))
it('renders', () => {
const wrapper = component()
expect(wrapper).toMatchSnapshot()
})
Here is how you test the loaded component:
import {LoadableFoo} from './loadable-foo';
import {Foo} from './Foo';
it('should load the Foo component', async () => {
// usual shallow render
const component = shallow(<LoadableFoo/>);
// prerender the Loadable component
const loadedComponent = await component.preload();
expect(loadedComponent.Foo).toEqual(Foo);
});

Resources