How to mock machine state in jest in react? - javascript

Below is the snippet for Homepage with its unit test in jest, which will display error message if state.includes('error') and display welcome message if state.includes('success').
while I run a unit test for Homepage component, beginning state is initial and then always changes to error skipping success state without any reason, which fails the unit test. Is there a way to inject the state inside the Apptest in the unit test so that it always get success state and skip error state.
log for state
console.log
<----- State is -------> initial
console.log
<----- State is -------> homePage.error
Can we mock the state so that new state is always homepage.success?
import React from 'react';
const Homepage = props => {
const { appState, globalDispatch, globalStore } = props;
const { store, transition, state } = appState;
const { welcomeMessage } = globalStore;
//useEffect hook to check the state
useEffect(() => {
console.log('<----- State is ------->', state);
});
return(
<React.Fragment>
{state.includes('error') ? (
<p>
There was error processing your request
</p>
): null}
{state.includes('success') ? (
<p>
{welcomeMessage}
</p>
): null}
</React.Fragment>
)
}
And the unit test for above component is
import React from 'react';
import { IntlProvider } from 'react-intl';
const AppTest = props => {
const { additionalData } = props;
const { globalDispatch, globalStore } = useMothershipContext();
const { theme } = useTheme({
globalStore,
globalDispatch
});
return (
<ThemeProvider theme={theme}>
<IntlProvider>
<Homepage
globalDispatch={globalDispatch}
globalStore={globalStore}
additionalData={additionalData}
/>
</IntlProvider>
</ThemeProvider>
);
};
beforeEach(() => {
jest.setTimeout(60000);
});
test('Stage 1.0 - Display Welcome Message', async () => {
const { getByText } = render(
<TestWrapper>
<AppTest additionalData={'Welcome Lorem Ipsum'} />
</TestWrapper>
);
await waitFor(() => getByText('Welcome Lorem Ipsum'));
const welcomeMessage = getByText('Welcome Lorem Ipsum');
expect(welcomeMessage).toBeTruthy();
});
And machine is
import { Machine } from 'xstate';
export default Machine({
id: 'homePage',
initial: 'initial',
states: {
initial: {
on: {
INITIALIZE_HOMEPAGE_SUCCESS: 'success',
INITIALIZE_HOMEPAGE_ERROR: 'error'
}
}
}
})

Related

React test a component with saga

Hllo Guys, I'm having a bit trouble with testing my component
The problem is that I would like to test my React Native Component that uses saga to fetch data from server.
The Problem is that I do know what I'm supposed to do, I think I should mock my API calls in my test file but I do not know how :/
The component file is really simple, when mounted it dispatches action to fetch list on vehicles, and then it shows them in UI. And until that is fetched it shows loading text
Bellow are my current setup of components & test file.
Here is a screen component that fetches initial data on screen load
Screen Component
import React, { useContext, useEffect, useState } from 'react';
import { Platform, FlatList, View, ActivityIndicator, Text } from 'react-native';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { vehiclesActions } from '_store/vehicles';
export const MainScreen = ({ navigation }) => {
/**
* Redux selectors and dispatch
*/
const {
loading = true,
vehicles = [],
loadMore = false
} = useSelector((state) => state.vehicles);
/**
* Initial effect, fetches all vehicles
*/
useEffect(() => {
dispatch(
vehiclesActions.vehicleGet({
page: 1,
})
);
}, []);
const renderCard = () => {
return (<View><Text>Test</Text></View>)
}
if (loading) {
return (<View><Text>App Loading </Text></View>
}
return (
<View style={styles.wrapper}>
<View
style={
Platform.OS === 'ios' ? { marginTop: 30 } : { marginTop: 0, flex: 1 }
}
>
{!loading && (
<View style={Platform.OS === 'ios' ? {} : { flex: 1 }}>
<FlatList
testID={'flat-list'}
data={vehicles}
renderItem={renderCard}
/>
</View>
)}
</View>
</View>
);
};
MainScreen.propTypes = {
navigation: PropTypes.object
};
export default MainScreen;
My Vehicles Saga:
const api = {
vehicles: {
getVehicles: (page) => {
return api.get(`/vehicles/list?page=${page}`, {});
},
}
function* getVehicles(action) {
try {
const { page } = action.payload;
const { data } = yield call(api.vehicles.getVehicles, page);
yield put({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload: data });
} catch (err) {
yield call(errorHandler, err);
yield put({ type: vehiclesConstants.VEHICLE_GET_FAIL });
}
}
export function* vehiclesSaga() {
yield takeLatest(vehiclesConstants.VEHICLE_GET_REQUEST, getVehicles);
}
Actions:
export const vehiclesActions = {
vehicleGet: payload => ({ type: vehiclesConstants.VEHICLE_GET_REQUEST, payload }),
vehicleGetSuccess: payload => ({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload }),
vehicleGetFail: error => ({ type: vehiclesConstants.VEHICLE_GET_FAIL, error }),
}
Reducer
import { vehiclesConstants } from "./constants";
const initialState = {
vehicles: [],
loading: true,
};
export const vehiclesReducer = (state = initialState, action) => {
switch (action.type) {
case vehiclesConstants.VEHICLE_GET_REQUEST:
return {
...state,
loading: true,
};
case vehiclesConstants.VEHICLE_GET_SUCCESS:
return {
...state,
loading: false,
vehicles: action.payload,
};
}
}
My Test File
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
Like I see above I cannot find element with testId flat-list, since component AppScreen it always show loading text, is there any way I could mock that API call and make this to work ?
Jest allows you to mock any module using jest.mock.
You have to write an alternative to axios.get like this
const vehiclesData = [
// ... put default data here
]
const delay = (ms, value) =>
new Promise(res => setTimeout(() => res(value), ms))
const mockAxiosGet = async (path) => {
let result = null
if (path.includes('vehicles/list') {
const query = new URLSearchParams(path.replace(/^[^?]+\?/, ''))
const page = + query.get('page')
const pageSize = 10
const offset = (page - 1)*pageSize
result = vehiclesData.slice(offset, offset + pageSize)
}
return delay(
// simulate 100-500ms latency
Math.floor(100 + Math.random()*400),
{ data: result }
)
}
Then modify the test file as
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import axios from 'axios'
// enable jest mock on 'axios' module
jest.mock('axios')
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
before(() => {
// mock axios implementation
axios.get.mockImplementation(mockAxiosGet)
})
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
For your use case, read more at Mocking Implementations

useEffect called multiple times when using <Redirect> or history.push()

I got a little problem because I can't redirect logged in user to app, when he's saved in localStorage.
Both react-router-dom functions return Maximum update depth exceeded. but why?
import React, { useState, useEffect } from 'react'
import { authLocalUser } from 'actions/userActions'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { useHistory } from 'react-router-dom'
// components
import SigninForm from 'components/organisms/Forms/SigninForm'
import SignupForm from 'components/organisms/Forms/SignupForm'
// styles
import { Content, Footer, Wrapper, Header } from './styles'
const Landing = ({ fetchLocalStorage, userID }) => {
const [isModalOpen, setModalOpen] = useState(false)
const history = useHistory()
useEffect(async () => {
const userData = await JSON.parse(
localStorage.getItem('userData'),
)
await fetchLocalStorage(userData)
}, [])
return (
<>
{userID && history.push('/app')}
<Content>
{isModalOpen && (
<div
style={{
zIndex: 300,
left: 0,
position: 'absolute',
width: '100%',
height: '100%',
background: 'rgb(0,0,0,0.5)',
}}
>
<SignupForm setModalOpen={setModalOpen} />
</div>
)}
<Wrapper w='60'>
<Header>
<h1>ChatterApp</h1>
<h3>
Chat with your friend in real-time using
magic of Web Sockets! Join our community
today!
</h3>
</Header>
</Wrapper>
<Wrapper signin w='40'>
<SigninForm setModalOpen={setModalOpen} />
</Wrapper>
</Content>
<Footer>
<Content>
<h3>ChatterApp</h3>
<h5>Dawid Szemborowski</h5>
</Content>
</Footer>
</>
)
}
const mapStateToProps = ({ user }) => ({
userID: user._id,
})
const mapDispatchToProps = dispatch => ({
fetchLocalStorage: localStorage =>
dispatch(authLocalUser(localStorage)),
})
export default connect(mapStateToProps, mapDispatchToProps)(Landing)
Landing.propTypes = {
userID: PropTypes.string,
fetchLocalStorage: PropTypes.func.isRequired,
}
Landing.defaultProps = {
userID: undefined,
}
I tried calling this function without async/await, I tried providing userID and localStorage as that last parameter for componentDidUpdate. Where is my problem? Error I get displays the problem is inside Lifecycle component
index.js:1 The above error occurred in the <Lifecycle> component:
at Lifecycle (http://localhost:3000/static/js/vendors~main.chunk.js:47761:29)
at Redirect (http://localhost:3000/static/js/vendors~main.chunk.js:47862:28)
authLocalUser code
export const authLocalUser = userData => {
return {
type: 'FETCH_LOCAL_STORAGE',
payload: userData,
}
}
You probably want to do something like this.
Replacing {userID && history.push('/app')} with:
useEffect(() => {
if(userId) {
history.push('/app')
}
}, [userId])
As a suggestion, your first useEffect call can be corrected. If you make the callback of useEffect as async it will return a promise which is not the way useEffect works. It returns a cleanup function.
Use an IIFE instead:
useEffect(() => {
(async () => {
const userData = JSON.parse(localStorage.getItem('userData'))
await fetchLocalStorage(userData)
})()
}, [])

How to test a react connected component and what to test of component?

Following is a simple login component in my learning application.
I'm trying to write test of the following component via jest and testing-library/react
import React, { useEffect } from 'react';
import { Form, Input, Button, Checkbox } from 'antd';
import { connect } from "react-redux";
import { userLoginThunk } from "../Thunk/LoginThunk";
import { getUserState } from '../selector';
const Login = ({ onFormSubmitFailed, onLoginPressed, history }) => {
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 8 },
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 },
};
const onFinish = (values) => {
onLoginPressed(values, history);
};
const onFinishFailed = (errorInfo) => {
onFormSubmitFailed();
};
return (
<Form data-testid="loginForm"
{...layout}
style={{
padding: '30vh',
alignItems: 'center'
}}
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input data-testid="UsernameInput" placeholder="UserName" />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password data-testid="PasswordInput" placeholder="Password" />
</Form.Item>
<Form.Item {...tailLayout}>
<Button data-testid="submitButton" type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
// const mapStateToProps = state => ({
// logingIn: getUserState(state),
// });
// const mapDispatchToProp = dispatch => ({
// onLoginPressed: (userObj, history) => dispatch(userLoginThunk(userObj, history)),
// });
export default connect((state) => ({
logingIn: getUserState(state)
}), {
onLoginPressed: (userObj, history) => userLoginThunk(userObj, history)
})(Login);
//export default connect(mapStateToProps, mapDispatchToProp)(Login);
I read in a discussion that we don't need mapStateToProps, mapDispatchToProp so I removed them too but still my test is getting failed.
What i'm trying to achieve is that passing a mock function as prop and recording that It was executed once as my
onFinish function is getting called in my login component, but as I have passed my mocked jest function as a prop it's not executing and passing my test.
test.only("Login On Submit", () => {
const onLoginPressed = jest.fn();
//(() => {throw new Error('QWERTY')});
// // (values, history) => {
// // console.log('Test');
// // console.log('inside Test', values)
// // }
const { getByTestId } = render(
<Provider store={storeMock}>
<Login onLoginPressed={onLoginPressed} />
</Provider>
);
fireEvent.change(screen.getByTestId(/UsernameInput/i), {
target: { value: 'admin' }
});
fireEvent.change(screen.getByTestId(/PasswordInput/i), {
target: { value: 'admin' }
});
fireEvent.click(screen.getByTestId("submitButton"));
expect(onLoginPressed).toHaveBeenCalled();
});
Test result
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
90 | fireEvent.click(screen.getByTestId("submitButton"));
91 | console.log("debug after testUtil render", debug());
> 92 | expect(onLoginPressed).toHaveBeenCalled();
| ^
93 |
94 | });
95 | });
Issue
The issue here is that your component is still having props from redux injected into it by the connect Higher Order Component. (You removed mapStateToProps and mapDispatchToProps but you've still "defined" them inline in connect) The injected onLoginPressed is overwriting your manually passed jest mock function.
Solution
IMO the easy solution, since you are still using the connect decorator, is to export the undecorated/unconnected Login component so you can pass the props that would normally be provided by redux.
export Login = ({ onFormSubmitFailed, onLoginPressed, history }) => {...
Test
import { Login } from './path/to/Login'; // <-- named import
...
test.only("Login On Submit", () => {
const onLoginPressed = jest.fn();
const { getByTestId } = render(Login onLoginPressed={onLoginPressed} />);
fireEvent.change(screen.getByTestId(/UsernameInput/i), {
target: { value: 'admin' }
});
fireEvent.change(screen.getByTestId(/PasswordInput/i), {
target: { value: 'admin' }
});
fireEvent.click(screen.getByTestId("submitButton"));
expect(onLoginPressed).toHaveBeenCalled();
});
Solution #2
Mock your store using redux-mock-store and assert the correct action(s) was/were dispatched. Here the mock store simply stores an array of dispatched actions to test against.
test.only("Login On Submit", () => {
const store = mockStore({});
const { getByTestId } = render(
<Provider store={store}>
<Login onLoginPressed={onLoginPressed} />
</Provider>
);
fireEvent.change(screen.getByTestId(/UsernameInput/i), {
target: { value: 'admin' }
});
fireEvent.change(screen.getByTestId(/PasswordInput/i), {
target: { value: 'admin' }
});
fireEvent.click(screen.getByTestId("submitButton"));
const actions = store.getActions();
expect(actions).toEqual([{ type: 'LOGIN' }]); // <-- your action type here!
});
I think there's a few key issues with the overall approach to testing that we should address:
How do we test Redux connected components?
How do we test behavior?
Test Connected Components by hijacking render()
Our team has a testUtils wrapper that hijacks the react-testing-library render() method and wraps components with a Redux Provider. You can find it on the Redux docs.
// test-utils.js
import React from 'react'
import { render as rtlRender } from '#testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
// Import your own reducer
import reducer from '../reducer'
function render(
ui,
{
initialState,
store = createStore(reducer, initialState),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
// override render method
export { render }
What's cool about this solution is you get your Redux store and you can initialize state the way you need.
Test behavior, not implementation details
For the next issue, I wouldn't test the mock/implementation. Does your form submit to a backend? Does it show a success message? You could use that to determine the test assertions. Below I used your test with the wrapped render() method and look for a flash message that signifies successful login.
test.only("<Login/>", async () => {
// You can stub the server response with nock or msw
nock(`${yoursite}`)
.post('/someApiEndpoint')
.reply(200);
const { getByTestId } = render(
<Login />
);
fireEvent.change(screen.getByTestId(/UsernameInput/i), {
target: { value: 'admin' }
});
fireEvent.change(screen.getByTestId(/PasswordInput/i), {
target: { value: 'admin' }
});
fireEvent.click(screen.getByTestId("submitButton"));
await waitFor(() => expect(getById('login-success')).toBeTruthy()); // <-- This is missing
});
What, as a user, would signify a successful login?

Enzyme mount wrapper is empty after simulate('click') in ReactApp

I'm trying to test a registration component that has a Vertical Stepper with Jest/Enzyme and I keep hitting a wall when trying to simulate the user clicking "Next" .
expected behavior is to do nothing if the "Required" input fields are empty, however after doing the .simulate('click') following assertions fail with not finding any html in the wrapper.
The component is passed through react-redux connect() so I don't know if that would be related.
UserRegistration.js
import React from 'react';
import { connect } from 'react-redux';
import Stepper from '#material-ui/core/Stepper';
import Step from '#material-ui/core/Step;
import StepLabel from '#material-ui/core/StepLabel;
import StepContent from '#material-ui/core/StepContent'
class UserRegistration extends React.Component {
constructor(props){
this.state = {
activeStep: 0,
inputData: {},
...
}
}
getStepContent = () => {
switch(this.state.activeStep)
case '...':
return
(<>
<input test-data="firstName"/>
...
</>);
...
}
render () {
const steps = ['Personal Info', 'Corporate Details', ...]
return (
<Stepper activeStep={this.state.activeStep} orientation="vertical">
{steps.map((label, index) => {
return (
<Step key={index}/>
<StepLabel>{label}</StepLabel>
<StepContent>
{this.getStepContent()}
<button data-test="btn-next" onClick={() => this.goNext()}> NEXT </button>
<button onClick={() => this.goBack()}> BACK </button>
)
}
}
</Stepper>
)
}
}
const mapStateToProps = () => {...}
const mapDispatchToProps = () => {...}
export default connect(mapStateToProps, mapDispatchToProps)(UserRegistration)
UserRegistration.test.js
const wrapper = mount(
<Provider store={store}
<UserCreate/>
</Provider>
)
it('Confirm REQUIRED fields rendered', () => {
expect(wrapper.find("input[data-test='firstName']").length).toEqual(1);
// PASS
});
it('Check if still on same step clicked NEXT with no user data', () => {
wrapper.find("button[data-test='btn-next']").simulate('click');
expect(wrapper.find("input[data-test='firstName']").length).toEqual(1);
// Expected value to equal: 1, Received: 0
})
Same outcome regardless of the element I'm looking up.
Any suggestions will be greatly appreciated.
You need to update. So you would change it:
it('Check if still on same step clicked NEXT with no user data', () => {
wrapper.find("button[data-test='btn-next']").simulate('click');
// Add this line
wrapper.update();
const button = wrapper.find("input[data-test='firstName']");
expect(button.length).toEqual(1);
// Expected value to equal: 1, Received: 0
});
Then the test should work as you intend.

Test properties that injected by React Redux

I have a component that renders a button if a property errorMessage is not null.
class App extends Component {
static propTypes = {
// Injected by React Redux
errorMessage: PropTypes.string,
resetErrorMessage: PropTypes.func.isRequired,
};
renderErrorMessage() {
const { errorMessage } = this.props;
if (!errorMessage) return null;
return (
<p id="error-message">
<b>{errorMessage}</b>{' '}
<button id="dismiss" onClick={this.props.resetErrorMessage()}>
Dismiss
</button>
</p>
);
}
render() {
return (
<div className="app">
{this.renderErrorMessage()}
</div>
);
}
}
The property injected by React Redux:
import { connect } from 'react-redux';
import App from '../components/App/App';
const mapStateToProps = (state, ownProps) => ({
errorMessage: state.errorMessage,
});
export default connect(mapStateToProps, {
resetErrorMessage: () => ({
type: 'RESET_ERROR_MESSAGE',
})
})(App);
As you can see I also have resetErrorMessage that clears errorMessage:
const errorMessage = (state = null, action) => {
const { type, error } = action;
if (type === RESET_ERROR_MESSAGE) {
return null;
} else if (error) {
return error;
}
return state;
};
How can I test my component and say if I click the button then button hides or if errorMessage is not null button shows?
I want to get something like this:
const props = {
errorMessage: 'Service Unavailable',
resetErrorMessage,
};
it('renders error message', () => {
const wrapper = shallow(<App {...props} />);
expect(wrapper.find('#error-message').length).toBe(1);
wrapper.find('#dismiss').simulate('click');
expect(wrapper.find('#error-message').length).toBe(0);
});
But now my problem is that if I simulate click to dismiss button - error message doesn't hide.
As I posted in the previous question you deleted, if you want to test button clicks your best bet would be to call the 'unconnected' component. If you want to test the connected component, then you have to pass a mockstore into it like so.
const wrapper = shallow(<App {...props} store={store} />);
So import the app in your test and just pass the resetErrorMessage function as a mocked function, such as what you do with jest.
const resetErrorMessage = jest.fn(() => {});
const wrapper = shallow(<App {...props} resetErrorMessage={resetErrorMessage} />);
wrapper.find('#dismiss').simulate('click');
expect(resetErrorMessage).toHaveBeenCalled();
My advice would be to only test the connected component when you want to manipulate directly from store changes.

Categories

Resources