Enzyme / Jest spies not called onClick - reactjs

I am trying to test an event handler with Enzyme / Jest for a react component, however my spy function is never called...
My component has a div with an id and I am using that to find the dom elem
render() {
return (
<div>
<Top
{...this.props}
/>
<div
id = 'keyboard_clickable'
onClick = {this._handleClick}
style= {styles.main}>
{this._showKeyBoard()}
</div>
<input onChange = {() => {}}/>
</div>
);
}
}
My test
describe('onclick function is called ...', () => {
it.only('spyOn', () => {
const spy = jest.fn()
const wrapper = shallow(
<Keyboard
_getData = { () => {} }
_erase = { () => {} }
_get_letters = { () => {} }
_createWord = { () => {} }
_modeSwitch = { () => {} }
onClick = { spy }
/>
)
wrapper.find('#keyboard_clickable').simulate('click', {
target:{
parentElement:{ id: 5 },
id:6
}
})
expect(spy).toHaveBeenCalled();
})
})
my HandleClick
_handleClick = e => {
let parent = e.target.parentElement;
if(e.target.id || (!isNaN(parent.id) && parent.id > 0) ) {
this.props._getData(e.target.id || e.target.parentElement.id)
this.setState({status:'ok'})
}else{
this.setState({status:'Use numbers between 2 and 9'},()=>{
return alert(this.state.status)
})
}
}
Test output
Expected mock function to have been called, but it was not called.

To test that your event handler is called, you'll have to replace the event handler with a mock function. One way to do that is to extend your component class:
class TestKeyboard extends Keyboard {
constructor(props) {
super(props)
this._handleClick = this.props._handleClick
}
}
describe('onclick function is called ...', () => {
it.only('spyOn', () => {
const spy = jest.fn()
const wrapper = shallow(
<TestKeyboard
_getData = { () => {} }
_erase = { () => {} }
_get_letters = { () => {} }
_createWord = { () => {} }
_modeSwitch = { () => {} }
_handleClick = { spy }
/>
)
wrapper.find('#keyboard_clickable').simulate('click', {
target:{
parentElement:{ id: 5 },
id:6
}
})
expect(spy).toHaveBeenCalled();
})
})
The prop _handleClick replaces Keyboard._handleClick and is therefore called when onClick fires in the clicked element.
Alternatively, jest.spyOn
In case you need to let the event handler execute and test what it was called with, you can use jest.spyOn. I find this method more complex, but it is more flexible.
import { mount } from 'enzyme'
describe('onclick function is called ...', () => {
it.only('spyOn', () => {
const wrapper = mount(
<Keyboard
_getData = { () => {} }
_erase = { () => {} }
_get_letters = { () => {} }
_createWord = { () => {} }
_modeSwitch = { () => {} }
/>
)
const spy = jest.spyOn(wrapper.instance(), '_handleClick')
wrapper.instance().forceUpdate()
wrapper.find('#keyboard_clickable').simulate('click', {
target:{
parentElement:{ id: 5 },
id:6
}
})
expect(spy).toHaveBeenCalled();
})
})
Note that this will fail when using shallow rendering so you'll have to use enzyme.mount instead.

It seems that simulating a click with enzyme in shallow render does work, but only if I forceUpdate(), like in Jemi's solution.
example:
beforeEach(() => {
wrapper = shallow(<VehicleDamage handleSubmit={mockFunction} onPrevClick={mockFunction} />);
});
it('handlePrevClick is called on click', function() {
const spyHandlePrevClick = jest.spyOn(wrapper.instance(), 'handlePrevClick');
wrapper.instance().forceUpdate(); // I assume required to assign mocked function
let buttonContainer = wrapper.find('ButtonContainer').dive();
let button = buttonContainer.find('Button').at(0);
button.simulate('click', {
preventDefault: () => jest.fn()
});
expect(spyHandlePrevClick).toHaveBeenCalled();
});

Related

React - child.ref.current is null in componentdidmount

Here's many similar questions but I still couldn't solve this problem.
Child's ref is null in Listner.
I really don't understand what this is.
The code is below.
react 17.0.1
// Parent.tsx
const Parent: React.FC<{id: string}> = (props) => {
const [id] = useState(props.id)
const modalRef = createRef<ModalRef>();
// If I registerd the Listner here, modalRef is not null but,
// multiple Listner has registered.
useEffect(() => {
listner.on('MODAL_POPUP', (o:{param:string}) => {
modalRef.current?.pop(o.param); // <--- modalRef.current is null
});
return() => {};
}, []);
return (
<Modal ref={modalRef} id={id}>
<div>contents</div>
</Modal>
);
};
// Modal.tsx
export interface ModalProps {
id: string;
}
export interface ModalRef {
pop: () => void;
}
const Modal = React.forwardRef<ModalRef, ModalProps>((props, ref) => {
const [id] = useState(props.id);
useImperativeHandle(ref, () => ({
pop() {
console.log('popup modal');
},
}));
return createPotal(
<div>contents..</div>,
document.getElementById('modal-root') as HTMLElement,
);
});
Any advice for me?
Thanks.
You need to use useRef for creating the ref in React Function Components, so change it to this:
const Parent: React.FC<{id: string}> = (props) => {
const [id] = useState(props.id)
const modalRef = useRef<ModalRef>(); // <== here
useEffect(() => {
listner.on('MODAL_POPUP', (o:{param:string}) => {
modalRef.current?.pop(o.param);
});
return() => {
listner.off('MODAL_POPUP', ()=>{});
};
}, []);
return (
<Modal ref={modalRef} id={id}>
<div>contents</div>
</Modal>
);
};

How to test call back refs with Enzyme and Jest

I want to test addEventListener/RemoveEventListener for an Element.
class Image extends PureComponent {
state = {
isLoaded: false,
};
componentDidMount() {
if (!this.props.src) return;
this.imageToLoad = document.createElement('img');
this.imageToLoad.addEventListener('load', this.loadImage);
this.imageToLoad.src = this.props.src;
}
componentWillUnmount() {
if (!this.imageToLoad) return;
this.imageToLoad.removeEventListener('load', this.loadImage);
}
loadImage = () => {
this.setState({ isLoaded: true }, () => {
this.myimg.src = this.imageToLoad.src;
});
}
render() {
const { className, src, alt } = this.props;
const { isLoaded } = this.state;
if (isLoaded === false || !src) return '';
return (
<img
className={className}
ref={img => (this.myimg = img)}
src={src}
alt={alt}
/>
);
}
}
Test code:-
it('should add event listener on mount', () => {
const elementMock = { addEventListener: jest.fn() };
jest.spyOn(Image.prototype, 'myimg').mockImplementation(() => elementMock);
expect(elementMock.addEventListener).toBeCalledWith('load', expect.any(Function), false);
});
But it is not working as expected.

value must be a mock or spy function when using jest.fn

Getting this error
Matcher error: received value must be a mock or spy function
Received has type: object
Received has value: {}
However, i think i shouldn't be getting this error because im using jest.fn. So im mocking the function.
describe('Should simulate button click', ()=> {
it('should simulate button click', () => {
// add the name of the prop, which in this case ites called onItemAdded prop,
// then use jest.fn()
const wrapper = shallow(<TodoAddItem onItemAdded={() => jest.fn()}/>)
// console.log('props',wrapper.find('button').props());
wrapper.find('button').simulate('click');
expect(wrapper).toHaveBeenCalled(); // error happens when this executes
})
})
todo-add-item.js
import React, { Component } from 'react';
import './todo-add-item.css';
export default class TodoAddItem extends Component {
render() {
return (
<div className="todo-add-item">
<button
className="test-button btn btn-outline-secondary float-left"
onClick={() => this.props.onItemAdded('Hello world')}>
Add Item
</button>
</div>
);
}
}
app.js (using the component in this file)
import React, { Component } from 'react';
import AppHeader from '../app-header';
import SearchPanel from '../search-panel';
import TodoList from '../todo-list';
import ItemStatusFilter from '../item-status-filter';
import TodoAddItem from '../todo-add-item';
import './app.css';
export default class App extends Component {
constructor() {
super();
this.createTodoItem = (label) => {
return {
label,
important: false,
done: false,
id: this.maxId++
}
};
this.maxId = 100;
this.state = {
todoData: [
this.createTodoItem('Drink Coffee'),
this.createTodoItem('Make Awesome App'),
this.createTodoItem('Have a lunch')
]
};
this.deleteItem = (id) => {
this.setState(({ todoData }) => {
const idx = todoData.findIndex((el) => el.id === id);
const newArray = [
...todoData.slice(0, idx),
...todoData.slice(idx + 1)
];
return {
todoData: newArray
};
});
};
this.addItem = (text) => {
const newItem = this.createTodoItem(text);
this.setState(({ todoData }) => {
const newArray = [
...todoData,
newItem
];
return {
todoData: newArray
};
});
};
this.onToggleImportant = (id) => {
console.log('toggle important', id);
};
this.onToggleDone = (id) => {
console.log('toggle done', id);
};
};
render() {
return (
<div className="todo-app">
<AppHeader toDo={ 1 } done={ 3 } />
<div className="top-panel d-flex">
<SearchPanel />
<ItemStatusFilter />
</div>
<TodoList
todos={ this.state.todoData }
onDeleted={ this.deleteItem }
onToggleImportant={ this.onToggleImportant }
onToggleDone={ this.onToggleDone } />
<TodoAddItem onItemAdded={ this.addItem } />
</div>
);
};
};
I'm not 100% sure, but I believe you should do something like this:
describe('should simulate button click', () => {
it('should simulate button click', () => {
const mockedFunction = jest.fn();
const wrapper = shallow(<TodoAddItem onItemAdded={ mockedFunction } />);
wrapper.find('button').simulate('click');
expect(mockedFunction).toHaveBeenCalled();
});
});
You are testing if the onItemAdded function gets called when you click the <TodoAddItem /> component. So you have to mock it first using jest.fn and then check if the mocked function got called after you simulated the click.
For me works replacing the next one:
const setCategories = () => jest.fn();
With this one:
const setCategories = jest.fn();
I suppose that you should to set just jest.fn or jest.fn() in your code.

React Native Navigation / Context API

I'm using React-Native-Navigation with the Context api.
I'm wrapping my screens with a HOC component below.
const ProviderWrap = Comp => props => (
<Provider>
<Comp {...props} />
</Provider>
);
Navigation.registerComponent('app.AuthScreen', () => ProviderWrap(AuthScreen));
Navigation.registerComponent('app.FindPlaceScreen', () => ProviderWrap(FindPlaceScreen));
Navigation.registerComponent('app.SharePlaceScreen', () => ProviderWrap(SharePlaceScreen));
And this is my Provider Component
class Provider extends Component {
state = {
placeName: 'hello',
image: 'https://images.unsplash.com/photo-1534075786808-9b819c51f0b7?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=4dec0c0b139fb398b714a5c8264b4a9a&auto=format&fit=crop&w=934&q=80',
places: [],
}
textInputHandler = (text) => {
this.setState({
placeName: text,
})
}
searchInputHandler = (text) => {
const SearchedPlace = this.state.places.filter(place => {
return place.name.includes(text)
})
this.setState(prevState => {
return {
places: SearchedPlace,
}
})
}
placeSubmitHandler = () => {
if (this.state.placeName) {
this.setState(prevState => {
return {
places: [...prevState.places, {
name: prevState.placeName,
image: prevState.image,
}],
placeName: '',
}
})
} else {
return;
}
}
placeDeleteHandler = (id, deleteModal) => {
const newPlaces = this.state.places.filter((place, index) => {
return index !== id
})
this.setState({
places: newPlaces,
})
deleteModal();
}
render() {
return (
<GlobalContext.Provider value={{
state: this.state,
textInputHandler: this.textInputHandler,
placeSubmitHandler: this.placeSubmitHandler,
placeDeleteHandler: this.placeDeleteHandler,
searchInputHandler: this.searchInputHandler,
}}>
{this.props.children}
</GlobalContext.Provider>
)
}
}
My issue is that The Context state isnt shared between screens. in my context state i have a places array which i add to in the SharePlaceScreen which works fine but when i go over to FindPlaceScreen that context state places is empty. As if i had two separate context for each screen.
Here is an example of singleton Object there are many implementation you cas use es6 class also ... es6 examples
var SingletonState = (function () {
var state;
function createState() {
return {someKey:'what ever'};
}
return {
getState: function () {
if (!state) {
state = createState();
}
return state;
}
};
})();
// usage
var state1 = SingletonState.getState();
var state2 = SingletonState.getState();
console.log("Same state? " + (state1 === state2));

Recaptcha Missing required parameters: sitekey

I'm integrating recaptcha to my react app and I'm having this error: "Missing required parameters: sitekey" when I use the render method of grecaptch even though I have properly included the sitekey in to the parameters.
Here is my code for reference:
class ReCaptcha extends React.Component {
componentDidMount() {
this.callbackName = 'g-lf-callback';
window[this.callbackName] = this.props.callback;
if(window.grecaptcha) {
this.renderRecaptcha();
} else {
this.loadRecaptchaScript();
}
}
componentWillUnmount() {
delete window['rconload'];
delete window[this.callbackName];
}
loadRecaptchaScript() {
window['rconload'] = () => {
this.renderRecaptcha();
};
let url = 'https://www.google.com/recaptcha/api.js?onload=rconload&render=explicit';
loadScript(url, '', 'recaptcha', true, true)
.then( () => {})
.catch( () => {});
}
renderRecaptcha = () => {
if(this.container) {
let parameters = {
sitekey: process.env.MY_SITEKEY,
size: 'invisible',
badge: null,
callback: 'g-lf-callback'
};
const recaptchaId = window.grecaptcha.render(this.container, parameters);
this.execute = () => window.grecaptcha.execute(recaptchaId);
this.reset = () => window.grecaptcha.reset(recaptchaId);
this.getResponse = () => window.grecaptcha.getResponse(recaptchaId);
}
}
onClick = () => {
this.execute();
}
render() {
let { callback, label, className, ...btnProps } = this.props;
return (
<Button
{ ...btnProps }
className={ className }
onClick={ this.onClick }
ref = { ref => this.container = ref }
>
{ label }
</Button>
)
}
}
I have also set the render parameter of recaptcha url to explicit
https://www.google.com/recaptcha/api.js?onload=rconload&render=explicit
Onload callback:
window['rconload'] = () => {
this.renderRecaptcha();
};

Resources