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);
});
})
Related
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
I am have trouble trying to make a unit test for this function. The problem is it using a lib noUiSlider for a range slider and when the test get there , it does not recongnise noUiSlider.set. How do I correctly mock this
TypeError: Cannot read property 'set' of undefined
function popState(){
if (rangeSlider) {
$('.range-reset.' + name).removeClass('hidden');
var element = self.isRangeElement(name).element;
var unit = element.getAttribute('data-unit');
//since noUiSlider accepts no unit,Remove unit from values
unit = new RegExp(unit, 'g');
value = value.replace(unit, '');
value = value.split('-');
***element.noUiSlider.set(value);***
}
}
I have tried this approach it did not work
import { JSDOM } from 'jsdom';
const dom = new JSDOM();
dom.noUiSlider = {
set: jest.fn()
} ;
global.window = dom.window;
And I have tried this as well none work
Object.defineProperty(window.document, 'noUiSlider', {
set: jest.fn(),
});
Unit test case
test('should set range state', () => {
document.body.innerHTML = `<div id="twobuttons-range_0" class="twobuttons-range" data-technicalname="motor" data-unit="mm" data-min="100" data-max="500" data-start="[30,50]"></div>`;
Object.defineProperty(window.document, 'noUiSlider', {
set: jest.fn(),
});
popState();
expect($('#twobuttons-range_0').length).toBe(1);
});
I use it like this and it works:
Inside myComponent.tsx:
import noUiSlider from "nouislider";
....
globalSlider: noUiSlider.noUiSlider;
Inside myComponent.spec.tsx:
component = new myComponent();
...
it("Set value for slider", () => {
component.globalSlider = {
options: undefined, target: undefined, destroy(): void {
}, get(): string | string[] {
return undefined;
}, off(): void {
}, on(): void {
}, reset(): void {
}, updateOptions(): void {
}, set: jest.fn() };
component.setSliderValue("3");
});
I am following some api docs where the only code examples are in vanilla JS but I am trying to use them in React Native. They give fully functional React Native apps for reference but I can't figure out how to repurpose the methods for my needs.
In the api docs it gives the example:
ConnectyCube.videochat.onCallListener = function(session, extension) {
// here show some UI with 2 buttons - accept & reject, and by accept -> run the following code:
var extension = {};
session.accept(extension);
};
ConnectyCube is an module import and I need to use this particular method in React Native. In the app they provide as an example, it looks like this in a class component:
class AppRoot extends React.Component {
componentDidMount() {
ConnectyCube.init(...config)
this.setupListeners();
}
setupListeners() {
ConnectyCube.videochat.onCallListener = this.onCallListener.bind(this);
ConnectyCube.videochat.onUserNotAnswerListener = this.onUserNotAnswerListener.bind(this);
ConnectyCube.videochat.onAcceptCallListener = this.onAcceptCallListener.bind(this);
ConnectyCube.videochat.onRemoteStreamListener = this.onRemoteStreamListener.bind(this);
ConnectyCube.videochat.onRejectCallListener = this.onRejectCallListener.bind(this);
ConnectyCube.videochat.onStopCallListener = this.onStopCallListener.bind(this);
ConnectyCube.videochat.onSessionConnectionStateChangedListener = this.onSessionConnectionStateChangedListener.bind(this);
}
onCallListener(session, extension) {
console.log('onCallListener, extension: ', extension);
const {
videoSessionObtained,
setMediaDevices,
localVideoStreamObtained,
callInProgress
} = this.props
videoSessionObtained(session);
Alert.alert(
'Incoming call',
'from user',
[
{text: 'Accept', onPress: () => {
console.log('Accepted call request');
CallingService.getVideoDevices()
.then(setMediaDevices);
CallingService.getUserMedia(session).then(stream => {
console.log(stream)
localVideoStreamObtained(stream);
CallingService.acceptCall(session);
callInProgress(true);
});
}},
{
text: 'Reject',
onPress: () => {
console.log('Rejected call request');
CallingService.rejectCall(session);
},
style: 'cancel',
},
],
{cancelable: false},
);
}
onUserNotAnswerListener(session, userId) {
CallingService.processOnUserNotAnswer(session, userId);
this.props.userIsCalling(false);
}
onAcceptCallListener(session, userId, extension) {
CallingService.processOnAcceptCallListener(session, extension);
this.props.callInProgress(true);
}
onRemoteStreamListener(session, userID, remoteStream){
this.props.remoteVideoStreamObtained(remoteStream, userID);
this.props.userIsCalling(false);
}
onRejectCallListener(session, userId, extension){
CallingService.processOnRejectCallListener(session, extension);
this.props.userIsCalling(false);
this.props.clearVideoSession();
this.props.clearVideoStreams();
}
onStopCallListener(session, userId, extension){
this.props.userIsCalling(false);
this.props.callInProgress(false);
this.props.clearVideoSession();
this.props.clearVideoStreams();
CallingService.processOnStopCallListener(session, extension);
}
onSessionConnectionStateChangedListener(session, userID, connectionState){
console.log('onSessionConnectionStateChangedListener', userID, connectionState);
}
render() {
console.log('hey');
return <AppRouter />
}
}
function mapDispatchToProps(dispatch) {
return {
videoSessionObtained: videoSession => dispatch(videoSessionObtained(videoSession)),
userIsCalling: isCalling => dispatch(userIsCalling(isCalling)),
callInProgress: inProgress => dispatch(callInProgress(inProgress)),
remoteVideoStreamObtained: remoteStream => dispatch(remoteVideoStreamObtained(remoteStream)),
localVideoStreamObtained: localStream => dispatch(localVideoStreamObtained(localStream)),
clearVideoSession: () => dispatch(clearVideoSession()),
clearVideoStreams: () => dispatch(clearVideoStreams()),
setMediaDevices: mediaDevices => dispatch(setMediaDevices(mediaDevices)),
setActiveVideoDevice: videoDevice => dispatch(setActiveVideoDevice(videoDevice))
}
}
export default connect(null, mapDispatchToProps)(AppRoot)
I want to set up the listeners but I am not using classes like the one in the component above called CallingService or using the same redux actions - I'm taking a functional approach. When I paste the code from the docs in to a service which is just a normal function, I get the error:
Cannot set property 'onCallListener' of undefined.
Any ideas welcome!
componentDidMount() {
document.addEventListener("keyup",this.login,false);
}
login = (event) => {
console.log('i have been activated on keyup event from the componentDidMount()');
};
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]
I have this Jasmine 2 test I am trying to understand why it does not pass.
describe('Some functionality', () => {
let fixture;
let nativeElement;
let componentInstance;
beforeEachProviders(() => [
CenterOnMeButton,
EventAggregatorService,
GeoLocationService,
provide(GoogleMapsAPIWrapper, {useClass: StabGoogleMapsApiWrapper}),
provide(MapController, {useClass: StabMapControllerService}),
provide(MapEventsService, {useClass: StabMapEventsService})
]);
beforeEach(injectAsync([TestComponentBuilder, GoogleMapsAPIWrapper, GeoLocationService, MapController
], (tcb: TestComponentBuilder ) => {
return tcb
.createAsync(CenterOnMeButton).then((componentFixture: ComponentFixture<CenterOnMeButton>) => {
({nativeElement, componentInstance} = componentFixture);
fixture = componentFixture;
componentFixture.detectChanges();
});
}));
it('should emit an event when center button is clicked',
injectAsync([EventAggregatorService], (eventAggregator: EventAggregatorService) => {
spyOn(eventAggregator, 'trigger');
spyOn(componentInstance, 'toogleEnableGeolocation');
nativeElement.querySelector('.toggle.fa.fa-crosshairs').click();
expect(eventAggregator.trigger).toHaveBeenCalled();
expect(componentInstance.toogleEnableGeolocation).toHaveBeenCalled();
}));
});
Center On Me Button
× should emit enableGeolocation event when center button is clicked, but it fails:
Expected spy trigger to have been called.
The method I am spying on:
trigger(event: BaseEvent): EventAggregatorService {
const eventType = event.constructor.name;
const subject: Subject<BaseEvent> = this._subjects[eventType];
if (subject) {
subject.next(event);
}
return this;
}
I would expect(eventAggregator.trigger).toHaveBeenCalled() to be true. It is when I debug the app, but the test fails. Any hint ?