Unit testing onClick handler using Jest in React - javascript

I have the following code which sets a custom handler to each element (which can be a button or a li) click.
componentWillReceiveProps(nextProps) {
this._setCallbackToItemLinks(nextProps);
}
_setCallbackToItemLinks(props) {
const items = props && props.items;
items.forEach(item => {
item.links.forEach(link => {
this._setCallbackToLink(link);
});
});
}
_setCallbackToLink(link) {
link.onClickFromProps = link.onClick;
link.onClick = e => this._onItemClick(e, link.onClickFromProps);
}
onItemClick(e, onClickFromProps) {
console.log("Custom click event handler called");
if (onClickFromProps)
onClickFromProps(e);
}
I want to unit test the above functionality using Jest.
const component= mount(<Comp />);
const mock2 = jest.fn();
component.setProps({ items: [{links: [{ name: 'Test', key: 'Test', url: 'Test', onClick: mock2 }]}] });
const link= component.find('.nav-link');
navLink.simulate('click');
expect(mock2.mock.calls.length).toBe(1);
the test is passing, however I am not getting the console.log statement written in onItemClick method. When I try to debug this test, it is not even going into onItemClick after the simulate click is called.
the navLink does show an <a> tag.
when I log navLink.props().onClick, it shows [Function: bound]

Related

why this function is behaving weirdly?

I've a state named modal in my React App. The initial value is an object that says {show: false, photo: null}.
I've two buttons in the page. One is calling the close function and another is calling the open function. open is setting the state to {show: true, photo: true} and close is just logging modal
I also wrote some code to call the close function when the Esc button is clicked.
Here's my code:
function App() {
const [modal, setModal] = useState({ show: false, photo: null });
// open func
function open() {
setModal({ show: true, photo: true });
}
// close func
function close() {
console.log(modal);
}
// function for esc key press
function escFunc(event) {
if (event.key === `Escape`) {
close();
}
}
useEffect(() => {
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, []);
return (
<>
<button onClick={open}>open</button>
<br />
<button onClick={close}>close</button>
</>
);
}
so now when I click the open button and then click the close button, it's logging {show: true, photo: true} (as expected). but the problem comes in if I press Esc now. It should log {show: true, photo: true} (as the state is already updated by the open function), but it's logging {show: false, photo: null} as if the state hasn't changed yet
Why is it happening?
Whenever a component rerenders, the entire function is reran.
In your useEffect, which is only called on the first render, you call document.addEventListener with the callback function escFunc. This escFunc has a closure that stores the value of modal, which is a reference to the original object state { show: false, photo: null }.
In your open function, you set the state to { show: true, photo: true } using the object literal syntax, which creates a whole new object with a new reference location.
The event listener is still tracking the original object.
To be able to get the new state reference, you need to remove the old event listener and then add a new event listener.
There are multiple ways to do this.
useEffect(() => {
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, [modal]); // add modal to dep array
useEffect(() => {
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, [escFunc]); // add escFunc to dep array, but this would run every render
Stylistically, this is the best option because it properly shows dependencies and doesn't have extra rerenders, but the calls to useCallback might make it slower
const close = useCallback(function() {
console.log(modal);
}, [modal]); // depends on modal
const escFunc = useCallback(function(event) {
if (event.key === `Escape`) {
close();
}
}, [close]); // depends on close
useEffect(() => {
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, [escFunc]); // add escFunc to dep array
In fact, you don't even need to have escFunc outside of useEffect if you don't use it elsewhere
const close = useCallback(function() {
console.log(modal);
}, [modal]); // depends on modal
const escFunc = useCallback(function(event) {
if (event.key === `Escape`) {
close();
}
}, [close]); // depends on close
useEffect(() => {
function escFunc(event) {
if (event.key === `Escape`) {
close();
}
}
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, [close]); // add escFunc to dep array

How to trigger place_changed event in Jest

I am learning how to use the Google Maps API with my React app.
However, I am facing issues with unit testing the google api.
However, there is an issue with global.google.maps.event (it is apparently undefined). I had to mock window.google for the Autocomplete object. So I need to use the 'real' trigger method on the event obj, but mock the autocomplete.
My test for initialisation passes, but not for the 2nd one on triggering the event. I am trying to trigger a place_changed event manually and to check that the place changed handler is called.
Code is as below:
initAutocomplete.ts
import { handlePlaceChange } from "../handlePlaceChange/handlePlaceChange";
export const initAutocomplete = () => {
let autoComplete: google.maps.places.Autocomplete;
autoComplete = new window.google.maps.places.Autocomplete(
document.getElementById('autocomplete') as HTMLInputElement,
{
types: ['establishment'],
componentRestrictions: { 'country': ['SG']},
fields: ['place_id', 'name', 'geometry']
}
)
autoComplete.addListener("place_changed", () => handlePlaceChange(autoComplete))
}
initAutocomplete.test.ts
import { handlePlaceChange } from "../handlePlaceChange/handlePlaceChange";
import { initAutocomplete } from "./initAutocomplete";
jest.mock("../handlePlaceChange/handlePlaceChange");
document.getElementById = jest.fn();
const mockAddListener = jest.fn();
const mockAutoComplete = jest.fn();
window['google'] = {
maps: {
places: {
Autocomplete: mockAutoComplete
} as unknown as google.maps.places.Autocomplete
} as unknown as google.maps.Place
} as any
describe('initAutocomplete', () => {
const mockInputElement : HTMLInputElement = document.createElement('input');
beforeEach(() => {
(document.getElementById as jest.Mock).mockReturnValue(mockInputElement);
(mockAutoComplete as jest.Mock).mockReturnValue({ addListener: mockAddListener });
});
// this test passes
it('initialises google map Autocomplete object', () => {
initAutocomplete();
expect(mockAutoComplete).toHaveBeenCalledWith(
mockInputElement,
{
types: ['establishment'],
componentRestrictions: { 'country': ['SG']},
fields: ['place_id', 'name', 'geometry']
}
);
});
it('calls handlePlaceChange handler when place_changed event is triggered', () => {
initAutocomplete();
// Cannot read properties of undefined (reading 'trigger')
global.google.maps.event.trigger(mockAutoComplete, 'place_changed'); // fails at this line
expect(handlePlaceChange).toHaveBeenCalledWith(mockAutoComplete);
});
})

Javascript removeEventListener on dynamic components

so my code looks like
useEffect(() => {
const element = document.getElementById('player');
document.getElementById('fullscreen').addEventListener('click', () => {
if (screenfull.isEnabled) {
screenfull.request(element);
}
});
document.getElementById('fullscreen-out').addEventListener('click', () => {
if (screenfull.isEnabled) {
screenfull.toggle(element);
}
});
return () => {
document.getElementById('fullscreen').removeEventListener('click', () => {
if (screenfull.isEnabled) {
screenfull.request(element);
}
});
document.getElementById('fullscreen-out').removeEventListener('click', () => {
if (screenfull.isEnabled) {
screenfull.toggle(element);
}
});
}
}, [])
The content platform I'm building has a master state that the admins can change at any time, some components, (such as in the example code) will not be displayed&rendered on the certain state.
Now the problem then lies in the detachment of a event listener. When the state changes, the component is then ripped out of the DOM and the event listener then cannot be removed(in my understanding).
So this causes the following error TypeError: Cannot read properties of null (reading 'removeEventListener')
How can I detach the listener when the component exits?
Any help is appreciated, thanks in advance.
As Dai pointed out in his comment, I should've let React do the work and use the event handlers the framework provides.
onClick is an event handler by itself, and does the exact same thing as the code I provided in my first example.
Thanks Dai.
function requestScreenfull() {
const element = document.getElementById('player');
if (screenfull.isEnabled) {
screenfull.request(element);
}
}
function toggleScreenfull() {
const element = document.getElementById('player');
if (screenfull.isEnabled) {
screenfull.toggle(element);
}
}
<button onClick={requestScreenfull}>Fullscreen</button>
<button onClick={toggleScreenfull}>Toggle</button>
More information on React Documentation

setState causing infinite loop in custom hook

I've created a custom hook within my React app, but for some reason when I update the internal state via an event listener, it causes an infinite loop to be triggered (when it shouldn't). Here's my code:
// Note that this isn't a React component - just a regular JavaScript class.
class Player{
static #audio = new Audio();
static #listenersStarted = false;
static #listenerCallbacks = {
playing: [],
paused: [],
loaded: []
};
static mount(){
const loaded = () => {
this.removeListenerCallback("loaded", loaded);
};
this.addListenerCallback("loaded", loaded);
}
// This method is called on the initialization of the React
// app and is only called once. It's only purpose is to ensure
// that all of the listeners and their callbacks get fired.
static startListeners(){
const eventShorthands = {
playing: "play playing",
paused: "pause ended",
loaded: "loadedmetadata"
};
Object.keys(eventShorthands).forEach(key => {
const actualEvents = eventShorthands[key];
actualEvents.split(" ").forEach(actualEvent => {
this.#audio.addEventListener(actualEvent, e => {
const callbacks = this.#listenerCallbacks[key];
callbacks.forEach(callback => {
callback(e)
});
});
});
});
}
static addListenerCallback(event, callback){
const callbacks = this.#listenerCallbacks;
if(callbacks.hasOwnProperty(event)){
// Remember this console log
console.log(true);
this.#listenerCallbacks[event].push(callback);
}
}
static removeListenerCallback(event, callback){
const listenerCallbacks = this.#listenerCallbacks;
if(listenerCallbacks.hasOwnProperty(event)){
const index = listenerCallbacks[event].indexOf(callback);
this.#listenerCallbacks[event].splice(index, 1);
}
}
}
const usePlayer = (slug) => {
// State setup
const [state, setState] = useReducer(
(state, newState) => ({ ...state, ...newState }), {
mounted: false,
animationRunning: false,
allowNextFrame: false
}
);
const _handleLoadedMetadata = () => {
// If I remove this _stopAnimation, the console log mentioned
// in the player class only logs true to the console 5 times.
// Whereas if I keep it, it will log true infinitely.
_stopAnimation();
};
const _stopAnimation = () => {
setState({
allowNextFrame: false,
animationRunning: false
});
}
useEffect(() => {
Player.addListenerCallback("loaded", _handleLoadedMetadata);
return () => {
Player.removeListenerCallback("loaded", _handleLoadedMetadata);
};
}, []);
return {
mounted: state.mounted
};
};
This makes me think that the component keeps on re-rendering and calling Player.addListenerCallback(), but the strange thing is, if I put a console.log(true) within the useEffect() at the end, it'll only output it twice.
All help is appreciated, cheers.
When you're hooking (pun unintended) up inner functions in React components (or hooks) to external event handlers, you'll want to be mindful of the fact that the inner function's identity changes on every render unless you use useCallback() (which is a specialization of useMemo) to guide React to keep a reference to it between renders.
Here's a small simplification/refactoring of your code that seems to work with no infinite loops.
instead of a class with only static members, Player is a regular class of which there is an app-wide singletonesque instance.
instead of hooking up separate event listeners for each event, the often-overlooked handleEvent protocol for addEventListener is used
the hook event listener callback is now properly useCallbacked.
the hook event listener callback is responsible for looking at the event.type field to figure out what's happening.
the useEffect now properly has the ref to the callback it registers/unregisters, so if the identity of the callback does change, it gets properly re-registered.
I wasn't sure what the state in your hook was used for, so it's not here (but I'd recommend three separate state atoms instead of (ab)using useDispatch for an object state if possible).
The same code is here in a Codesandbox (with a base64-encoded example mp3 that I didn't care to add here for brevity).
const SMALL_MP3 = "https://...";
class Player {
#audio = new Audio();
#eventListeners = [];
constructor() {
["play", "playing", "pause", "ended", "loadedmetadata", "canplay"].forEach((event) => {
this.#audio.addEventListener(event, this);
});
}
play(src) {
if (!this.#audio.parentNode) {
document.body.appendChild(this.#audio);
}
this.#audio.src = src;
}
handleEvent = (event) => {
this.#eventListeners.forEach((listener) => listener(event));
};
addListenerCallback(callback) {
this.#eventListeners.push(callback);
}
removeListenerCallback(callback) {
this.#eventListeners = this.#eventListeners.filter((c) => c !== callback);
}
}
const player = new Player();
const usePlayer = (slug) => {
const eventHandler = React.useCallback(
(event) => {
console.log("slug:", slug, "event:", event.type);
},
[slug],
);
React.useEffect(() => {
player.addListenerCallback(eventHandler);
return () => player.removeListenerCallback(eventHandler);
}, [eventHandler]);
};
export default function App() {
usePlayer("floop");
const handlePlay = React.useCallback(() => {
player.play(SMALL_MP3);
}, []);
return (
<div className="App">
<button onClick={handlePlay}>Set player source</button>
</div>
);
}
The output, when one clicks on the button, is
slug: floop event: loadedmetadata
slug: floop event: canplay

Testing input in MochaJS

I'm fairly new to the MochaJS testing framework and I'm trying to validate a dropdown menu in response to user input. I have a component with the following method:
inside my Search component:
createQueryInputEl() {
const inputEl = document.createElement('input');
Object.assign(inputEl, {
type: 'search',
name: 'query',
autocomplete: 'off',
});
inputEl.addEventListener('input', event =>
this.onQueryChange(this, event.target.value));
The onQueryChange() function creates a dropdown by creating an li element for each result like this:
createResultsEl(results) {
const fragment = document.createDocumentFragment();
results.forEach((result) => {
const el = document.createElement('li');
Object.assign(el, {
className: 'result',
textContent: result.text,
});
// Pass the value to the onSelect callback
el.addEventListener('click', (event) => {
const { onSelect } = this.options;
if (typeof onSelect === 'function') onSelect(result.text);
this.displaySelected(this.inputEl, result.text)
});
fragment.appendChild(el);
});
return fragment;
}
In MochaJS, I'm trying to test the population of that dropdown like this:
it('should populate input on value selection', () => {
const input = document.querySelector('input');
input.value = "New"
input.dispatchEvent(new Event('input'));
const results = document.querySelectorAll('.result');
expect(true).toBeTruthy();
})
The problem is that results keeps coming up empty. When I enter that code into the console, results is a NodeList with each li element of the result.
How do I get MochaJS to simulate this behavior?

Categories