Home > Back-end >  how to setup properly environment for testing using React, Redux and typescript?
how to setup properly environment for testing using React, Redux and typescript?

Time:07-06

I have a problem while setting up the environment for testing a React app that uses Redux and Typescript

Here is my test:

import React from 'react';
import { render, screen } from '@testing-library/react'
import Login from './Login'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import { MemoryRouter } from 'react-router-dom';
import {createMemoryHistory} from 'history'

describe('Rendering', ()=>{

    const initialState = {
        user: {
            id:'',
            name: "",
            email: ''           
        },  
        loading: false,
        error:{
            message: ''
        }
    }
    const mockStore = configureStore()
    let store

    test('renders initial screen', () => {  

        const history = createMemoryHistory()
        store = mockStore(initialState)

        render(
            <Provider store={store}>
                <MemoryRouter history={history}>
                    <Login />
                </MemoryRouter>
            </Provider>
        )

        const loginScreenMessage = screen.getByRole('heading', { name:/login/i } );
        expect(loginScreenMessage).toBeInTheDocument();
        
    })

})

This is the Login component:

import React, {useState, useEffect} from 'react'
import { useAppDispatch, useAppSelector } from 'store/hooks';
import {useNavigate} from 'react-router-dom';
import {loginUserAction} from 'store/actions/AuthAction'
import ValidationErrors from 'components/ValidationErrors'

function Login() {
    
    const dispatch = useAppDispatch()
    const navigate = useNavigate()
    const loading =  useAppSelector(state =>state.auth.loading)   
    const [form , setForm] = useState({
        email : "",
        password : ""        
    })

    const error = useAppSelector(state =>state.auth.error)    
    
    useEffect(()=>{     
        dispatch(ErrorAction({message:''})                  
    }, [])

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setForm({
            ...form,
            [e.target.name] : e.target.value
        })
    }   
    
    const submitLogin = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()      
        try{
            await dispatch(loginUserAction(form))
            navigate('/home')
        }catch{
            //
        }
                
    }                       

    return (        
        <div className="login" data-test="component-login">                     
            <div className="w-full mx-auto sm:w-8/12 md:w-6/12 lg:w-4/12 bg-white pt-5 px-10">
            <AiOutlineTwitter className="text-5xl text-twitter" />             
            <h1 className="text-4xl font-bold my-10 ">Login</h1>
            { error.message &&                                             
                <ValidationErrors errors={error} />                    
            }   
            <form onSubmit={submitLogin}>                  
                <div className="special-label-holder relative mt-5">
                    <input 
                        type="text" 
                        id="email"
                        name="email"
                        placeholder=" "                         
                        className="input-form" 
                        value={form.email}
                        onChange={handleChange}
                    />
                    <label 
                        className="text-lg h-full left-2 text-gray-500 top-3 overflow-hidden pointer-events-none absolute transition-all duration-200 ease-in-out" 
                        htmlFor="email"
                    >
                        Email, phone or username
                    </label>
                </div>                      
                <div className="special-label-holder relative mt-5">    
                    <input 
                        id="password"
                        type="password" 
                        name="password"
                        placeholder=" "                                                     
                        className="input-form" 
                        value={form.password}
                        onChange={handleChange}
                    />
                    <label 
                        className="text-lg h-full left-2 text-gray-500 top-3 overflow-hidden pointer-events-none absolute transition-all duration-200 ease-in-out" 
                        htmlFor="password"
                    >
                        Password
                    </label>
                </div>               
                <button disabled={loading} type="submit" className="button flex items-center justify-center w-full mt-5 disabled:opacity-50">
                    <div className={`loading animate-spin mr-1 ${!loading ? 'hidden' : '' } `} /></div>
                    Login
                </button>                              
                </form>   
            </div>              
        </div>      
    );
}

export default Login;

store/hooks.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

store/store.ts

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import reducer from './reducers'

export const store = configureStore({
    reducer
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

store/reducers/index.ts

import {combineReducers} from 'redux'
import {authReducer} from './authReducer'
import {errorReducer} from './errorReducer'

const rootReducer = combineReducers({
    auth: authReducer,
    error: errorReducer
})

export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;

store/reducers/authReducer.ts

import { Auth } from "interfaces/Auth";
import {Action, ActionType} from 'store/types'

const initialState: Auth = {
    user: {
        id:'',
        name: "",
        email: ''   
    },  
    loading: false,
    error:{
        message: ''     
    }
};

export const authReducer = (state = initialState, action: Action) : Auth => {
    switch (action.type) {
        case ActionType.LOGIN_USER_START:
        case ActionType.CHECK_USER_START:
            return {
                ...state,
                loading: true,
            };
        case ActionType.SET_USER:
            return {
                ...state,
                loading: false,             
                user: action.payload,
            };
        case ActionType.SET_USER_ERROR:
            return {
                ...state,
                loading: false,             
                error: action.payload,
            };  
        case ActionType.PURGE_AUTH:
            return state;
            
        default:
            return state;
    }
}

And this is the error message:

TypeError: Cannot read property 'loading' of undefined

  13 |     const navigate = useNavigate()
> 14 |     const loading =  useAppSelector(state =>state.auth.loading)

What is happening? thanks.

Edit

Now I modified this store/reducers/authReducer.ts

export const initialState: Auth = {

... Now I am exporting it, to get it in my test:

...
import {initialState} from 'store/reducers/authReducer'

describe('Rendering', ()=>{

    ////I added auth
    const initialStateLogin = {
        auth: initialState           
    }
...

Now my test is passing, thanks

CodePudding user response:

The initialState in your test has no nested auth state that your component seems to be expecting. Adapt the initialState in your test to reflect what the app uses. Better yet, use the same initialState your app is using.

  • Related