Mocking snackbar using jest - reactjs

I try to test the following code
import { useSnackbar, VariantType, WithSnackbarProps } from 'notistack';
import React from 'react';
interface IProps {
setUseSnackbarRef: (showSnackbar: WithSnackbarProps) => void;
}
const InnerSnackbarUtilsConfigurator: React.FC<IProps> = (props: IProps) => {
props.setUseSnackbarRef(useSnackbar());
return null;
};
let useSnackbarRef: WithSnackbarProps;
const setUseSnackbarRef = (useSnackbarRefProp: WithSnackbarProps) => {
useSnackbarRef = useSnackbarRefProp;
};
export const SnackbarUtilsConfigurator = () =>
<InnerSnackbarUtilsConfigurator setUseSnackbarRef={setUseSnackbarRef} />;
export default {
success(msg: string) {
this.toast(msg, 'success');
},
warning(msg: string) {
this.toast(msg, 'warning');
},
info(msg: string) {
this.toast(msg, 'info');
},
error(msg: string) {
this.toast(msg, 'error');
},
toast(msg: string, variant: VariantType = 'default') {
useSnackbarRef.enqueueSnackbar(msg, { variant });
},
};
if I mock the complete notistack object
import React from "react";
import {render as testingRender} from '#testing-library/react';
import { SnackbarProvider} from 'notistack';
import SnackbarUtils,{SnackbarUtilsConfigurator} from './SnackbarUtils';
jest.mock('notistack');
beforeEach(() =>{
});
it('DownloadDialog renders correctly - open=true', async () => {
const component = await testingRender(<SnackbarProvider maxSnack={3}><SnackbarUtilsConfigurator /></SnackbarProvider>);
SnackbarUtils.success("success");
});
I get an error message:
TypeError: Cannot read property 'enqueueSnackbar' of undefined
33 | },
34 | toast(msg: string, variant: VariantType = 'default') {
> 35 | useSnackbarRef.enqueueSnackbar(msg, { variant });
| ^
36 | },
37 | };
because useSnackbarRef.
How can i mock only the useSnackbar method of notitstack, so that i can check if enqueueSnackbar is called with the correct values?

This works for me. Place this outside all tests (top level scope):
const mockEnqueue = jest.fn();
jest.mock('notistack', () => ({
...jest.requireActual('notistack'),
useSnackbar: () => {
return {
enqueueSnackbar: mockEnqueue
};
}
}));

Related

React Context value doesn't change

I just finished working on a custom Context Provider and I came across a problem. The context isn't getting updated with the data from the function inside of it. It just stays the same (as specified default value).
I'm not really sure what the bug is :/
Here's the code:
useBreakpoints.tsx
// Functions
import { createContext } from 'react';
// Hooks
import { useContext, useEffect, useState } from 'react';
// Types
import type { ReactNode } from 'react';
type Width = number | undefined;
interface Breakpoints {
[key: number | string]: number;
}
interface Values {
[key: number | string]: boolean;
}
interface IProps {
children: ReactNode;
breakpoints: Breakpoints;
}
// Context
export const BreakpointsContext = createContext({});
export const BreakpointsProvider = ({ children, breakpoints }: IProps) => {
const [width, setWidth] = useState<Width>(undefined);
const [values, setValues] = useState<Values | undefined>(undefined);
useEffect(() => {
if (typeof window !== 'undefined') {
const handleResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
handleResize();
return () =>
window.removeEventListener('resize', () => {
handleResize();
});
}
}, []);
useEffect(() => {
if (width !== undefined) {
const handleValues = () => {
Object.keys(breakpoints).forEach((breakpoint, index) => {
setValues((prev) => ({
...prev,
[breakpoint]: width >= Object.values(breakpoints)[index],
}));
});
};
handleValues();
return () => window.removeEventListener('resize', handleValues);
}
}, [width]);
const exposed = {
width,
values,
};
return <BreakpointsContext.Provider value={exposed}>{children}</BreakpointsContext.Provider>;
};
export const useBreakpoints = () => useContext(BreakpointsContext);
export default BreakpointsProvider;
_app.tsx
// Providers
import { ThemeProvider } from 'next-themes';
import BreakpointsProvider from '../hooks/useBreakpoints';
// Variables
import { Style } from '#master/css';
// Styles
import '#master/normal.css';
import '../styles/master.css';
// Types
import { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
interface AppPropsWithLayout extends AppProps {
Component: NextPageWithLayout;
}
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(
<ThemeProvider themes={Style.colorSchemes} attribute="class">
<BreakpointsProvider breakpoints={Style.breakpoints}>
<Component {...pageProps} />
</BreakpointsProvider>
</ThemeProvider>
);
}
export default MyApp;
Then I call it inside of a component like this
const test = useBreakpoints();
console.log(test);
I am working with NextJS, if that changes anything.

How do write unit test for function inside Context API?

Currently I am not able to test the lines as test coverage says. Please help me.
Test coverage is saying lines like dispatch but I don't know how to test it.
This is my file ListMeetingContext.tsx
import React, { createContext, ReactElement, useReducer } from 'react';
import { IMeeting } from '#interfaces';
import { REDUCER_ACTIONS } from '#enums/ReducerAction';
import { meetingData } from '#constants';
export const initListMeeting: IMeeting[] = meetingData;
type ReducerAction = {
type: REDUCER_ACTIONS;
payload: IMeeting | number;
};
export type MeetingsContext = {
children: ReactElement | ReactElement[];
};
export type ListMeetingContextType = {
listMeeting: IMeeting[];
handleAddMeeting: (newMeeting: IMeeting) => void;
handleUpdateMeeting: (newMeeting: IMeeting) => void;
handleRemoveMeeting: (id: number) => void;
};
export const reducer = (
state: IMeeting[],
action: ReducerAction,
): IMeeting[] => {
switch (action.type) {
case REDUCER_ACTIONS.ADD_MEETING:
// TODO: as IMeeting
return [...state, action.payload as IMeeting];
case REDUCER_ACTIONS.REMOVE_MEETING:
return state.filter((meeting) => meeting.id !== action.payload);
case REDUCER_ACTIONS.UPDATE_MEETING:
return state.map((meeting: IMeeting) =>
// TODO: as IMeeting
meeting.id === (action.payload as IMeeting).id
? (action.payload as IMeeting)
: meeting,
);
default:
return state;
}
};
export const ListMeetingContext = createContext<ListMeetingContextType>({
listMeeting: meetingData,
handleAddMeeting: () => null,
handleUpdateMeeting: () => null,
handleRemoveMeeting: () => null,
});
export const StateProvider = ({ children }: MeetingsContext): JSX.Element => {
const [listMeeting, dispatch] = useReducer(reducer, initListMeeting);
const handleAddMeeting = (newMeeting: IMeeting) => {
// Uncovered line
dispatch({ type: REDUCER_ACTIONS.ADD_MEETING, payload: newMeeting });
};
const handleUpdateMeeting = (newMeeting: IMeeting) => {
// Uncovered line
dispatch({ type: REDUCER_ACTIONS.UPDATE_MEETING, payload: newMeeting });
};
const handleRemoveMeeting = (id: number) => {
// Uncovered line
dispatch({ type: REDUCER_ACTIONS.REMOVE_MEETING, payload: id });
};
return (
<ListMeetingContext.Provider
value={{
listMeeting,
handleAddMeeting,
handleRemoveMeeting,
handleUpdateMeeting,
}}
>
{children}
</ListMeetingContext.Provider>
);
};
This is Image about test Coverage
The test for coverage lines tells me to put it at the bottom
This is file ListMeetingContext.test.tsx
import React from 'react';
import {
ListMeetingContext,
MeetingsContext,
reducer,
StateProvider,
} from '#context';
import { REDUCER_ACTIONS } from '#enums';
import { IMeeting } from '#interfaces';
import renderer from 'react-test-renderer';
import { meetingData } from '#constants';
import { fireEvent, render, screen } from '#testing-library/react';
describe('Test [ListMeetingContext] function', () => {
test('Action [ADD_MEETING] should update context state/add new item to context state with action[ADD_MEETING]', () => {
const state: IMeeting[] = [];
const action = {
type: REDUCER_ACTIONS.ADD_MEETING,
payload: meetingData[0],
};
const result = reducer(state, action);
expect(result).toEqual([action.payload]);
});
test('Action [REMOVE_MEETING] should update context state/remove an item from context state with action[REMOVE_MEETING]', () => {
const state: IMeeting[] = meetingData;
const action = {
type: REDUCER_ACTIONS.REMOVE_MEETING,
payload: 1,
};
const result = reducer(state, action);
expect(result).toEqual([meetingData[1]]);
});
test('Action [UPDATE_MEETING] should update context state/update an item from context state with action[UPDATE_MEETING]', () => {
const state: IMeeting[] = [meetingData[0]];
const action = {
type: REDUCER_ACTIONS.UPDATE_MEETING,
payload: meetingData[0],
};
const result = reducer(state, action);
expect(result).toEqual([action.payload]);
});
test('Action [EMPTY_ACTION] should return state', () => {
const state: IMeeting[] = [meetingData[0]];
const action = {
type: REDUCER_ACTIONS.EMPTY_ACTION,
payload: meetingData[0],
};
const result = reducer(state, action);
expect(result).toEqual(state);
});
test('[StateProvider] with default value', () => {
const component = renderer
.create(<StateProvider {...({} as MeetingsContext)} />)
.toJSON();
expect(component).toMatchSnapshot();
});
type Props = {
handleAddItem: () => void;
handleUpdateItem: () => void;
handleRemoveItem: () => void;
};
const TestComponent = ({
handleAddItem,
handleUpdateItem,
handleRemoveItem,
}: Props): JSX.Element => {
return (
<>
<button data-testid="remove-meeting-button" onClick={handleRemoveItem}>
Remove
</button>
<button data-testid="add-meeting-button" onClick={handleAddItem}>
Add
</button>
<button data-testid="update-meeting-button" onClick={handleUpdateItem}>
Update
</button>
</>
);
};
test('component handles button click', () => {
const handleAddMeeting = jest.fn();
const handleUpdateMeeting = jest.fn();
const handleRemoveMeeting = jest.fn();
const listMeeting = meetingData;
render(
<ListMeetingContext.Provider
value={{
listMeeting,
handleAddMeeting,
handleUpdateMeeting,
handleRemoveMeeting,
}}
>
<TestComponent
handleAddItem={handleAddMeeting}
handleUpdateItem={handleUpdateMeeting}
handleRemoveItem={handleRemoveMeeting}
/>
</ListMeetingContext.Provider>,
);
fireEvent.click(screen.getByTestId('remove-meeting-button'));
expect(handleRemoveMeeting).toHaveBeenCalledTimes(1);
});
});
Currently I am not able to test the lines as test coverage says. Please help me

Cannot read properties of undefined (the general state is undefined)

I just finished a small project and I tried to publish it on Vercel, the project works without errors locally, but on the server I get the error: "Cannot read properties of undefined (reading 'notes')", apparently not It recognizes the state that I have in my application, this would be my useNotes file:
import { useContext } from "react";
import { NotesContext } from "../context/NotesContext";
export const useNotes = () => {
const { notestate, toggleNote, addNote, changeState, handleDeleteNote } = useContext(NotesContext);
const { notes } = notestate;
return {
noteGeneral: notestate,
notes: notes,
notinteresting: notes?.filter(note => !note?.interesting).length,
interesting: notes?.filter(note => note?.interesting === true).length,
lisInteresting: notes?.filter(note => note?.interesting === true),
listNotInteresting: notes?.filter(note => note?.interesting === false),
toggleNote,
addNote,
changeState,
handleDeleteNote
}
}
this would be the context of my application:
import { createContext } from "react";
import { Note, NoteState } from "../interfaces/interfaces";
export type NoteContextProps = {
notestate: NoteState,
toggleNote: (id: number) => void;
addNote: (note: Note) => void;
changeState: (action: string) => void;
handleDeleteNote: (id: number) => void;
}
export const NotesContext = createContext<NoteContextProps>({} as NoteContextProps);
and this is the provider of my application
import { useEffect, useReducer } from 'react';
import { Note, NoteState } from '../interfaces/interfaces';
import { NoteReducer } from './NoteReducer';
import { NotesContext } from './NotesContext';
import { DateNote } from '../../helpers/DateNote';
const { today } = DateNote();
const INITIAL_STATE: NoteState = {
notesCount: 2,
notes: [
{
id: 1,
description: 'Welcome to NotesApp, here you can write all the notes you want. If you liked the project leave a comment on my social networks :)',
interesting: true,
title: 'Hello there',
created: today
},
],
active: 'All',
}
interface props {
children: JSX.Element | JSX.Element[]
}
const NotesProvider = ({ children }: props) => {
const localData = localStorage.getItem('notes');
const toggleNote = (id: number) => {
dispatch({ type: 'toggleInteresting', payload: { id } })
}
const addNote = (note: Note) => {
dispatch({ type: 'addNote', payload: note })
}
const changeState = (active: string) => {
dispatch({ type: 'changeState', payload: active })
}
const handleDeleteNote = (id: number) => {
dispatch({ type: 'deleteNote', payload: { id } })
}
const [notestate, dispatch] = useReducer(NoteReducer, INITIAL_STATE, () => {
return localData ? JSON.parse(localData) : localStorage.setItem('notes', JSON.stringify(INITIAL_STATE))
});
useEffect(() => {
localStorage.setItem('notes', JSON.stringify(notestate))
}, [notestate])
return (
<NotesContext.Provider value={{ toggleNote, addNote, changeState, handleDeleteNote, notestate }}>
{children}
</NotesContext.Provider>
)
}
export default NotesProvider;
I am storing the state of my application in Localstorage.
I've been looking for the solution for more than two days but I can't find it
const [notestate, dispatch] = useReducer(NoteReducer, INITIAL_STATE, () => {
return localData ? JSON.parse(localData) : localStorage.setItem('notes', JSON.stringify(INITIAL_STATE))
});
I think here when there is no local data, the notestate receives value from localStorage.setItem(), which is undefined, so the notestate will be undefined.

How to test React.js page that uses Context and useEffect?

I'm having trouble testing a page that has Context and useEffect using Jest and Testing-library, can you help me?
REPO: https://github.com/jefferson1104/padawan
My Context: src/context/personContext.tsx
import { createContext, ReactNode, useState } from 'react'
import { useRouter } from 'next/router'
import { api } from '../services/api'
type PersonData = {
name?: string
avatar?: string
}
type PersonProviderProps = {
children: ReactNode
}
type PersonContextData = {
person: PersonData
loading: boolean
handlePerson: () => void
}
export const PersonContext = createContext({} as PersonContextData)
export function PersonProvider({ children }: PersonProviderProps) {
const [person, setPerson] = useState<PersonData>({})
const [loading, setLoading] = useState(false)
const router = useRouter()
function checkAvatar(name: string): string {
return name === 'Darth Vader'
? '/img/darth-vader.png'
: '/img/luke-skywalker.png'
}
async function handlePerson() {
setLoading(true)
const promise1 = api.get('/1')
const promise2 = api.get('/4')
Promise.race([promise1, promise2]).then(function (values) {
const data = {
name: values.data.name,
avatar: checkAvatar(values.data.name)
}
setPerson(data)
setLoading(false)
router.push('/battlefield')
})
}
return (
<PersonContext.Provider value={{ person, handlePerson, loading }}>
{children}
</PersonContext.Provider>
)
}
My Page: src/pages/battlefield.tsx
import { useContext, useEffect } from 'react'
import { useRouter } from 'next/router'
import { PersonContext } from '../context/personContext'
import Person from '../components/Person'
const Battlefield = () => {
const { person } = useContext(PersonContext)
const router = useRouter()
useEffect(() => {
if (!person.name) {
router.push('/')
}
})
return <Person />
}
export default Battlefield
My Test: src/tests/pages/Battlefield.spec.tsx
import { render, screen } from '#testing-library/react'
import { PersonContext } from '../../context/personContext'
import Battlefield from '../../pages'
jest.mock('../../components/Person', () => {
return {
__esModule: true,
default: function mock() {
return <div data-test-id="person" />
}
}
})
describe('Battlefield page', () => {
it('renders correctly', () => {
const mockPerson = { name: 'Darth Vader', avatar: 'darth-vader.png' }
const mockHandlePerson = jest.fn()
const mockLoading = false
render(
<PersonContext.Provider
value={{
person: mockPerson,
handlePerson: mockHandlePerson,
loading: mockLoading
}}
>
<Battlefield />
</PersonContext.Provider>
)
expect(screen.getByTestId('person')).toBeInTheDocument()
})
})
PRINSCREEN ERROR
enter image description here
I found a solution:
The error was happening because the path where I call the Battlefield page didn't have the absolute path.

React create hooks with next.js

I'm trying to add toast messages to my site using createContextand useContext from react to create a hook but when I use it I got an exception TypeError: addToast is not a function
import React, {
createContext, useContext, useCallback, useState,
} from 'react';
import { v4 as uuid } from 'uuid';
import Toast from '../components/Toast';
export interface ToastMessage {
id: string;
type?: 'success' | 'error' | 'default';
title: string;
description?: string;
}
interface ToastContextData {
addToast(message: Omit<ToastMessage, 'id'>): void;
removeToast(id: string): void;
}
const ToastContext = createContext<ToastContextData>({} as ToastContextData);
export const ToastProvider: React.FunctionComponent = ({ children }) => {
const [messages, setMessages] = useState<ToastMessage[]>([]);
const addToast = useCallback(
({ type, title, description }: Omit<ToastMessage, 'id'>) => {
const id = uuid();
const toast = {
id,
type,
title,
description,
};
setMessages((state) => [...state, toast]);
}, [],
);
const removeToast = useCallback((id: string) => {
setMessages((state) => state.filter((message) => message.id !== id));
}, []);
return (
<ToastContext.Provider value={{ addToast, removeToast }}>
{children}
<Toast messages={messages} />
</ToastContext.Provider>
);
};
export function useToast(): ToastContextData {
const context = useContext(ToastContext);
if (!context) {
throw new Error('use Toast must be used within a ToastProvider');
}
return context;
}
Using like this:
import { useToast } from '../../hooks/toast';
const { addToast } = useToast();
addToast({
type: 'error',
title: 'Problemo!',
description: 'That password and login doesn`t match. Try again?',
});
Obs: that code works perfectly in pure React (createReactApp) and on this project I'm using create next app

Resources