Testing Redux Enzyme: Component doesn't re-render after state has changed - reactjs

I have a connected component (to Redux store) called DrawerAvatar, that I export for testing purpose (Enzyme + Jest) both the connected and non connected version.
Basically, I want to test that my DrawerAvatar render the user avatar when my Redux state isAuthenticated is true, and it renders a logo picture when isAuthenticated is false.
DrawerAvatar.js
export class DrawerAvatar extends React.Component {
render () {
const avatarSrc = this.props.isAuthenticated ?
'http://user-avatar.png'
) : (
'http://logo.png'
);
return (
<StyledAvatarContainer>
<StyledAvatar src={avatarSrc} />
</StyledAvatarContainer>
);
}
}
const mapStateToProps = state => ({
isAuthenticated: state.authReducer.isAuthenticated
});
export default compose(
connect(mapStateToProps, null)
)(DrawerAvatar);
And in my test, I'm using the non-connected DrawerAvatar, and connect it to my real Redux store via the Provider like this: (initiale state: isAuthenticated: false)
DrawerAvatar.test.js:
import React from 'react';
import { shallow } from 'enzyme';
import { Provider } from 'react-redux';
import store from '../../store';
import connectedDrawerAvatar, { DrawerAvatar } from './DrawerAvatar';
describe('Header > DrawerAvatar: component', () => {
it('should render logo for the DrawerAvatar if not authenticated, and the user avatar if authenticated', () => {
const wrapper = shallow(<Provider store={store}><DrawerAvatar /></Provider>);
console.log(wrapper.dive().debug());
// Output:
// <StyledAvatarContainer>
// <StyledAvatar src="https://logo.png" />
// </StyledAvatarContainer>
const StyledAvatarSrc = wrapper.dive().find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://logo.png'); // assertion passed
store.dispatch({ type: 'LOGIN_WITH_EMAIL_REQUESTED_TEST' });
// the state has been correctly updated, now isAuthenticated: true
console.log(wrapper.dive().debug());
// Output: same as above, whereas it should be:
// <StyledAvatarContainer>
// <StyledAvatar src="https://user-avatar.png" />
// </StyledAvatarContainer>
expect(StyledAvatarSrc).toBe('https://user-avatar.png'); // assertion failed
});
});
And here is my authReducer:
authReducer.js
const initialState = {
isAuthenticated: false
};
export default function authReducer (state = initialState, action) {
switch (action.type) {
case 'LOGIN_WITH_EMAIL_REQUESTED_TEST':
return {
...state,
isAuthenticated: true,
};
default:
return state;
}
}
So basically, I have a real action with type of LOGIN_WITH_EMAIL_REQUESTED that will call a bunch of Redux-saga with Axios, but for testing purpose, I just added to my real authReducer a LOGIN_WITH_EMAIL_REQUESTED_TEST case that will set the state isAuthenticated to true to avoid Axios calls etc...Not sure if it's a good way to do things though..lol
I've tried in vain to force the component to update with wrapper.update()...
I've also looked at redux-mock-store, but it seems like you cannot modify the state and only deals with actions and not the states.
I just start writing my first React test so...thank you !

Basically, I want to test that my DrawerAvatar render the user avatar when my Redux state isAuthenticated is true, and it renders a logo picture when isAuthenticated is false.
I would recommend not connecting the entire connected component and trying to bother with Redux at all. You can achieve your expected result with the following:
describe('Header > DrawerAvatar: component', () => {
it('should render logo for the DrawerAvatar if not authenticated, and the user avatar if authenticated', () => {
let wrapper = shallow(<DrawerAvatar isAuthenticated={false} />);
let StyledAvatarSrc = wrapper.find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://logo.png');
wrapper = shallow(<DrawerAvatar isAuthenticated={true} />);
StyledAvatarSrc = wrapper.find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://user-avatar.png');
});
});
Then, you can write separate tests for each of the pieces involved, for example your mapStateToProps function, which is really just a simple function that returns an object based on its input. And another simple test of your authReducer, etc

Related

TypeError: Cannot read properties of undefined - React Redux Toolkit Testing

In my React project I'm using redux toolkit and all is working fine on the UI side, but I'm running into the following error now I've come to testing my code (Note - I have chosen RTL as my library of choice, and I know I should test as I write code but I'm here now anyway).
TypeError: Cannot read property 'isSearching' of undefined
16 | const postState = useSelector(selectInitialPostsState);
> 17 | const isSearching = postState.isSearching;
As can be seen above, I import my initialState and save it to postState in my component, and then access isSearching from there - which works absolutely fine in my code and my App. I have accessed my state properties this way throughout my code as it seemed easier than having to write an individual selector for each state property used.
In my test file, I have resorted to Redux's test writing docs and have tried two different ways of rendering my store via manipulating RTL's render() method. The first function written locally in my test file, which for the time being I have commented out, and the second is imported into my test file from test-utils. Both methods give me the same errors.
Here is my test file:
import React from 'react';
// import { Provider } from 'react-redux';
// import { render as rtlRender, screen, fireEvent, cleanup } from '#testing-library/react';
// import { store } from '../../app/store';
import { render, fireEvent, screen } from '../../../utilities/test-utils';
import Header from '../Header';
// const render = component => rtlRender(
// <Provider store={store}>
// {component}
// </Provider>
// );
describe('Header Component', () => {
// let component;
// beforeEach(() => {
// component = render(<Header />)
// });
it('renders without crashing', () => {
render(<Header />);
// expect(screen.getByText('Reddit')).toBeInTheDocument();
});
});
And here is my test-utils file:
import React from 'react';
import { render as rtlRender } from '#testing-library/react';
import { configureStore } from '#reduxjs/toolkit';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
// Import your own reducer
import sideNavReducer from '../src/features/sideNavSlice';
import postReducer from '../src/features/postSlice';
function render(
ui,
{
preloadedState,
store = configureStore({ reducer: { sideNav: sideNavReducer, posts: postReducer }, preloadedState }),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return (
<Provider store={store}>
<BrowserRouter>{children}</BrowserRouter>
</Provider>
)
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
//override render method
export { render }
I have been able to prevent this error by creating a selector that saves the property directly, and accessing it in my component as isSearching without having to use postState.isSearching, but then the next error comes up about the next property I have to do this with, and then the next and so on.
It seems that in my test file, postState is undefined, whereas in my console, it holds the initialState from my slice file. Can anyone please advise why this is? I don't see a reason why I would have to write numerous selectors that directly access properties, rather than accessing them through an initialState selector. I can't seem to grasp how it works in my functioning code but not in my test code.
For further context if required, here is the mentioned initialState and selector from my slice file:
const postSlice = createSlice({
name: 'posts',
initialState: {
allPosts: [],
popularPosts: [],
sportPosts: [],
newsPosts: [],
savedPosts: [],
hiddenPosts: [],
reportedPosts: [],
dataLoading: true,
allPostsShown: true,
ellipsisClicked: false,
reportModal: false,
modalClosed: true,
imgClicked: false,
searchedPostsFound: false,
searchedPosts: [],
isSearching: false,
searchText: "",
},
});
export const selectInitialPostsState = state => state.posts;
And here is how it is used in my component. I have only included the relevant code.
const SideNav = () => {
const postState = useSelector(selectInitialPostsState);
const isSearching = postState.isSearching;
useEffect(() => {
!isSearching && setSearchText("");
if(!searchText){
dispatch(userNoSearch());
dispatch(searchedPostsFound({ ids: undefined, text: searchText })); // reset searchedPosts state array
}
}, [dispatch, searchText, isSearching]);
useEffect(() => {
if(isSearching){
dispatch(searchedPostsFound());
}
}, [dispatch, isSearching, search]);
}
If you want to load the initialState of your component, you need to send it as a parameter to rtlRender with your component.
it('renders without crashing', () => {
rtlRender(<Header />, { preloadedState: mockStateHere });
// expect(screen.getByText('Reddit')).toBeInTheDocument();
});
your mockStateHere structure needs to resemble what your redux store's state looks like as best as possible, using entities and id's etc
If you look at how you are building the render function in this example, you'll see the param preloadedState being deconstructed along with store which has the default configuration in this example:
function render(
ui,
{
preloadedState,
store = configureStore({ reducer: { sideNav: sideNavReducer, posts: postReducer }, preloadedState }),
...renderOptions
} = {}
...
)
that's the same preloadedState value that you send in with your ui component, which is being loaded into your store with the configureStore method and that store value will be sent into the Provider

Redux store states conditional initialization with localStorage during testing - React Testing Library

I am using React Testing Library to test some features of my website and I am facing issues with the initialization of the Redux store while running tests. The following is the initialState:
const initialState = {
isAuth: false,
loading: localStorage.getItem("authToken") ? true : false,
error: false,
user: {
username: "",
},
};
I have set up the tests with the following code to wrap everything around the Provider:
import { FC, ReactElement } from "react";
import { render, RenderOptions } from "#testing-library/react";
import { Provider } from "react-redux";
import { store } from "../store/store";
const AllTheProviders: FC = ({ children }) => {
return <Provider store={store}>{children}</Provider>;
};
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) => render(ui, { wrapper: AllTheProviders, ...options });
export * from "#testing-library/react";
export { customRender as render };
Then, in the actual test I use beforeAll(() => window.localStorage.setItem("authToken", "mockToken") to set the token in localStorage. Based on the value of the loading state the Login component should be rendered on my website, but I'm always getting false as the value of loading.
import { render, screen, waitFor } from "../utils/test-utils";
import App from "../App";
beforeAll(() => window.localStorage.setItem("authToken", "MockAuthToken"));
test("Login page not rendered if a valid auth token is present", async () => {
render(<App />);
//this is to check that the Login component is not rendered
await waitFor(() => expect(screen.queryByText("Sign in")).toBeNull());
await waitFor(() => expect(screen.getByRole("navigation")).toBeDefined(), {
timeout: 5000,
});
});
Is this happening because the Redux store is created before the setItem function execution during the tests? While in the browser the token is already there when I enter the website and so the intended behavior is not resembled by the test.
That's correct. We have an open issue asking us to add a "lazy initialization function" overload for initialState in createSlice:
https://github.com/reduxjs/redux-toolkit/issues/1024
I was actually working on trying to implement this last night and have a PR up, but there's some concerns about changes in the semantics of behavior:
https://github.com/reduxjs/redux-toolkit/pull/1662
I'll look at this some more tonight and see what we can come up with.

Jest with Enzyme, why is `toHaveBeenCalled` not triggering in my React Component test?

I'm working on a unit test in a React application that verifies a passed in prop function is being conditionally called based on another props value. I'm utilizing Typescript/Enzyme/Jest in my application and am using a Root wrapper around the component I'm testing to inject the Redux store (and override initial state if desired).
import { mount, ReactWrapper } from "enzyme";
import React from "react";
import Login from "src/components/auth/Login";
import Root from "src/Root";
let wrapped: ReactWrapper;
let defaultProps = {
signIn: jest.fn(),
token: null,
};
beforeEach(() => {
wrapped = mount(
<Root>
<Login {...defaultProps} />
</Root>
);
});
describe("on mount", () => {
describe("if no token is supplied to the props", () => {
it("will call the props.signIn() function", () => {
expect(defaultProps.signIn).toHaveBeenCalled();
});
});
});
When I run the test the toHaveBeenCalled() (as well as toBeCalled(), tried both) are not registering any calls. However, I have supplied a console.log statement that is getting triggered within the same conditional that the signIn() function lives.
import React from 'react';
import { AuthState, JWT } from "src/components/auth/types";
import { signIn } from "src/redux/auth";
interface Props {
signIn: () => Promise<void>;
token: null | JWT;
}
class Login extends React.Component<Props> {
/**
* Sign the user in on mount
*/
public componentDidMount(): void {
if (!this.props.token) {
console.log("GETTING HERE");
this.props.signIn();
}
}
public render(): JSX.Elemeent {
// ... More code
}
}
const mapStateToProps = (state: AuthState) => {
return {
token: state.auth.token;
};
};
export default connect(mapStateToProps, { signIn })(Login);
I've gone through several related posts/articles but all of the different configurations, such as traversing enzyme to get the direct prop or utilizing spyOn, have failed.
The only thing I can figure that's different is my wrapping of the Login component with Root, but considering I can see the console.log being triggered this seems like a complete shot in the dark.
Can anyone tell me what I'm doing wrong here?
You've to wait for component to mount, so:
it("will call the props.signIn() function", (done) => {
setImmediate(() => {
expect(defaultProps.signIn).toHaveBeenCalled();
done()
});
});
Ended up being me forgetting to place the override via mapDispatchToProps and mapStateToProps in the connect function. This was causing my passed in signIn function to be overridden by the signIn action imported in the file. Updating with ownProps and conditionally utilizing the passed in value fixes the issue:
const mapStateToProps = (state: AuthState, ownProps: Props) => {
return {
token: ownProps.token || state.auth.token;
};
};
const mapDispatchToProps = (dispatch: ThunkDispatch<{}, {}, any>, ownProps: Props) => {
return {
signIn: ownProps.signIn || (() => { return dispatch(signIn()) })
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);

react Login with useContext useReducer basic example

I'm using Typescript. I want to
Create a context
Use the context in routing
Update the context when logged in.
I'm following this tutorial, except Typescript cut my hopes short. See below
I have this in my App.tsx
import React from 'react';
import './App.css';
import Login from "./auth/login";
import Home from "./dash/home";
import Header from "./menu";
const initialState = {
isAuthenticated: false,
user: null,
token: null,
};
export const AuthContext = React.createContext(); // <---- This right here complains:
// Expected 1-2 arguments, but got 0.ts(2554)
// index.d.ts(349, 9): An argument for 'defaultValue' was not provided.
const reducer = (state: any, action: any) => {
switch (action.type) {
case "LOGIN":
return {
...state,
isAuthenticated: true,
user: action.payload.user,
token: action.payload.token
};
case "LOGOUT":
localStorage.clear();
return {
...state,
isAuthenticated: false,
user: null
};
default:
return state;
}
};
const App: React.FC = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<AuthContext.Provider
value={{
state,
dispatch
}}
>
<Header />
<div className="App">{!state.isAuthenticated ? <Login /> : <Home />}</div>
</AuthContext.Provider>
);
}
export default App;
In my login page, I have this:
import React from "react";
import { useForm } from 'react-hook-form';
import axios from 'axios';
import { AuthContext } from "../App";
export const Login = () => {
const { dispatch } = React.useContext(AuthContext) // <--- This the right way to do it?
// Where is the "dispatch" magically coming from?
const onSubmit = (data: any) => {
axios.post('http://localhost/api/v1/users/login/', data)
.then(res => {
console.log(res);
})
}
return (
<div className="login-container">
<!-- assume there's a form here -->
</div>
);
};
export default Login;
So
What do I put in for the "defaultValue"?
How do I update the context after login?
EDIT:
More context (pun intended) of what I wanna achieve.
The App.tsx has an <AuthContext.Provider ...> ... </AuthContext.Provider> If I understand correctly, this Context takes in the value of the state, dispatch and as per the {!state.isAuthenticated ... } part, dynamically alternates between <Login /> and <Home/> component.
This <AuthContext.Provider/> as per the initialState is set to isAuthenticated: false
When the user logs in at the Login Component, my question is, how do I, via the AuthContext, update the values in the App.tsx so as the routing will change?
If you take a look at the docs about React.createContext
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This can be helpful for testing components in isolation without wrapping them. Note: passing undefined as a Provider value does not cause consuming components to use defaultValue.
So you don't need to pass anything to the context, but you are getting the error because of typescript.
You need to create an interface for the context value.
You are passing the context as
value={{
state,
dispatch
}}
But you need to have an interface for that when create the context.
export const AuthContext = React.createContext(); // <---- missing interface
Here is an example on how to do it.
Where is the "dispatch" magically coming from?
This is how useContext works.
The value you get form useContext(MyContex) is the value passed by MyContext.Provider by value prop.
e.g.
<MyContext.Provider value={fooValue}>
{/* ... */}
</MyContext.Provider>
// same fooValue from the Provider
const fooValue = useState(MyContext)
Edit
After the async call inside onSubmit you must call dispatch and pass {type: 'LOGIN', payload: DATA_FROM_API} so it does in the reducer method and sets isAuthenticated to true
const onSubmit = (data: any) => {
axios.post('http://localhost/api/v1/users/login/', data)
.then(res => {
console.log(res.data) // <= if this is `payload`, than pass it to dispatch
dispatch({type: 'LOGIN', payload: res.data})
})
}
I had a similar problem, but after many trials and errors I was able to fix it.
I was following this tutorial:
Link: https://soshace.com/react-user-login-authentication-using-usecontext-and-usereducer/
Code: https://github.com/nero2009/login-auth-useContext
The problem about routing is also covered there. But it is in JS, so you have to make some modifications. Here is what I did:
First, I specified the action types, in your example it would be:
export type AuthenticationAction = { type: 'LOGIN', payload: IPayload } | {type: 'LOGOUT'}
I specified an interface for my payload, but it may work with "any" as well.
Then, I specified the default values (this is probably what you were looking for):
const AuthenticationStateContext = React.createContext(initialState)
const AuthenticationDispatchContext = React.createContext({} as Dispatch<AuthenticationAction>)
After that it should work.

How to properly ask for props on component

I have a component SinglePost that is called when another component Post is clicked in the main page, and also through path /post/:id. The thing is, in SinglePost, I call an API through Redux on componentDidMount, and then ask for it on componentWillReceiveProps. And when you access the component from the URL /post/:id, the props throws undefined two times after it loads.
I was following a React/Redux tutorial, to make a CRUD web app with API calls. And, in that project, the props loads fine in both cases (through main page navigation, and from URL). I did exactly the same thing and it doesn't work. Is this a problem, or does it work this way?
This is the SinglePost component:
import { connect } from 'react-redux';
import { mostrarPublicacion } from '../../actions/publicacionesActions';
state = {
publicacion: {},
}
componentDidMount() {
const {id} = this.props.match.params;
//this is the action defined on PublicacionesActions
this.props.mostrarPublicacion(id);
}
componentWillReceiveProps(nextProps, nextState) {
const {publicacion} = nextProps;
this.setState({
publicacion
})
}
const mapStateToProps = state => ({
publicacion: state.publicaciones.publicacion
})
export default connect(mapStateToProps, {mostrarPublicacion}) (SinglePost);
PublicacionesActions:
export const mostrarPublicacion = (id) => async dispatch => {
const respuesta = await axios.get(`http://www.someapi.com:8000/api/publicacion/${id}`);
dispatch({
type: MOSTRAR_PUBLICACION,
payload: respuesta.data
})
}
I debugged this and it actually returns the publication. In fact, it renders properly in the SinglePost component. But at first it loads undefined when accessing the component through the url.
RootReducer:
import { combineReducers } from 'redux';
import publicacionesReducer from './publicacionesReducer';
import seccionesReducer from './seccionesReducer';
export default combineReducers({
publicaciones: publicacionesReducer,
secciones: seccionesReducer
});
PublicacionesReducer:
export default function (state = initialState, action) {
switch(action.type) {
case MOSTRAR_PUBLICACION:
return {
...state,
publicacion: action.payload
}
default:
return state
}
}
This is the Router component (wrapped into <Provider store={store}>):
<Switch>
<Route exact path='/post/:id' component={SinglePost} />
</Switch>
I actually ask for (!this.state.publication) and return null, to render the component. It is working, but it is necesary?

Resources