I have a simple component with a button, that when pressed fetches a comments JSON from placeholder API.
My enzyme test with mount() is failing, even though I can see that the state is updating in the CommentList component.
My manual tests in the browser display the comments fine.
My test with mount and a mock store passes.
I can even see that 2 li elements are created if I debug or console.log in CommentList.
Does it seem like the view is not being updated in mount after the redux state change?
Apologies for the amount of code below, I'm not sure which part is the culprit. The project can be cloned from https://github.com/Hyllesen/react-tdd
integration.test.js (failing test)
import React from "react";
import { mount } from "enzyme";
import Root from "Root";
import CommentList from "components/CommentList";
import moxios from "moxios";
beforeEach(() => {
moxios.install();
moxios.stubRequest("http://jsonplaceholder.typicode.com/comments", {
status: 200,
response: [{ name: "Fetched #1" }, { name: "Fetched #2" }]
});
});
it("can fetch a list of comments and display them", () => {
//Render entire app
const wrapped = mount(
<Root>
<CommentList />
</Root>
);
//Find fetchComments button and click it
wrapped.find(".fetch-comments").simulate("click");
wrapped.update();
//Expect to find a list of comments
expect(wrapped.find("li").length).toBe(2);
});
Root.js
import React from "react";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import reducers from "reducers";
import reduxPromise from "redux-promise";
export default ({
children,
store = createStore(reducers, {}, applyMiddleware(reduxPromise))
}) => {
return <Provider store={store}>{children}</Provider>;
};
CommentList.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchComments } from "actions";
class CommentList extends Component {
renderComments() {
console.log(this.props.comments);
return this.props.comments.map(comment => <li key={comment}>{comment}</li>);
}
render() {
const comments = this.renderComments();
//This is actually creating 2 li elements in the test
console.log("render", comments);
return (
<div>
<button className="fetch-comments" onClick={this.props.fetchComments}>
Fetch comments
</button>
<ul>{comments}</ul>
</div>
);
}
}
function mapStateToProps(state) {
return { comments: state.comments };
}
export default connect(
mapStateToProps,
{ fetchComments }
)(CommentList);
actions/index.js
import { FETCH_COMMENTS } from "actions/types";
import axios from "axios";
export function fetchComments() {
const response = axios.get("http://jsonplaceholder.typicode.com/comments");
return {
type: FETCH_COMMENTS,
payload: response
};
}
reducers/comments.js
import { FETCH_COMMENTS } from "actions/types";
export default function(state = [], action) {
switch (action.type) {
case FETCH_COMMENTS:
const comments = action.payload.data.map(comment => comment.name);
return [...state, ...comments];
default:
return state;
}
}
reducers/index.js
import { combineReducers } from "redux";
import commentsReducer from "reducers/comments";
export default combineReducers({
comments: commentsReducer
});
CommentList.test.js (test passing, using mock store)
import React from "react";
import { mount } from "enzyme";
import Root from "Root";
import CommentList from "components/CommentList";
import createMockStore from "utils/createMockStore";
let wrapped, store;
beforeEach(() => {
const initialState = {
comments: ["Comment 1", "Comment 2"]
};
store = createMockStore(initialState);
wrapped = mount(
<Root store={store}>
<CommentList />
</Root>
);
});
afterEach(() => {
wrapped.unmount();
});
it("Creates one li per comment", () => {
expect(wrapped.find("li").length).toBe(2);
});
it("shows text for each comment", () => {
expect(wrapped.render().text()).toEqual("Fetch commentsComment 1Comment 2");
});
It looks like your problem is caused by your moxios request stubbing. I think you need to wait for the response to be returned before calling update() on your wrapper.
beforeEach(() => {
moxios.install()
})
it('can fetch a list of comments and display them', done => {
// Render entire app
const wrapped = mount(
<Root>
<CommentList />
</Root>
)
// Find fetchComments button and click it
wrapped.find('.fetch-comments').simulate('click')
moxios.wait(() => {
let request = moxios.requests.mostRecent()
request
.respondWith({
status: 200,
response: [{ name: 'Fetched #1' }, { name: 'Fetched #2' }]
})
.then(function () {
wrapped.update()
expect(wrapped.find('li').length).toBe(2)
done()
})
})
})
Related
I need some help figuring this issue out. After setting up Redux store and reducer in my app, I was able to successfully log and render updated state upon click in one, but not multiple pages. Below are steps and code sample:
Step1:
I installed Redux and wrapped the store around my entire app
// _app.js
import Layout from '../components/Layout';
import { SessionProvider } from 'next-auth/react';
import store from '../store';
import { Provider } from 'react-redux';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<>
<Provider store={store}>
<SessionProvider session={session}>
<Layout>
<Component {...pageProps} />
<ToastContainer />
</Layout>
</SessionProvider>
</Provider>
</>
);
}
export default MyApp;
Step 2:
Setup an instance of a slice, Store and reducer
// mySlice.js
import { createSlice} from '#reduxjs/toolkit';
const initialState = {
user: {
role: ""
},
};
export const userStatusSlice = createSlice({
name: 'userStatus',
initialState,
reducers: {
userInfo: (state, action) => {
state.user.role = action.payload.role; // only this value comes from payload onClick
},
},
});
// Action creators are generated for each case reducer function
export const { userInfo } = userStatusSlice.actions;
export default userStatusSlice.reducer;
Step 3: Store...
//store.js
import { configureStore } from '#reduxjs/toolkit';
import userStatusSlice from './slices/userSlice/userStatus';
export default configureStore({
reducer: {
userStatus: userStatusSlice,
},
});
Step 4: Setup pages and React Hook useSelector, and tried accessing dispatched actions set as state variables in multiple pages. On one page I was able to fetch the data successfully, but not on the other page(s)
//First Page
import { useSession, getSession } from 'next-auth/react';
import { useSelector } from 'react-redux';
const firstPage = () => {
const { data: session } = useSession();
const { role } = useSelector((state) => state.userStatus.user);
console.log(role); // There is role successfully logged to the console
return (
<>
</>
);
};
export default firstPage;
//Second page.js
import { useSession } from 'next-auth/react';
import { useSelector } from 'react-redux';
const secondPage = () => {
const { data: session } = useSession();
const { role } = useSelector((state) => state.userStatus.user);
console.log(role) // There is NO role - why?
return (
<>
</>
);
};
export default secondPage;
I appreciate all input to help resolving this issue. Thanks in advance
Am new in writing testcases using React Test library.
Here is my component
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
class MyContainer extends React.Component {
static propTypes = {
graphicalData: PropTypes.object.isRequired,
};
render() {
const { graphicalData } = this.props;
return (
graphicalData && (
<div>
/////some action and rendering
</div>
))}
}
const mapStateToProps = (state) => {
return {
graphicalData: state.design.contents ? state.design.contents.graphicalData : {},
};
};
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(MyContainer)));
So i am writing my test case file using React Testing library
import React from 'react';
import '#testing-library/jest-dom';
import { render, cleanup, shallow } from '#testing-library/react';
import MyContainer from '../../MyContainer';
import configureMockStore from 'redux-mock-store';
import ReactDOM from 'react-dom';
const mockStore = configureMockStore();
import { Provider } from 'react-redux';
const store = mockStore({
state: {
design: {
contents: {
graphicalModel: { cars: [{},{}], bikes: [{},{}] },
},
},
},
});
afterEach(cleanup);
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(
<Provider store={store}>
<MyContainer />
</Provider>,
div
);
ReactDOM.unmountComponentAtNode(div);
});
Am not sure what went wrong , my idea is to check if the component loads without crashing , also if the cars array length is greater than 0 , check something rendered on page.
But am getting some error, any help with example or suggestion will save my day
The error seems correct. Check the structure that you have passed into the mockStore() function.
It is
state: {
design: {
contents: {
graphicalModel: { cars: [{},{}], bikes: [{},{}] },
},
},
},
So, when you access in your MyContainer, you should access it as
state.state.design.contents
It is just that you have an extra hierarchy in the state object.
Or, if you don't want to change the component, change the structure of the state passed into your mockStore() method as:
const store = mockStore({
design: {
contents: {
graphicalModel: { cars: [{},{}], bikes: [{},{}] },
},
},
});
I am trying to implement Redux in a Next.js app and have problems getting the dispatch function to work in getInitialProps. The store is returned as undefined for some reason that I cannot figure out. I am using next-redux-wrapper. I have followed the documentation on next-redux-wrapper GitHub page but somewhere on the way it goes wrong. I know the code is working - I used axios to directly fetch the artPieces and then it worked just fine but I want to use Redux instead. I am changing an react/express.js app to a Next.js app where I will use the API for the basic server operations needed. This is just a small blog app.
Here is my store.js:
import { createStore } from 'redux';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
// create your reducer
const reducer = (state = { tick: 'init' }, action) => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload };
case 'TICK':
return { ...state, tick: action.payload };
default:
return state;
}
};
// create a makeStore function
const makeStore = (context) => createStore(reducer);
// export an assembled wrapper
export const wrapper = createWrapper(makeStore, { debug: true });
And here is the _app.js:
import './styles/globals.css';
import { wrapper } from '../store';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default wrapper.withRedux(MyApp);
And finally here is where it does not work. Trying to call dispatch on the context to a sub component to _app.js:
import React from 'react';
import { ArtPiecesContainer } from './../components/ArtPiecesContainer';
import { useDispatch } from 'react-redux';
import axios from 'axios';
import { getArtPieces } from '../reducers';
const Art = ({ data, error }) => {
return (
<>
<ArtPiecesContainer artPieces={data} />
</>
);
};
export default Art;
Art.getInitialProps = async ({ ctx }) => {
await ctx.dispatch(getArtPieces());
console.log('DATA FROM GETARTPIECES', data);
return { data: ctx.getState() };
};
This should probably work with "next-redux-wrapper": "^7.0.5"
_app.js
import { wrapper } from '../store'
import React from 'react';
import App from 'next/app';
class MyApp extends App {
static getInitialProps = wrapper.getInitialAppProps(store => async ({Component, ctx}) => {
return {
pageProps: {
// Call page-level getInitialProps
// DON'T FORGET TO PROVIDE STORE TO PAGE
...(Component.getInitialProps ? await Component.getInitialProps({...ctx, store}) : {}),
// Some custom thing for all pages
pathname: ctx.pathname,
},
};
});
render() {
const {Component, pageProps} = this.props;
return (
<Component {...pageProps} />
);
}
}
export default wrapper.withRedux(MyApp);
and Index.js
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { END } from 'redux-saga'
import { wrapper } from '../store'
import { loadData, startClock, tickClock } from '../actions'
import Page from '../components/page'
const Index = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(startClock())
}, [dispatch])
return <Page title="Index Page" linkTo="/other" NavigateTo="Other Page" />
}
Index.getInitialProps = wrapper.getInitialPageProps(store => async (props) => {
store.dispatch(tickClock(false))
if (!store.getState().placeholderData) {
store.dispatch(loadData())
store.dispatch(END)
}
await store.sagaTask.toPromise()
});
export default Index
For the rest of the code you can refer to nextjs/examples/with-redux-saga, but now that I'm posting this answer they're using the older version on next-redux-wrapper ( version 6 ).
I'm trying pass the data from reducer to component and receive as props.
But the data return UNDEFÄ°NED, so I have tried console the data on reducer and action, but it's okey. There isn't any problem with the data coming from the API, but it always return to component undefined. Where is my fault?
Action
export default ProfileTab;
import axios from 'axios';
import { BASE, API_KEY } from '../config/env';
export const FETCHED_MOVIES = 'FETCHED_MOVIES';
export function fetchMovies() {
return (dispatch) => {
axios
.get(`${BASE}s=pokemon&apikey=${API_KEY}`)
.then((result) => result.data)
.then((data) =>
dispatch({
type: FETCHED_MOVIES,
payload: data.Search,
}),
);
};
}
Reducer
import { FETCHED_MOVIES } from '../actions/movies';
const initialState = {
fetching: false,
fetched: false,
movies: [],
error: {},
};
export default (state = initialState, action) => {
switch (action.type) {
case 'FETCHED_MOVIES':
return {
...state,
movies: action.payload,
};
default:
return state;
}
};
Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchMovies } from '../../actions/movies';
class Case extends Component {
static propTypes = {
movies: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
}
componentDidMount() {
this.props.fetchMovies();
}
onChangeHandler = (e) => {
this.setState({
input: e.target.value,
});
};
render() {
console.log(this.props.movies);
return (
<div>
<div className="movies-root">
<div className="movies-wrapper">
<div className="movies-container safe-area">
<h1>mert</h1>
</div>
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
movies: state.movies,
};
};
const mapDispatchToProps = {
fetchMovies,
};
export default connect(mapStateToProps, mapDispatchToProps)(Case);
Do this in the connect statement:
export default connect(mapStateToProps,{fetchMovies})(Case);
And remove the mapDispatchToProps function from your code.
Dispatching props as an object is quite incorrect. Try this, and it should work.
That's because your mapDispatchToProps function should return an object and take dispatch as parameter. Each field in your returned object should contain a function that dispatches your action.
So try something like this:
const mapDispatchToProps = dispatch => {
return {
fetchMovies: () => dispatch(fetchMovies())
}
}
Although there's already an accepted answer, I'm not sure how correct it is, as it's completely valid to pass mapDispatchToProps the way you did with the latest react (16.13.1) and react-redux (7.2.1) versions (I'm not sure about earlier versions).
Now, assuming your question contains the whole code, there are two important things missing:
Creating the store:
import { createStore } from "redux";
const store = createStore(reducer);
and passing it to the Provider component:
<Provider store={store}>
If you go ahead and do as above, you'll see that this.props.fetchMovies emits the following error:
Actions must be plain objects. Use custom middleware for async actions.
To fix it, do as it says and add a middleware, e.g. thunk:
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const store = createStore(rootReducer, applyMiddleware(thunk));
What follows is the full code. Note that I "split" fetchMovies into two functions: sync and async, for illustrating the difference usage between the two. I also modified your code (made is shorter, mostly) for this answer's readability. You can also see a live demo here:
File app.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchMoviesSync, fetchMoviesAsyncMock } from "./api";
class App extends Component {
componentDidMount() {
this.props.fetchMoviesSync();
this.props.fetchMoviesAsyncMock();
}
render() {
return (
<div>
<div className="movies-root">
<div className="movies-wrapper">
<div className="movies-container safe-area">
{this.props.movies.join("\n")}
</div>
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state) => ({ movies: state.movies });
const mapDispatchToProps = {
fetchMoviesSync,
fetchMoviesAsyncMock
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
File api.js
export const FETCHED_MOVIES = "FETCHED_MOVIES";
export const fetchMoviesSync = () => ({
type: FETCHED_MOVIES,
payload: ["movie1", "movie2", "movie3", "movie4"]
});
export const fetchMoviesAsyncMock = () => (dispatch) => {
dispatch({
type: FETCHED_MOVIES,
payload: ["movie5", "movie6", "movie7", "movie8"]
});
};
File reducer.js
const initialState = {
movies: [],
};
export default (state = initialState, action) => {
switch (action.type) {
case "FETCHED_MOVIES":
return {
...state,
movies: state.movies.concat(action.payload)
};
default:
return state;
}
};
File index.js
import React from "react";
import ReactDOM from "react-dom";
import Case from "./app";
import reducer from "./reducer";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
let store = createStore(reducer, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<Case />
</Provider>,
document.getElementById("container")
);
File index.html
<body>
<div id="container"></div>
</body>
I am working on a Blog project in redux where I am calling data from an api server and trying to display the default data(for now I am trying to retrieve the default data,I haven't implemented posting to the api server as of now) from the server.The data contains posts that are posted by users on the blog.The default data for the api server looks like this:
const defaultData = {
"8xf0y6ziyjabvozdd253nd": {
id: '8xf0y6ziyjabvozdd253nd',
timestamp: 1467166872634,
title: 'Udacity is the best place to learn React',
body: 'Everyone says so after all.',
author: 'thingtwo',
category: 'react',
voteScore: 6,
deleted: false,
commentCount: 2
},
"6ni6ok3ym7mf1p33lnez": {
id: '6ni6ok3ym7mf1p33lnez',
timestamp: 1468479767190,
title: 'Learn Redux in 10 minutes!',
body: 'Just kidding. It takes more than 10 minutes to learn technology.',
author: 'thingone',
category: 'redux',
voteScore: -5,
deleted: false,
commentCount: 0
}
}
So,what I am trying to do is, I want to filter out the posts based on a category.So,I have the homepage which lists all the categories available.So,when the user clicks on a category,he/she will be taken to a page where the posts for that category are shown.
The redux's "Action" file for filtering out the posts according to a particular category is given below:
import axios from 'axios';
export const FETCH_CATEGORIES = 'fetch_categories';
export const FETCH_PARTICULAR_CATEGORY_POSTS = 'fetch_particular_category';
let token;
if (!token)
token = localStorage.token = Math.random().toString(32).substr(-8);
const API = 'http://localhost:3001';
const headers = {
'Accept' : 'application/json',
'Authorization' : 'token'
}
//Action creaor for fetching all the categories available
export function fetchCategories() {
const URL = `${API}/categories`;
const request = axios.get(URL,{headers});
return dispatch => {
return request.then((data) => {
dispatch({
type : FETCH_CATEGORIES,
payload : data
})
})
}
}
//Action creator to fetch all the available posts for a particular category
export function fetchPostWithCateogry(category) {
const URL = `${API}/${category}/posts`;
const request = axios.get(URL,{headers});
return dispatch => {
return request.then((data) => {
console.log(data);
dispatch({
type: FETCH_PARTICULAR_CATEGORY_POSTS,
payload: data
})
})
}
}
The component file for displaying the posts of a particular category is:
import React, { Component } from 'react';
import { fetchPostWithCateogry } from '../actions/categories_action';
import { connect } from 'react-redux';
class CategoryView extends Component {
componentDidMount() {
const { category } = this.props.match.params;
this.props.fetchPostWithCateogry(category);
console.log(category);
}
render() {
const { category } = this.props;
console.log(category);
if (!category) {
return <div>Loading...</div>
}
return(
<div>
<h3>category.title</h3>
<h5>category.category</h5>
<h6>category.body</h6>
</div>
);
}
}
function mapStateToProps({ categories },ownProps) {
return { category: categories[ownProps.match.params.category]}
}
export default connect(mapStateToProps, {fetchPostWithCateogry})(CategoryView);
The reducer file for the same is:
import _ from 'lodash';
import { FETCH_CATEGORIES, FETCH_PARTICULAR_CATEGORY_POSTS } from '../actions/categories_action';
export default function(state={}, action) {
switch(action.type) {
case FETCH_CATEGORIES:
return {categories: {...state.categories, ...action.payload.data.categories}};
//return {categories: [...state.categories]};
case FETCH_PARTICULAR_CATEGORY_POSTS:
console.log(action.payload);
return {...state, [action.payload]: action.payload};
default:
return state;
}
}
I am using Route to navigate to the component depending on the endpoint the user entered.The index file for the same is given below:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { BrowserRouter, Route } from 'react-router-dom';
import thunk from 'redux-thunk';
import './index.css';
import App from './App';
import reducers from './reducers/index.js'
import Posts from './components/posts_index';
import CreatePost from './components/new_post';
import PostDetail from './components/post_detail';
import CategoryView from './components/category';
const createStoreWithMiddleware = createStore(reducers,applyMiddleware(thunk));
ReactDOM.render(
<Provider store={createStoreWithMiddleware}>
<BrowserRouter>
<div>
<Route path="/new" component={CreatePost} />
<Route path="/posts/:id" component={PostDetail} />
<Route exact path="/" component={Posts} />
<Route path="/:category/posts" component={CategoryView} />
</div>
</BrowserRouter>
</Provider> , document.getElementById('root'));
So,I am talking about the last route above.Now,the problem is, I do not get any errors,but the posts for a category are not being displayed.But if I try to console.log the request response returned from axios,I can see the required result,i.e.,I see the post with the category which is enter in my route url.A screenshot of the output of console.log that I get is attached below:
Can anyone please suggest where am I going wrong? Thanks in advance.
EDIT 1:
After trying to use redux devtools,I get the below error as shown in the screenshot:
EDIT 2
the output of promise:
NOTE I get the error only when I try to use redux devtools.Am I doing something wrong while using it.Below is my index file:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { BrowserRouter, Route } from 'react-router-dom';
import thunk from 'redux-thunk';
import './index.css';
import App from './App';
import reducers from './reducers/index.js'
import Posts from './components/posts_index';
import CreatePost from './components/new_post';
import PostDetail from './components/post_detail';
import CategoryView from './components/category';
const createStoreWithMiddleware = createStore(reducers,applyMiddleware(thunk),window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__());
ReactDOM.render(
<Provider store={createStoreWithMiddleware}>
<BrowserRouter>
<div>
<Route path="/new" component={CreatePost} />
<Route path="/posts/:id" component={PostDetail} />
<Route exact path="/" component={Posts} />
<Route path="/:category/posts" component={CategoryView} />
</div>
</BrowserRouter>
</Provider> , document.getElementById('root'));
Edit 3:
Actions in redux devtools:
Edit 4
redux devtools State screenshot
Edit : I edited all my post to make it clear.
Your reducer :
import _ from 'lodash';
import { FETCH_CATEGORIES, FETCH_PARTICULAR_CATEGORY_POSTS } from '../actions/categories_action';
export default function(state={}, action) {
switch(action.type) {
case FETCH_CATEGORIES:
return {categories: {...state.categories, ...action.payload.data.categories}};
case FETCH_PARTICULAR_CATEGORY_POSTS:
return {...state, [action.category]: action.payload.data};
default:
return state;
}
}
Your action :
export function fetchPostWithCateogry(category) {
const URL = `${API}/${category}/posts`;
const request = axios.get(URL,{headers});
return dispatch => {
return request.then((data) => {
dispatch({
type: FETCH_PARTICULAR_CATEGORY_POSTS,
payload: data,
category
})
})
}
}
Now we pass the category property to the action to name the index in which we'll store our array of posts.
You'll access this array of posts in your component but you need to iterate in this array to display his content :
import React, { Component } from 'react';
import { fetchPostWithCateogry } from '../actions/categories_action';
import { connect } from 'react-redux';
class CategoryView extends Component {
componentDidMount() {
const { category } = this.props.match.params;
this.props.fetchPostWithCateogry(category);
console.log(category);
}
render() {
const { category } = this.props;
console.log(category);
if (!category) {
return <div>Loading...</div>
}
return(
<div> {category.map(post => (<h3>{post.title}</h3>))}
</div>
);
}
}
function mapStateToProps({ categories },ownProps) {
return { category: categories[ownProps.match.params.category]}
}
export default connect(mapStateToProps, {fetchPostWithCateogry})(CategoryView);