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)
})
})
Related
I use lodash-debounce on input tag.
If I typed in five letters, api called 5 times.
const onChangeInput: ChangeEventHandler = (e: any) => {
setWords(e.target.value);
e.target.value.length >= 2 && debounceInputChanged();
};
const debounceInputChanged = useMemo(
() =>
debounce(() => {
searchWords();
}, 1000),
[words],
);
doesnt work lodash-debounce.
const onChangeInput: ChangeEventHandler = (e: any) => {
setInput(e.target.value);
debounceInput(e.target.value);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceInput = useMemo(
() =>
debounce((value: any) => {
console.log(value);
}, 3000),
[input],
);
console.log shows 1 12 123 1234
I expect console.log only shows 1234
Create seperate hook for debounce like this
import { useEffect, useState } from "react";
export default function useDebounce(value: string, delay: number = 1000) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
In the Component
const debouncedSearch = useDebounce(words); // usestate variable
useEffect(() => {
searchWords();
}, [debouncedSearch]);
const onChangeInput: ChangeEventHandler = (e: any) => {
setWords(e.target.value);
e.target.value.length >= 2 && debounceInputChanged();
};
const debounceInputChanged = useMemo(
() =>
debounce(() => {
searchWords();
}, 1000),
[words], // you are passing dependency over here so it's called when the word is updated.
);
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
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);
});
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)
})
})
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]);
}