How to test a React Component with a Custom Hook - reactjs

Given a simple custom hook (which I know how to test):
const useCounter = () => {
const [value, setValue] = useState(0)
const increase = () => {
setValue(value + 1)
}
return {
value,
increase
}
}
And a component that use it:
const App = (props) => {
const { value, increase } = useCounter()
return (
<div>
<div>{value}</div>
<button onClick={increase}>
plus
</button>
</div>
)
}
How do I test the component?
What is the "right way" of doing it?
Do I have to mock the custom hook? How?

Assuming you are working with Jest and Enzyme for unit testing, I would wrap the App component into a shallow wrapper using Enzyme's shallow rendering API.
Then, I will use .find() to find the button element, and use .simulate('click') on the element, to simulate an onClick event on the button, such that the increase method will be called.
After which, I will carry on to check with the expected result and behaviour.
Here is a brief on how it can be done:
import { shallow } from 'enzyme';
import * as React from 'react';
describe('<App />', () => {
it('test for onClick', () => {
const wrapper = shallow(<App />);
wrapper.find('button').simulate('click');
// do the rest here
// expect().....
});
})

You should not need to mock it. When you run App in a test it will just use your custom hook.
If you want to modify it's behaviour somehow specifically for the test, then you would have to mock it.

Related

Adding functions to lit web components in react with typescript

I have a web component i created in lit, which takes in a function as input prop. but the function is not being triggered from the react component.
import React, { FC } from 'react';
import '#webcomponents/widgets'
declare global {
namespace JSX {
interface IntrinsicElements {
'webcomponents-widgets': WidgetProps
}
}
}
interface WidgetProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
var1: string,
successCallback: Function,
}
const App = () =>{
const onSuccessCallback = () =>{
console.log("add some logic here");
}
return(<webcomponents-widgets var1="test" successCallBack={onSuccessCallback}></webcomponents-widgets>)
}
How can i trigger the function in react component? I have tried this is vue 3 and is working as expected.
Am i missing something?
As pointed out in this answer, React does not handle function props for web components properly at this time.
While it's possible to use a ref to add the function property imperatively, I would suggest the more idiomatic way of doing things in web components is to not take a function as a prop but rather have the web component dispatch an event on "success" and the consumer to write an event handler.
So the implementation of <webcomponents-widgets>, instead of calling
this.successCallBack();
would instead do
const event = new Event('success', {bubbles: true, composed: true});
this.dispatch(event);
Then, in your React component you can add the event listener.
const App = () => {
const widgetRef = useRef();
const onSuccessCallback = () => {
console.log("add some logic here");
}
useEffect(() => {
widgetRef.current?.addEventListener('success', onSuccessCallback);
return () => {
widgetRef.current?.removeEventListener('success', onSuccessCallback);
}
}, []);
return(<webcomponents-widgets var1="test" ref={widgetRef}></webcomponents-widgets>);
}
The #lit-labs/react package let's you wrap the web component, turning it into a React component so you can do this kind of event handling declaratively.
React does not handle Web Components as well as other frameworks (but it is planned to be improved in the future).
What is happening here is that your successCallBack parameter gets converted to a string. You need to setup a ref on your web component and set successCallBack from a useEffect:
const App = () => {
const widgetRef = useRef();
const onSuccessCallback = () =>{
console.log("add some logic here");
}
useEffect(() => {
if (widgetRef.current) {
widgetRef.current.successCallBack = onSuccessCallback;
}
}, []);
return(<webcomponents-widgets var1="test" ref={widgetRef}></webcomponents-widgets>)
}

How to test onClick with Jest that is NOT a callback function in props?

I found lots of ways of using mock functions in jest to spy on callback functions that are passed down to a component but nothing on testing a simple onClick that is defined in the same component.
My Example Page:
const ExamplePage: NextPage = () => {
const router = useRouter();
const onClick = (): Promise<void> => {
axios.post(`/api/track`, {
eventName: Event.TRACK_CLICK,
});
router.push("/new-route");
return Promise.resolve();
};
return (
<Container data-testid="container">
<Title>Example Title</Title>
<CreateButton data-testid="create-button" onClick={onClick}>
Create Partner
</CreateButton>
</Container>
);
};
export default ExamplePage;
My current test where I am attempting to get the onClick from getAttribute:
import { fireEvent, render } from "../../../../test/customRenderer";
import ExamplePage from "../../../pages/example-page";
describe("Example page", () => {
it("has a button to create", () => {
const { getByTestId } = render(<ExamplePage />);
const createButton = getByTestId("create-button");
expect(createButton).toBeInTheDocument();
});
it(" the button's OnClick function should be executed when clicked", () => {
const { getByTestId } = render(<ExamplePage />);
// find the button
const createButton = getByTestId("create-button");
// check the button has onClick
expect(createButton).toHaveAttribute("onClick");
// get the onClick function
const onClick = createButton.getAttribute("onClick");
fireEvent.click(createButton);
// check if the button's onClick function has been executed
expect(onClick).toHaveBeenCalled();
});
});
The above fails since there is no onClick attribute only null. My comments in the test highlight my thought process of trying to reach down into this component for the function on the button and checking if it has been called.
Is there any way to test a onClick that is self contained in a react component?
You need to provide mocked router provider and expect that a certain route is pushed to the routers. You also need extract the RestAPI into a separate module and mock it! You can use Dependency Injection, IOC container or import the Api in the component and mock it using jest. I will leave the RestAPi mocking to you.
Mocking router details here: How to mock useRouter
const useRouter = jest.spyOn(require('next/router'), 'useRouter')
describe("", () => {
it("",() => {
const pushMock = jest.fn();
// Mocking Rest api call depends on how you are going to "inject it" in the component
const restApiMock = jest.jn().mockResolvedValue();
useRouter.mockImplementationOnce(() => ({
push: pushMock,
}))
const rendrResult = render(<ExamplePage />);
//get and click the create button
//expect the "side" effects of clicking the button
expect(restApiMock).toHaveBeenCalled();
expect(pushMock).toHaveBeenCalledWith("/new-route");
});
});

How do I use a custom hook inside of useEffect?

I'm working with Amazon's Chime SDK Component Library for React. The component library has a number of components and hooks available for use.
One of the component is <LocalVideo />. The problem is that it starts with the video disabled. In order to turn it on you use the hook useLocalVideo().
In the examples I've seen they use a button to execute the hook:
const MyComponent = () => {
const togglevideo = { useLocalVideo };
return(
<>
<LocalVideo />
<button onClick={toggleVideo}>Toggle</button>
</>
);
};
This works fine. But what if I want the LocalVideo component to load enabled? e.g., if I don't want someone to have to click a button?
I've tried several different approaches including:
Add the code directly to the component (this doesn't work, I assume because the hook is called before the render with the LocalVideo component completes).
Adding the code inside useEffect (invalid Hook call errors).
For example:
const MyComponent = () => {
useEffect( () => {
useLocalVideo();
},
);
const MyComponent = () => {
const { tileId, isVideoEnabled, setIsVideoEnabled, toggleVideo } = useLocalVideo();
useEffect( () => {
setIsVideoEnabled(true);
}, []
);
How can I make this hook run after the component renders?
You should call the function toggleVideo inside the useEffect, not the hook useLocalVideo itself:
const MyComponent = () => {
const { togglevideo } = useLocalVideo();
useEffect(() => {
togglevideo();
}, []);
return (
<LocalVideo />
);
};

How to test React useEffect hooks with jasmine and enzyme

I can't find how to call my useEffect hooks while testing my component.
I tried several solution like this one, but it didn't work: https://reactjs.org/docs/test-utils.html#act
My component :
const mapDispatchToProps = (dispatch: IDispatch, ownProps: ITextAreaOwnProps): ITextAreaDispatchProps => ({
onMount: () => dispatch(addTextArea(ownProps.id)),
});
export const TextArea = (props) => {
React.useEffect(() => {
props.onMount();
}, []);
// more code... //
return (
<>
<TextareaTagName
{...props.additionalAttributes}
className={props.className}
/>
{props.children}
{getValidationLabel()}
</>
);
};
My test :
it('should call prop onMount on mount', () => {
const onMount = jasmine.createSpy('onMount');
mount(<TextArea id="textarea-id" onMount={onMount} />);
expect(onMount).toHaveBeenCalledTimes(1);
});
Regarding the documentation, useEffect should update on any prop change.
You can just recall the test using other props.
You can mock the use effect.
/* mocking useEffect */
useEffect = jest.spyOn(React, "useEffect");
mockUseEffect(); // 2 times
mockUseEffect(); //
const mockUseEffect = () => {
useEffect.mockImplementationOnce(f => f());
};
Short answer is you can't test it directly. You basically need to trigger the change detection in your component to activate the useEffect hook. You can easily use the react-dom/test-utils library by doing something like this
import { act } from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';
it('should call prop onMount on mount', () => {
const onMount = jasmine.createSpy('onMount');
const wrapper = mount(<TextArea id="textarea-id" onMount={onMount} />);
act(() => {
wrapper.update();
});
expect(onMount).toHaveBeenCalledTimes(1);
});
The way you can test the onMount function call is by calling the wrapper.update() method. You have to use mount from enzyme as shallow doesn't support calling hooks yet. More about the issue here -
useEffect not called when the component is shallow rendered #2086
import { mount } from 'enzyme';
it('should call prop onMount on mount', () => {
// arrange
const onMount = jasmine.createSpy('onMount');
const wrapper = mount(<TextArea id="textarea-id" onMount={onMount} />);
// act
wrapper.update();
// assert
expect(onMount).toHaveBeenCalledTimes(1);
});
In case you are making an api call in the useEffect hook, you have to update the wrapper in setTimeout hook so that the changes are reflected on the component.
import { mount } from 'enzyme';
it('should call prop onMount on mount', () => {
// arrange
const onMount = jasmine.createSpy('onMount');
const wrapper = mount(<TextArea id="textarea-id" onMount={onMount} />);
setTimeout(() => {
// act
wrapper.update();
// assert
expect(onMount).toHaveBeenCalledTimes(1);
});
});

Mock a dependency of a React component using Jest

I have a react component (CreateForm). The React component depends on a module (Store). The CreateForm has a Cancel button. On clicking the cancel button, the handleCancel function of the Store module should be called.
I wrote a test unsuccessfully using Jest:
test.only('should handle cancel button click', () => {
jest.mock('../../src/store');
const store = require('../../src/store');
const wrapper = shallow(<CreateForm />);
const cancelButton = wrapper.find('button').at(1);
cancelButton.simulate('click');
expect(store.default.handleCancel).toBeCalled();
});
The test failed. The mock function did not get called and the test failed. Does the react component not get this version of the mock? If so, how do I fix the test? Thanks.
My CreateForm component looks something like the below:
import Store from './store';
render() {
return (
<Panel>
<FormControls />
<button onClick={Store.create}>Create</button>
<button onClick={Store.handleCancel}>Cancel</button>
</Panel>
);
}
A second improvised test that works for me is shown below.
test.only('should handle cancel button click', () => {
const store = require('../../src/store').default;
const cancel = store.handleCancel;
store.handleCancel = jest.fn();
const wrapper = shallow(<CreateForm />);
const cancelButton = wrapper.find('button').at(1);
cancelButton.simulate('click');
expect(store.handleCancel).toBeCalled();
store.handleCancel = cancel;
});
The above test works. I am manually mocking the function, doing the test and restoring the function back to its original after the test. Is there a better way or Jest way of writing the above test? Thanks.
This is how I have managed to spy on imported functions using Jest.
Import everything that is imported in the file you're testing.
Mock it in the beforeEach, you can use more complex mocking if you need to return values or whatever.
In the afterEach call jest.clearAllMocks() to reset all the functions to normal to stop any mocking falling through to other tests.
Putting it all together it looks something like this.
import shallow from 'enzyme'
import * as Store from './Store' // This should be the actual path from the test file to the import
describe('mocking', () => {
beforeEach(() => {
jest.spyOn(Store, 'handleCancel')
jest.spyOn(Store, 'create')
})
afterEach(() => {
jest.clearAllMocks();
})
test.only('should handle cancel button click', () => {
const wrapper = shallow(<CreateForm />);
const cancelButton = wrapper.find('button').at(1);
cancelButton.simulate('click');
expect(Store.handleCancel).toBeCalled();
})
})
Also, in case you need to mock a default import you can do so like this. jest.spyOn(Store, 'default')
You forgot to tell jest how to mock the store module, in your case it is just undefined.
const store = require('../../src/store');
jest.mock('../../src/store', () =>({
handleCancel: jest.fn()
}));
test.only('should handle cancel button click', () => {
const wrapper = shallow(<CreateForm />);
const cancelButton = wrapper.find('button').at(1);
cancelButton.simulate('click');
expect(store.default.handleCancel).toBeCalled();//I'm not sure about the default here
});
With this solution you tell jest to mock the store with an object that has the handleCancel function which is a jest spy. On this spy you can then test that it was called.

Resources