How to use enzyme and jest to detect change React state - javascript

I have a simple React component, when user click the button I want to increase the internal value of state and render in an input button.
The component works, but I am not able to write a test with enzyme, basically the internal value is not being updated.
I think it is connected with setState being asynch, do you have any idea how to fix my test?
import * as React from 'react'
type TestCounterProps = Readonly<{
defaultValue: number
onClick: (value: number) => void
}>
export const TestCounter = ({ defaultValue, onClick }: TestCounterProps) => {
const [value, setValue] = React.useState(defaultValue)
const handleIncrease = () => {
setValue(value + 1)
onClick(value)
}
return (
<div>
<input value={value} readOnly />
<button onClick={handleIncrease}>Click to increase</button>
</div>
)
}
Test:
import * as React from 'react'
import { mount } from 'enzyme'
import { TestCounter } from './TestCounter'
describe('TestCounter', () => {
it('should increase counter by 1 when user click button', () => {
const cbClick = jest.fn()
const container = mount(<TestCounter defaultValue={0} onClick={cbClick} />)
const input = container.find('input')
const button = container.find('button')
button.simulate('click')
container.update()
expect(input.props().value).toBe(1) // issue here still 0 <<<
expect(cbClick).toBeCalledWith(1)
})
})

I have a similar example/component, I am going to past it here so could be useful as example:
import * as React from 'react'
type CounterProps = Readonly<{
initialCount: number
onClick: (count: number) => void
}>
export default function Counter({ initialCount, onClick }: CounterProps) {
const [count, setCount] = React.useState(initialCount)
const handleIncrement = () => {
setCount((prevState) => {
const newCount = prevState + 1
onClick(newCount)
return newCount
})
}
const handleIncrementWithDelay = () => {
setTimeout(() => {
setCount((prevState) => {
const newCount = prevState + 1
onClick(newCount)
return newCount
})
}, 2000)
}
return (
<div>
Current value: {count}
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleIncrementWithDelay}>Increment with delay</button>
</div>
)
}
The test:
import * as React from 'react'
import { mount, ReactWrapper } from 'enzyme'
import Counter from './Counter'
import { act } from 'react-dom/test-utils'
const COUNT_UPDATE_DELAY_MS = 2000
const waitForComponentToPaint = async (wrapper: ReactWrapper) => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
wrapper.update()
})
}
describe('Counter', () => {
beforeAll(() => {
jest.useFakeTimers()
})
afterAll(() => {
jest.useRealTimers()
})
it('should display initial count', () => {
const cbClick = jest.fn()
const wrapper = mount(<Counter initialCount={5} onClick={cbClick} />)
expect(wrapper.text()).toContain('Current value: 5')
expect(cbClick).not.toBeCalled()
})
it('should increment after "Increment" button is clicked', () => {
const cbClick = jest.fn()
const wrapper = mount(<Counter initialCount={5} onClick={cbClick} />)
wrapper.find('button').at(0).simulate('click')
expect(wrapper.text()).toContain('Current value: 6')
expect(cbClick).toHaveBeenCalledWith(6)
})
it('should increment with delay after "Increment with delay" button is clicked', () => {
const cbClick = jest.fn()
const wrapper = mount(<Counter initialCount={5} onClick={cbClick} />)
waitForComponentToPaint(wrapper)
wrapper.find('button').at(1).simulate('click')
jest.advanceTimersByTime(COUNT_UPDATE_DELAY_MS + 1000)
expect(wrapper.text()).toContain('Current value: 6')
expect(cbClick).toHaveBeenCalledWith(6)
})
})

Related

Listen to changes in a value inside an object in React context

Given the following React Context Provider. A simple Counter class with 2 methods, stored in the React context.
import { createContext, useContext } from "react";
class Counter {
public count: number = 0;
getCount = () => {
return this.count;
};
incrementCount = () => {
this.count = this.count + 1;
};
}
type CounterContextType = {
counter: Counter;
};
const defaults: CounterContextType = {
counter: new Counter()
};
const CounterContext = createContext<CounterContextType>(defaults);
export const CounterProvider: React.FC = ({ children }) => {
const counter = new Counter();
return (
<CounterContext.Provider
value={{
counter
}}
>
{children}
</CounterContext.Provider>
);
};
export const useCounter = () => {
return useContext(CounterContext);
};
I want to listen to changes in the count property of the Counter instance.
Here is what I have tried:
import { useMemo } from "react";
import { CounterProvider, useCounter } from "./CounterProvider";
const DisplayWithMethod = () => {
const { counter } = useCounter();
return <div>Method: {counter.getCount()}</div>;
};
const DisplayWithProperty = () => {
const { counter } = useCounter();
return <div>Prop: {counter.count}</div>;
};
const DisplayWithMemo = () => {
const { counter } = useCounter();
const val = useMemo(() => counter.count, [counter.count]);
return <div>Memo: {val}</div>;
};
const Button = () => {
const { counter } = useCounter();
return <button onClick={counter.incrementCount}>Increment</button>;
};
export default function App() {
return (
<CounterProvider>
<DisplayWithMethod />
<DisplayWithProperty />
<DisplayWithMemo />
<Button />
</CounterProvider>
);
}
None of these work since the counter instance never changes, so no re renders are triggered. Any idea(s) on how to make this work while keeping a class structure for Counter.
https://codesandbox.io/s/nostalgic-fast-cyflg
The issue is with React is not getting that the count has changed and does not rerender. You can get rid of the issue using useState hook.
You should change type definitions like below and construct counter instances using outputs of useState hook.
import { createContext, useContext, useState } from "react";
type CounterContextType = {
counter: {
count: number;
getCount: () => number;
incrementCount: () => void;
};
};
const defaults: CounterContextType = {
counter: {
count: 0,
getCount: () => 0,
incrementCount: () => undefined
}
};
const CounterContext = createContext<CounterContextType>(defaults);
export const CounterProvider: React.FC = ({ children }) => {
const [count, setCount] = useState<number>(0);
return (
<CounterContext.Provider
value={{
counter: {
count,
getCount: () => count,
incrementCount: () => {
setCount((prevCount) => prevCount + 1);
}
}
}}
>
{children}
</CounterContext.Provider>
);
};
export const useCounter = () => {
return useContext(CounterContext);
};
Code Sandbox

Testing react component in jest: ReferenceError: act is not defined

I have a component and a test which fails. I get the error ReferenceError: act is not defined. Any idea for a fix? I've been trying to read around and fix this myself but I havent yet managed to.
export const useCounter = (initialValue = 0) => {
const [counter, setCounter] = useState(initialValue)
const add = useCallback((delta = 1) => setCounter(counter => counter + delta), [setCounter])
const substract = useCallback((delta = 1) => setCounter(counter => counter - delta), [setCounter])
return (
<>
Count: {counter}
{add, substract}
</>
)
}
import { renderHook } from '#testing-library/react-hooks'
import { useCounter } from '#/hooks/useCounter'
describe('Testing useCounter hook', () => {
it('should add +1 to counter', () => {
const { result } = renderHook(useCounter)
act(() => result.current.add())
expect(result.current.counter).toBe(1)
})
it('should subtract -1 to counter', () => {
const { result } = renderHook(useCounter)
act(() => result.current.subtract())
expect(result.current.counter).toBe(-1)
})
})

Why is my component not re-rendering when passed in as a child?

I am testing a bit of code to count rerenders.
This one does not work as I am passing <MyComponent> as a child.
it("should get the same object when the parent rerenders", async () => {
jest.useFakeTimers();
const callback = jest.fn();
let renderCount = 0;
let x = 0;
function MyComponent() {
const random = Math.random();
const myRef = useRef({ random })
if (x === 0) {
x = myRef.current.random
}
++renderCount;
callback();
return (<div data-testid="test">{JSON.stringify(myRef.current)}</div>);
}
function MyStateComponent({ children }: PropsWithChildren<{}>) {
const forceUpdate = useReducer(() => ({}), {})[1] as () => void
useEffect(() => {
(async function asyncEffect() {
await delay(10000);
forceUpdate()
})()
}, [])
return (<>{children}</>);
}
const { getByTestId } = render(<MyStateComponent><MyComponent /></MyStateComponent>)
expect(getByTestId("test").textContent).toEqual(JSON.stringify({ random: x }));
expect(renderCount).toEqual(1);
expect(callback).toBeCalledTimes(1);
jest.runAllTimers();
await waitFor(() => {
expect(callback).toBeCalledTimes(2);
expect(getByTestId("test").textContent).toEqual(JSON.stringify({ random: x }));
expect(renderCount).toEqual(2);
});
})
However, this works but I embed <MyComponent /> into the component.
it("should get the same object when the parent rerenders with children", async () => {
jest.useFakeTimers();
const callback = jest.fn();
let renderCount = 0;
let x = 0;
function MyComponent() {
const random = Math.random();
const myRef = useRef({ random })
if (x === 0) {
x = myRef.current.random
}
++renderCount;
callback();
return (<div data-testid="test">{JSON.stringify(myRef.current)}</div>);
}
function MyStateComponent({ children }: PropsWithChildren<{}>) {
const forceUpdate = useReducer(() => ({}), {})[1] as () => void
useEffect(() => {
(async function asyncEffect() {
await delay(10000);
forceUpdate()
})()
}, [])
return (<MyComponent />);
}
const { getByTestId } = render(<MyStateComponent />)
expect(getByTestId("test").textContent).toEqual(JSON.stringify({ random: x }));
expect(renderCount).toEqual(1);
expect(callback).toBeCalledTimes(1);
jest.runAllTimers();
await waitFor(() => {
expect(callback).toBeCalledTimes(2);
expect(getByTestId("test").textContent).toEqual(JSON.stringify({ random: x }));
expect(renderCount).toEqual(2);
});
})
Since MyComponent is determined to be a Pure functional component and it has no state to speak of, I presume React memoizes it automatically. To get around that and force a re-render the component needs part of itself to change, e.g. a context.
import { createContext, PropsWithChildren, useContext, useEffect, useReducer } from "react";
import { delay } from "./delay";
type IRendering = {}
const RenderingContext = createContext<IRendering>({})
/**
* This is a component that rerenders after a short delay
*/
export function RerenderingProvider({ children }: PropsWithChildren<{}>): JSX.Element {
const forceUpdate = useReducer(() => ({}), {})[1] as () => void
useEffect(() => {
(async function asyncEffect() {
await delay(10000);
forceUpdate();
})()
}, [])
return (<RenderingContext.Provider value={{}}>{children}</RenderingContext.Provider>);
}
export function useRerendering(): IRendering {
return useContext(RenderingContext);
}
With the following test...
it("should get the same object when the parent rerenders using component, but the component will rerender as context has changed", async () => {
jest.useFakeTimers();
const callback = jest.fn();
let x = 0;
function MyComponent() {
const _ignored = useRerendering();
const random = Math.random();
const myRef = useRef({ random })
if (x === 0) {
x = myRef.current.random
}
callback();
return (<>
<div data-testid="test">{JSON.stringify(myRef.current)}</div>
<div data-testid="random">{JSON.stringify(random)}</div>
</>);
}
const { getByTestId } = render(<RerenderingProvider><MyComponent /></RerenderingProvider>)
expect(getByTestId("test").textContent).toEqual(JSON.stringify({ random: x }));
expect(callback).toBeCalledTimes(1);
jest.runAllTimers();
await waitFor(() => {
expect(getByTestId("test").textContent).toEqual(JSON.stringify({ random: x }));
expect(callback).toBeCalledTimes(2);
});
})
I put up my scenario here... https://github.com/trajano/react-hooks-tests

Not able to mock a function inside useEffect

I have a custom hook as below
export const useUserSearch = () => {
const [options, setOptions] = useState([]);
const [searchString, setSearchString] = useState("");
const [userSearch] = useUserSearchMutation();
useEffect(() => {
if (searchString.trim().length > 3) {
const searchParams = {
orgId: "1",
userId: "1",
searchQuery: searchString.trim(),
};
userSearch(searchParams)
.then((data) => {
setOptions(data);
})
.catch((err) => {
setOptions([]);
console.log("error", err);
});
}
}, [searchString, userSearch]);
return {
options,
setSearchString,
};
};
and I want to test this hook but am not able to mock userSearch function which is being called inside useEffect.
can anybody help?
this is my test
it('should set state and test function', async () => {
const wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
)
const { result } = renderHook(
() => useUserSearch(),
{ wrapper }
)
await act(async () => {
result.current.setSearchString('abc5')
})
expect(result.current.options).toEqual(expected)
})
useUserSearchMutation
import {createApi, fetchBaseQuery} from '#reduxjs/toolkit/query/react';
export const userSearchAPI = createApi({
reducerPath: 'userSearchResult',
baseQuery: fetchBaseQuery({baseUrl: process.env.REACT_APP_BASE_URL}),
tagTypes: ['Users'],
endpoints: build => ({
userSearch: build.mutation({
query: body => ({url: '/org/patient/search', method: 'POST', body}),
invalidatesTags: ['Users'],
}),
}),
});
export const {useUserSearchMutation} = userSearchAPI;
Because it's a named export you should return an object in the mock
it("should set state and test function", async () => {
jest.mock("./useUserSearchMutation", () => ({
useUserSearchMutation: () => [jest.fn().mockResolvedValue(expected)],
}));
const wrapper = ({ children }) => (
...
});
I have created a smaller example based on your code, where I am mocking a hook inside another hook.
hooks/useUserSearch.js
import { useEffect, useState } from "react";
import useUserSearchMutation from "./useUserSearchMutation.js";
const useUserSearch = () => {
const [text, setText] = useState();
const userSearch = useUserSearchMutation();
useEffect(() => {
const newText = userSearch();
setText(newText);
}, [userSearch]);
return text;
};
export default useUserSearch;
hooks/useUSerSearchMutation.js
I had to move this to its own file to be able to mock it when it was called
inside of the other hook.
const useUserSearchMutation = () => {
return () => "Im not mocked";
};
export default useUserSearchMutation;
App.test.js
import { render } from "react-dom";
import useUserSearch from "./hooks/useUserSearch";
import * as useUserSearchMutation from "./hooks/useUserSearchMutation";
import { act } from "react-dom/test-utils";
let container;
beforeEach(() => {
// set up a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
document.body.removeChild(container);
container = null;
});
function TestComponent() {
const text = useUserSearch();
return <div>{text}</div>;
}
test("should mock userSearch", async () => {
const mockValue = "Im being mocked";
jest
.spyOn(useUserSearchMutation, "default")
.mockImplementation(() => () => mockValue);
act(() => {
render(<TestComponent />, container);
});
expect(container.textContent).toBe(mockValue);
});

How to use useeffect hook in react?

i want to return a function that uses useEffect from the usehook and i am getting error "useeffect is called in a function which is neither a react function component or custom hook.
what i am trying to do?
i have addbutton component and when user clicks add button i want to call the function requestDialog.
below is my code within addbutton file
function AddButton () {
const count = useGetCount();
const requestDialog = useRequestDialog(); //using usehook here
const on_add_click = () => {
requestDialog(count); //calling requestDialog here
}
return (
<button onClick={on_add_click}>add</button>
);
}
interface ContextProps {
trigger: (count: number) => void;
}
const popupContext = React.createContext<ContextProps>({
trigger: (availableSiteShares: number) => {},
});
const usePopupContext = () => React.useContext(popupContext);
export const popupContextProvider = ({ children }: any) => {
const [show, setShow] = React.useState(false);
const limit = 0;
const dismiss = () => {
if (show) {
sessionStorage.setItem(somePopupId, 'dismissed');
setShow(false);
}
};
const isDismissed = (dialogId: string) =>
sessionStorage.getItem(dialogId) === 'dismissed';
const context = {
trigger: (count: number) => {
if (!isDismissed(somePopupId) && count <= limit) {
setShow(true);
} else if (count > limit) {
setShow(false);
}
},
};
return (
<popupContext.Provider value={context}>
{children}
{show && (
<Popup onHide={dismiss} />
)}
</popupContext.Provider>
);
};
export function useRequestDialog(enabled: boolean,count: number) {
return function requestDialog() { //here is the error
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}
}, [count, trigger]);
}
How to solve the error ""useEffect is called in a function which is neither a react function component or custom hook."
i am not knowing how to use useeffect and the same time use it in the addbutton component.
could someone help me with this. thanks
useEffect method is like, useEffect(() => {}, []), But your usage in requestDialog is wrong. Try changing with following.
function requestDialog() {
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}, [count, trigger]);
}

Categories

Resources