Can't access to state from redux action in jest - javascript

I cannot have my state when dispatch an action inside my jest unit test
I try to make like this :
import configureMockStore from 'redux-mock-store'
import * as React from 'react'
import MarginPage from './index'
import { Provider } from 'react-redux'
import {
mount,
} from 'enzyme'
import thunk from 'redux-thunk'
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
const state = {
margin: {
list: null,
},
}
it('Should list props are correctly filling', async () => {
await store.dispatch({
type: 'SET_MARGIN_LIST',
payload: [0, 1, 2],
})
const wrapper = mount(
<Provider store={store}>
<MarginPage />
</Provider>,
)
wrapper.update()
const actions = store.getActions() // Here I have my state
// But now I would like to have the updated state list inside getState()
console.log(store.getState().margin.list) // return undefined
})

I found an answer I hope its helping you if you encountered this problem.
We can take the above code and just update :
import * as React from 'react'
import MarginPage from './index'
import { Provider } from 'react-redux'
import {
mount,
} from 'enzyme'
// #ts-ignore
import store from '#App/helpers/configureStore' // Finally I prefer use store from my app
import { IAppState } from '#App/reducers' // This line come from interface of typeScript
describe('<MarginPage /> Component', () => {
it('Should list props are correctly filling', async () => {
// instanciate component and make the test use async/await
const wrapper = mount(
<Provider store={store}>
<MarginPage />
</Provider>,
)
await store.dispatch({
type: 'SET_MARGIN_LIST',
payload: MOCK_MARGIN_LIST,
})
// Don't forget to force the re-render
wrapper.update()
// Now we have access to our store
const state = (store.getState() as IAppState).margin.list as any
expect(state.results[0].campaigns_revenue).toEqual(47096.52)
})
})

Related

Refactoring thunk slices to RTK-Query causing top-level API calls invalid

I'm working on an app using react / redux / redux-toolkit.
Until now, I was using createAsyncThunk to wrap API calls, but for more convenience I decided to use RTK-Query.
It works fine in most cases. However, there's a situation in which I'm unable to make it work. This is when I want the API call to be done directly at the app startup, in the index.js file, as follows :
Working version :
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from "react-redux";
import App from './App';
import store from './store';
import {fetchUserInfo} from "./features/user";
store.dispatch(fetchUserInfo());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root')
);
Here, the store.dispatch(fetchUserInfo()); is placed here so that store.user.info will be available in any subsequent component.
The definition of the fetchUserInfo() is made here :
features/user.js
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
import {apiCall} from "./utils";
export const fetchUserInfo = createAsyncThunk(
'user/info',
async (thunkAPI) => {
const res = await apiCall('https://my.api/userinfo').json();
return res;
});
export const userSlice = createSlice({
name: 'user',
initialState: { info: '' },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserInfo.fulfilled, (state, action) => {
state.info = action.payload;
})
},
});
export default userSlice.reducer;
What I would like to do is replace this slice by something like that :
Errored Version :
api/user.js
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
const rawBaseQuery = (baseUrl) => fetchBaseQuery({
baseUrl,
prepareHeaders: (headers, {getState}) => {
headers.set('Authorization', '*******');
return headers
}
});
export const baseQuery = async (args, api, extraOptions) => {
const baseUrl = api.getState().api.root; // 'https://my.api'
return rawBaseQuery(baseUrl)(args, api, extraOptions);
};
export const userApi = createApi({
reducerPath: 'user',
baseQuery: baseQuery,
endpoints: (builder) => ({
getUserInfo: builder.query({
query: () => `/userinfo`
}),
}),
});
export const {
useGetUserInfoQuery
} = userApi;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from "react-redux";
import App from './App';
import store from './store';
import {useGetUserInfoQuery} from "./api/user";
store.dispatch(useGetUserInfoQuery());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root')
);
But, I can not call useGetUserInfoQuery() in ìndex.js. The following error occurs :
React Hook use query cannot be called at the top level.
I also tried to call useGetUserInfoQuery() without dispatch but the same error occurs.
How should I proceed? Thanks!

React Testing Library with Redux - Mocking Axios in action creator

I have an app I'm adding integration tests to for learning React Testing Library.
It's built in MERN stack, along with Redux for state management.
My test wrapper is a standard setup:
import React from 'react';
import { render as rtlRender } from '#testing-library/react';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const render = (
ui,
{
initialState,
store = createStore(rootReducer, compose(applyMiddleware(thunk))),
...renderOptions
} = {}
) => {
const Wrapper = ({ children }) => {
return <Provider store={store}>{children}</Provider>;
};
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
};
export * from '#testing-library/react';
export { render };
At the moment I'm trying to test a login form and errors that are returned when fields aren't valid.
import React from 'react';
import { Router } from 'react-router';
import '#testing-library/jest-dom';
import { createMemoryHistory } from 'history';
import { render, screen, fireEvent, waitFor } from '../../../utils/test-utils';
import Login from '../login';
jest.mock('axios', () => {
return {
post: jest.fn()
};
});
describe('<Login/>', () => {
beforeEach(() => {
// Some requirements for the component to render in the test
const history = createMemoryHistory();
const state = '';
history.push('/', state);
render(
<Router history={history}>
<Login history={history} />
</Router>
);
});
test('should show empty email error', async () => {
fireEvent.input(screen.getByRole('textbox', { name: /email/i }), {
target: {
value: ''
}
});
fireEvent.submit(screen.getByRole('button', { name: /login/i }));
await waitFor(() => {
expect(screen.getByText(/email field is required/i)).toBeInTheDocument();
});
});
});
Unfortunately, when I run this test it gives me TypeError: Cannot read property 'then' of undefined and I can't figure out why
My action looks like:
export const loginUser = (userData) => (dispatch) => {
return axios
.post('/api/users/login', userData)
.then((res) => {
const { token } = res.data;
localStorage.setItem('jwtToken', token);
setAuthToken(token);
const decoded = jwt_decode(token);
dispatch(setCurrentUser(decoded));
})
.catch((err) =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
};
Instead of testing the actions/reducers in my codebase I'd rather test what the user should see, which is what I've read Testing Library encourages.
I'm also using redux hooks - useDispatch/useSelector, throughout my app.
Any help would be grateful :)

Test React Component using Redux and react-testing-library

I am new to testing redux connected components in React and trying to figure out how to test them.
Currently I'm using react-testing-library and having trouble setting up the my renderWithRedux function to correctly setup redux.
Here is a sample component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class Sample extends Component {
constructor(props) {
super(props);
this.state = {
...
}
}
componentDidMount() {
//do stuff
console.log(this.props)
}
render() {
const { user } = this.props
return(
<div className="sample">
{user.name}
</div>
)
}
}
const mapStateToProps = state => ({
user: state.user
})
export default connect(mapStateToProps, {})(Sample);
Here is a sample test:
import React from 'react';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { render, cleanup } from 'react-testing-library';
import Sample from '../components/sample/'
const user = {
id: 1,
name: "John Smith"
}}
function reducer(state = user, action) {
//dont need any actions at the moment
switch (action.type) {
default:
return state
}
}
function renderWithRedux(
ui,
{ initialState, store = createStore(reducer, initialState) } = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
}
}
afterEach(cleanup)
test('<Sample> example text', () => {
const { getByTestId, getByLabelText } = renderWithRedux(<Sample />)
expect(getByText(user.name))
})
The user prop value always results as undefined. I have re-written this a couple of ways but can't seem to get it to work. If I pass the user data directly as a prop to Sample component in the test, it still resolves to be undefined.
I am learning from the tutorials and examples via the offical docs, like this one: https://github.com/kentcdodds/react-testing-library/blob/master/examples/tests/react-redux.js
Any pointers, tips or solutions would be greatly appreciated!
You should wrap the component inside Provider, here is the simple example
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import { Provider } from "react-redux";
import configureMockStore from "redux-mock-store";
import TestedComponent from '../index';
const mockStore = configureMockStore();
const store = mockStore({});
const renderTestedComponent = () => {
return render(
<Provider store={store}>
<TestedComponent />
</Provider>
);
};
describe('test TestedComponent components', () => {
it('should be render the component correctly', () => {
const { container } = renderTestedComponent();
expect(container).toBeInTheDocument();
});
});
**Unable to Fire event using #testing-library**
// demo.test.js
import React from 'react'
import { Provider } from "react-redux";
import '#testing-library/react/cleanup-after-each'
import '#testing-library/jest-dom/extend-expect'
import { render, fireEvent } from '#testing-library/react'
// this is used to fire the event
// import userEvent from "#testing-library/user-event";
//import 'jest-localstorage-mock';
import ChangePassword from './ChangePassword';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
test('test 1-> Update User password', () => {
// global store
const getState = {
authUser :{
user : {
email: "test#gmail.com",
id: 0,
imageURL: null,
name: "test Solutions",
roleId: 1,
roleName: "testRole",
userName: "testUserName"
},
loading: false,
showErrorMessage: false,
errorDescription: ""
}
}; // initial state of the store
// const action = { type: 'LOGIN_USER' };
// const expectedActions = [action];
// const store = mockStore(getState, expectedActions);
const onSaveChanges = jest.fn();
const changePassword = jest.fn();
const store = mockStore(getState);
const { queryByText, getByLabelText, getByText , getByTestId , getByPlaceholderText, } = render(
<Provider store={store}>
<ChangePassword
onSaveChanges={onSaveChanges}
changePassword={changePassword}
/>
</Provider>,
)
// test 1. check the title of component
expect(getByTestId('updateTitle')).toHaveTextContent('Update your password');
// test 2. chekck the inputfile
expect(getByPlaceholderText('Old Password')) //oldpassword
expect(getByPlaceholderText('New Password')) //newpassword
expect(getByPlaceholderText('Confirm New Password')) //confpassword
// change the input values
fireEvent.change(getByPlaceholderText("Old Password"), {
target: { value: "theOldPasword" }
});
fireEvent.change(getByPlaceholderText("New Password"), {
target: { value: "#Ab123456" }
});
fireEvent.change(getByPlaceholderText("Confirm New Password"), {
target: { value: "#Ab123456" }
});
// check the changed input values
expect(getByPlaceholderText('Old Password').value).toEqual("theOldPasword");
expect(getByPlaceholderText('New Password').value).toEqual("#Ab123456");
expect(getByPlaceholderText('Confirm New Password').value).toEqual("#Ab123456");
expect(getByText('Save Changes')); // check the save change button
// calling onSave function
//fireEvent.click(getByTestId('savechange'))
// userEvent.click(getByText('Save Changes'));
})

Test connected component in React/Redux

I am trying test my connected component of my React/Redux app and I wrote some test case which actually throws the error:
App component › shows account info and debits and credits`
Invariant Violation: Could not find "store" in either the context or props of "Connect(AccountInfo)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(AccountInfo)".
The test case which trow an error app.test.js is below. And my problem is that I don't understand what should I wrap here by Connect() because I didn't use AccountInfo here:
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import App from './App';
import * as actions from '../../actions';
function setup() {
const props = {
errorMessage: null,
actions
};
const enzymeWrapper = mount(<App {...props} />);
return {
props,
enzymeWrapper,
};
}
describe('App component', () => {
it('shows account info and debits and credits`', () => {
const {enzymeWrapper} = setup();
expect(enzymeWrapper.find('.account-info').exists()).toBe(true);
expect(enzymeWrapper.find('.debits-and-credits').exists()).toBe(true);
});
it('shows error message', () => {
const {enzymeWrapper} = setup();
enzymeWrapper.setProps({ errorMessage: 'Service Unavailable' });
expect(enzymeWrapper.find('.error-message').exists()).toBe(true);
});
});
My containers/app.js:
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions';
import AppComponent from '../components/App/App';
const mapStateToProps = state => ({
isFetching: state.balance.isFetching,
errorMessage: state.errorMessage,
});
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(actions, dispatch),
});
const AppContainer = connect(mapStateToProps, mapDispatchToProps)(AppComponent);
export default AppContainer;
The component app.js:
import React, { Component } from 'react';
import ErrorMessage from '../../containers/ErrorMessage';
import AccountInfo from '../../containers/AccountInfo';
import DebitsAndCredits from '../../containers/DebitsAndCredits';
import './App.css';
const AppComponent = () =>
<div className="app">
<AccountInfo />
<DebitsAndCredits />
</div>;
export class App extends Component {
componentWillMount() {
const { actions } = this.props;
actions.fetchBalance();
}
render() {
const { errorMessage } = this.props;
return errorMessage ? <ErrorMessage /> : <AppComponent />;
}
}
export default App;
UPD:
I updated my test case and now it looks like:
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import createSagaMiddleware from 'redux-saga';
import { initialState } from '../../reducers/balance/balance';
import App from './App';
import * as actions from '../../actions';
const middlewares = [createSagaMiddleware];
const mockStore = configureMockStore(middlewares);
const store = mockStore(initialState);
function setup() {
const props = {
errorMessage: null,
actions,
};
const enzymeWrapper = mount(
<Provider store={store}>
<App {...props} />
</Provider>
);
return {
props,
enzymeWrapper,
};
}
describe('App component', () => {
it('shows account info and debits and credits`', () => {
const { enzymeWrapper } = setup();
expect(enzymeWrapper.find('.account-info').exists()).toBe(true);
expect(enzymeWrapper.find('.debits-and-credits').exists()).toBe(true);
});
it('shows error message', () => {
const { enzymeWrapper } = setup();
enzymeWrapper.setProps({ errorMessage: 'Service Unavailable' });
expect(enzymeWrapper.find('.error-message').exists()).toBe(true);
});
});
And my error now is:
App component › shows account info and debits and credits`
TypeError: Cannot read property 'account' of undefined
UPD 2:
My initialState which I put when I create mocked store:
const initialState = {
isFetching: false,
account: {},
currency: '',
debitsAndCredits: [],
};
My AccountInfo component:
import React from 'react';
const AccountInfo = ({ account, currency }) =>
<header className="account-info">
<p>{account.name}</p>
<p>
IBAN: {account.iban}<br />
Balance: {account.balance}<br />
Currency: {currency}<br />
</p>
</header>;
export default AccountInfo;
For testing the connected component, you need to mock the provider as well, since the connect picks state variables from redux store.
Do this
const enzymeWrapper = mount (<Provider store={mockStore}><App {...props}/></Provider>)
You need to mock the redux store too.
Edit 1:
Just looking at your AccountInfo component it tells me that you are expecting account in the props here.
AccountInfo = ({account}) =>
So that means App.js has to pass down the accounts' value in the props. Same thing goes for currency.

TypeError: Cannot read property 'pathname' of undefined in react/redux testing

I'm testing some react components, a basic tests suite just to know if a component is rendering and their childs.
I'm using redux-mock-store to make the store and {mount} enzyme to mount the container in a provider, but even mocking the correct store this error is always fired:
TypeError: Cannot read property 'pathname' of undefined
Here is my very deadly basic test:
import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import App from '../containers/App.container';
describe('App', () => {
let wrapper;
const mockStore = configureStore([]);
const store = mockStore({
router: {
location: { pathname: '/home', query: {}, search: '' },
params: {}
}
});
console.log(store.getState());
beforeEach(() => {
wrapper = mount(
<Provider store={store}>
<App />
</Provider>
);
});
it('Should render app and container elements', () => {
expect(wrapper.find('.app').exists()).toBeTruthy();
expect(wrapper.find('.container').exists()).toBeTruthy();
});
it('Should render the navbar', () => {
expect(wrapper.find('nav').exists()).toBeTruthy();
});
});
And the (even more) simple component / container:
import React, { Component } from 'react';
import NavBar from '../components/Navbar';
class App extends Component {
render() {
const { location, logout} = this.props;
console.log(location);
return (
<section className='app'>
<NavBar location={location.pathname} onLogoutClick={logout}/>
<div className='container'>
{this.props.children}
</div>
</section>
);
}
}
export default App;
Container:
import { connect } from 'react-redux';
import { signOut } from '../actions/auth.actions'
import App from '../components/App';
const mapStateToProps = (state, ownProps) => {
return {
location: ownProps.location
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
logout: () => {
dispatch(signOut())
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
I can't figure out the problem of the test, the mockStore is in the correct format:
Any idea?
Update:
Thinking about it, I have no reducer / prop in the rootReducer for the location, but, i just want to pass down through the children components the location object properties that react-redux-router make available in the ownProps argument.
Weird fact: logging the location property in the app returns me the correct object.
In the tests, is always undefined... (as the error shows).
Here is my rootReducer:
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { routerReducer } from 'react-router-redux';
import authReducer from './auth.reducer';
import analysisReportsReducer from './AnalysisReports.reducer';
import titleAnalysisReducer from './TitleAnalysis.reducer';
import postsReportsReducer from './PostsReports.reducer';
const rootReducer = combineReducers({
form: formReducer,
routing: routerReducer,
auth: authReducer,
analysis: titleAnalysisReducer,
analysis_reports: analysisReportsReducer,
posts: postsReportsReducer
});
export default rootReducer;
It looks like your location object is scoped beneath the router.
Your test may be grabbing the window.location property, which your test suite may not replicate, assuming the test is cli and not in a browser.
Perhaps try:
<NavBar location={this.props.router.location.pathname} onLogoutClick={logout}/>

Categories

Resources