Test async function with Jasmine/Angular - javascript

There is the following Angular code:
$scope.clickByPoint = function(marker, eventName, point) {
var geocoder, location;
$scope.options.info.point = point;
$scope.options.info.show = true;
$scope.searched = false;
$scope.address = "";
geocoder = new google.maps.Geocoder();
location = {
lat: parseFloat(point.latitude),
lng: parseFloat(point.longitude)
};
geocoder.geocode({location: location}, function(results, status) {
$scope.searched = true;
if (status === google.maps.GeocoderStatus.OK) {
$scope.address = results[0].formatted_address;
}
$scope.$digest();
});
};
And my Jasmine test:
describe('$scope.clickByPoint', function() {
var point;
beforeEach(inject(function(_Point_) {
point = _Point_.build('PointName', { latitude: 0, longitude: 0 });
}));
describe('try to find the address', function() {
it('initialize google maps info window', function() {
$scope.clickByPoint(null, null, point)
expect($scope.searched).toEqual(true);
});
})
});
As you can see I'm trying to test 'scope.searched' variable is changed, but it's always 'false', because function is asynchronous. How can I test this code properly? Thanks in advance.

In this case, use a mock of google.maps.Geocoder() in test, using jasmine, because, you are testing clickByPoint() logic and not Google maps api.
var geocoder = new google.maps.Geocoder();
jasmine.createSpy("geocode() geocoder").andCallFake(function(location, callback) {
// no wait time ...
var results = {fake:'', data:''};
var status = google.maps.GeocoderStatus.OK; // get OK value and set it OR redefine it with a Spy.
callback(result, status);
});
Now you can use your test :
describe('try to find the address', function() {
it('initialize google maps info window', function() {
$scope.clickByPoint(null, null, point);
expect($scope.searched).toEqual(true);
});
})

Related

Functions run before button is clicked

I'm calling a chain of functions through one click function, like below (simplified from my actual code). The problem is, initMap() is running before the user clicks the <button>, throwing an error, because the latitude/longitude are not defined until the user inputs an address through the click() function.
How can I prevent initMap() from running before the click() function?
$("button").click(function() {
var user_search = $("input").val();
var url = "https://api.mapbox.com/geocoding/v5/mapbox.places/" + user_search + ".json?access_token=MYAPIKEY";
$.getJSON(url, function(data) {
var number = data.features[0].address;
var route = data.features[0].text;
var latitude = data.features[0].geometry.coordinates[1];
var longitude = data.features[0].geometry.coordinates[0];
initMap(latitude, longitude);
});
});
var map;
var infowindow;
// Create Google Map with location at center
function initMap(latitude, longitude) {
var location = {lat: latitude, lng: longitude};
console.log(location);
map = new google.maps.Map(document.getElementById('map'), {
center: location,
zoom: 15
});
var service = new google.maps.places.PlacesService(map);
service.nearbySearch({
location: location,
radius: 3200,
types: ['school']
}, callback);
}
// List nearby places
function callback(results, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
for (var i = 0; i < results.length; i++) {
listPlaces(results[i]);
}
}
else {
alert("There was an error finding address data.")
}
}
// ONLY list places with characteristics below
function listPlaces(place) {
if (place.name.indexOf('High ') > -1 && place.name.indexOf('Junior') == -1) {
$('body').append('<br>' + place.name);
}
}
ur initMap function is not getting closed properly.the error is because of syntax error not because of initMap() running without button click
$("button").click(function() {
initMap(latitude, longitude);
});
function initMap(latitude, longitude) {
//do stuff
callback();
}
function callback(results, status) {
//do stuff
}
function listPlaces(place) {
//do more stuff
}
Did this is the real code you want?
$("button").click(function() {
initMap(latitude, longitude);
});
function initMap(latitude, longitude) {
//do stuff
}
function callback(results, status) {
//do stuff
}
function listPlaces(place) {
//do more stuff
}

Object property is not created inside a factory member function

I have an object inside an Angular.js factory:
angular.module('myapp')
.factory('geoLocationService', function () {
var geoLocationObj = {};
var geocoder = new google.maps.Geocoder();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(_successFunction, _errorFunction);
}
function _successFunction(position) {
var lat = position.coords.latitude;
var lng = position.coords.longitude;
var latlng = new google.maps.LatLng(lat, lng);
geocoder.geocode({'latLng': latlng}, function (results, status) {
if (status === google.maps.GeocoderStatus.OK) {
if (results[2]) {
geoLocationObj.resultCity = results[2].formatted_address;
alert(geoLocationObj.resultCity);
} else {
alert("No results found");
}
} else {
alert("Geocoder failed due to: " + status);
}
});
}
return geoLocationObj;
});
This works alert(geoLocationObj.resultCity); and alerts in property value
But
console.log(geoLocationObj);
does not log the propety geoLocationObj.resultCity
I am trying to use geoLocationObj.resultCity outside the Factory in my controller but it is not there.
I have in my controller:
.controller('IndexCtrl', function ($scope, geoLocationService) {
var geoLocationService = geoLocationService;
console.log(geoLocationService);
geoLocationService is an empty Object
I cannot figure out why this is happening.
Can you help me with this?
The problem in your code is that your object is initialize in the callback (_successFunction) that means that the returned object is still empty because the _successFunction hasn't been called.
You should return a promise using $q and call .then() in your controller.
angular.module('myapp')
.factory('geoLocationService', ['$q', function ($q) {
var geoLocationObj = {};
var deferred = $q.defer();
var geocoder = new google.maps.Geocoder();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(_successFunction, _errorFunction);
}
function _successFunction(position) {
var lat = position.coords.latitude;
var lng = position.coords.longitude;
var latlng = new google.maps.LatLng(lat, lng);
geocoder.geocode({'latLng': latlng}, function (results, status) {
if (status === google.maps.GeocoderStatus.OK) {
if (results[2]) {
deferred.resolve(results[2].formatted_address);
alert(geoLocationObj.resultCity);
} else {
alert("No results found");
}
} else {
alert("Geocoder failed due to: " + status);
}
});
}
return deferred.promise;
})];
And in your controller
.controller('IndexCtrl', function ($scope, geoLocationService) {
geoLocationService.then(function(geoLocObj){
console.log(geoLocObj); Here your object has been resolved
});
Your factory should return something like this
angular.module('myapp')
.factory('geoLocationService', function() {
return{
geoLocationObj:function(){
var geocoder = new google.maps.Geocoder();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(_successFunction, _errorFunction);
}
function _successFunction(position) {
var lat = position.coords.latitude;
var lng = position.coords.longitude;
var latlng = new google.maps.LatLng(lat, lng);
geocoder.geocode({'latLng': latlng}, function (results, status) {
if (status === google.maps.GeocoderStatus.OK) {
if (results[2]) {
geoLocationObj.resultCity = results[2].formatted_address;
alert(geoLocationObj.resultCity);
} else {
alert("No results found");
}
} else {
alert("Geocoder failed due to: " + status);
}
});
}
return geoLocationObj
}
}
});
You can call in your controller like this
angular.module('myapp', [])
.controller('IndexCtrl', function ($scope, geoLocationService) {
var promise = geoLocationService.geoLocationObj()
promise.then(function(success){
console.log("me "+success);
},function(error){
console.log("They eat all my candies :" +error)
})
});
You can play it here in Js bin
PS. You need to include your google map libs

Google Maps AngularJS with multiple addresses

I have an application that uses an AngularJS service and using Angular-Google-Maps and I do get multiple markers on my map but I can't get the click on each marker to work. The only marker that allows a click is the last one which doesn't allow me to close it after opening the window or if I only have one address the marker works as expected. I think I'm close but can't figure out what I might be missing to have the click on the markers work for all of them. Any ideas on what I'm missing or need to do differently?
Here is the markup on my page.
<div ng-app="myMapApp" ng-controller="mapController">
<ui-gmap-google-map center='map.center' zoom='map.zoom' options="options">
<ui-gmap-markers models="directoryMarkers" coords="'self'" icon="'icon'" click="'onClick'">
<ui-gmap-windows show="show">
<div ng-non-bindable>{{organization}}</div>
</ui-gmap-window>
</ui-gmap-markers>
</ui-gmap-google-map>
</div>
The code in myMapApp.js
var app = angular.module("myMapApp", ['uiGmapgoogle-maps', 'ngStorage']);
The code in mapController.js
app.controller('mapController', function ($scope, Geocoder) {
$scope.map = { center: { latitude: 45, longitude: -73 }, zoom: 10 };
var hfValue = $("#ucDirectory_UcResults_hfResults");
$scope.directoryMarkers = [];
var createMarker = function (organization, address, latitude, longitude, i) {
var ret = {
latitude: latitude,
longitude: longitude,
address: address,
organization: organization,
show: false
};
ret.onClick = function () {
console.log("Clicked!");
ret.show = !ret.show;
};
ret["id"] = i;
return ret;
};
var json = jQuery.parseJSON(hfValue[0].value);
var markers = [];
var i = 0;
var org;
for (var key in json) {
if (json.hasOwnProperty(key)) {
org = json[key].organization;
if (json[key].address.length > 0) {
Geocoder.geocodeAddress(json[key].address).then(function (data) {
markers.push(createMarker(org, json[key].address, data.lat, data.lng, i))
$scope.map.center.latitude = data.lat;
$scope.map.center.longitude = data.lng;
});
i++;
}
}
}
$scope.directoryMarkers = markers;
});
The code in geocoder-service.js
* An AngularJS Service for intelligently geocoding addresses using Google's API. Makes use of
* localStorage (via the ngStorage package) to avoid unnecessary trips to the server. Queries
* Google's API synchronously to avoid `google.maps.GeocoderStatus.OVER_QUERY_LIMIT`.
*
* #author: benmj
* #author: amir.valiani
*
* Original source: https://gist.github.com/benmj/6380466
*/
/*global angular: true, google: true, _ : true */
'use strict';
//angular.module('geocoder', ['ngStorage']).factory('Geocoder', function ($localStorage, $q, $timeout, $rootScope) {
app.factory('Geocoder', function ($localStorage, $q, $timeout, $rootScope) {
var locations = $localStorage.locations ? JSON.parse($localStorage.locations) : {};
var queue = [];
// Amount of time (in milliseconds) to pause between each trip to the
// Geocoding API, which places limits on frequency.
var QUERY_PAUSE = 250;
/**
* executeNext() - execute the next function in the queue.
* If a result is returned, fulfill the promise.
* If we get an error, reject the promise (with message).
* If we receive OVER_QUERY_LIMIT, increase interval and try again.
*/
var executeNext = function () {
var task = queue[0],
geocoder = new google.maps.Geocoder();
geocoder.geocode({ address: task.address }, function (result, status) {
if (status === google.maps.GeocoderStatus.OK) {
var parsedResult = {
lat: result[0].geometry.location.lat(),
lng: result[0].geometry.location.lng(),
formattedAddress: result[0].formatted_address
};
locations[task.address] = parsedResult;
$localStorage.locations = JSON.stringify(locations);
queue.shift();
task.d.resolve(parsedResult);
} else if (status === google.maps.GeocoderStatus.ZERO_RESULTS) {
queue.shift();
task.d.reject({
type: 'zero',
message: 'Zero results for geocoding address ' + task.address
});
} else if (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) {
if (task.executedAfterPause) {
queue.shift();
task.d.reject({
type: 'busy',
message: 'Geocoding server is busy can not process address ' + task.address
});
}
} else if (status === google.maps.GeocoderStatus.REQUEST_DENIED) {
queue.shift();
task.d.reject({
type: 'denied',
message: 'Request denied for geocoding address ' + task.address
});
} else {
queue.shift();
task.d.reject({
type: 'invalid',
message: 'Invalid request for geocoding: status=' + status + ', address=' + task.address
});
}
if (queue.length) {
if (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) {
var nextTask = queue[0];
nextTask.executedAfterPause = true;
$timeout(executeNext, QUERY_PAUSE);
} else {
$timeout(executeNext, 0);
}
}
if (!$rootScope.$$phase) { $rootScope.$apply(); }
});
};
return {
geocodeAddress: function (address) {
var d = $q.defer();
if (_.has(locations, address)) {
d.resolve(locations[address]);
} else {
queue.push({
address: address,
d: d
});
if (queue.length === 1) {
executeNext();
}
}
return d.promise;
}
};
});
As an aside, if you don't have a lot of windows open at the same time, you shouldn't use the windows directive, instead use the window directive and define it as a sibling to your markers. As recommended by the documentation.
But to answer the original question, this plnkr uses your code, minus the geocoding, to produce markers with windows. It takes two clicks on a marker to get to where you want it to be because the click happens before the value is changed.
I think to get the behavior you want it would look more like the following:
html:
<ui-gmap-google-map center='map.center' zoom='map.zoom' options="options">
<ui-gmap-markers fit="true" models="directoryMarkers" coords="'self'" icon="'icon'" click="'onClick'">
</ui-gmap-markers>
<ui-gmap-window show="selected.show" coords="selected">
<div>{{selected.organization}}</div>
</ui-gmap-window>
controller:
$scope.map = {
center: {
latitude: 45,
longitude: -73
},
zoom: 10
};
$scope.directoryMarkers = [];
$scope.selected = null;
var createMarker = function(latitude, longitude, i) {
var ret = {
latitude: latitude,
longitude: longitude,
organization: "Foo",
show: false
};
ret.onClick = function() {
console.log("Clicked!");
$scope.selected = ret;
ret.show = !ret.show;
};
ret["id"] = i;
return ret;
};
var markers = [];
var org;
var coords = chance.coordinates().split(",");
$scope.map.center.latitude = coords[0];
$scope.map.center.longitude = coords[1];
for (var i = 0; i < 20; i++) {
coords = chance.coordinates().split(",");
markers.push(createMarker(coords[0], coords[1], i));
}
$scope.directoryMarkers = markers;
Which can be seen tied together in this plnkr: http://plnkr.co/edit/rT4EufIGcjplgd8orVWu?p=preview

How to get the result from an anonymous function in Javascript?

I'm using a simple script to geocode addresses.
var geocoder;
var departure;
var arrival;
function initialize() {
geocoder = new google.maps.Geocoder();
}
google.maps.event.addDomListener(window, 'load', initialize);
function geocode(options) {
var address = options.address.val() || null;
var result = {};
if (address) {
geocoder.geocode( { 'address': address }, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
result.lat = results[0].geometry.location.lat();
result.lng = results[0].geometry.location.lng();
} else {
result.lat = null;
result.lng = null;
}
});
}
}
function geocode_all() {
departure = geocode({
address: $('#departure')
});
//console.log(departure);
arrival = geocode({
address: $('#arrival')
});
//console.log(arrival);
}
I would like that my departure variable and my arrival variable to be object with the resulting latitude and longitude.
How should I proceed as my result variable is out of scope?
Thanks !
The problem isn't that the function is anonymous, and it isn't scope; the problem is that the Google geolocation API is asynchronous. It's impossible for your geocode function to return its result; instead, just like Google's function, it must accept a callback that it calls back with the result later, when the result is known. (Or it can use callbacks indirectly, via the "promise" pattern, but the fundamental concept is the same.)
So, using a callback:
function geocode(options, callback) {
// ^--- Accept the callback
var address = options.address.val() || null;
var result = {};
if (address) {
geocoder.geocode( { 'address': address }, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
result.lat = results[0].geometry.location.lat();
result.lng = results[0].geometry.location.lng();
} else {
result.lat = null;
result.lng = null;
}
callback(result); // <== Call it
});
}
}
// usage
geocode({/*...*/}, function(result) {
// This gets called later, asynchronously, with the result
});
Or using a jQuery Deferred/Promise:
function geocode(options) {
var d = new $.Deferred(); // <== Create the deferred
var address = options.address.val() || null;
var result = {};
if (address) {
geocoder.geocode( { 'address': address }, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
result.lat = results[0].geometry.location.lat();
result.lng = results[0].geometry.location.lng();
} else {
result.lat = null;
result.lng = null;
}
// Resolve the Deferred using your result
d.resolve(result);
});
}
// Return the Promise for the Deferred
return d.promise();
}
// Usage
geocode({/*....*/}).done(function(result) {
// This gets called later, asynchronously, with the result
});

Using Google Maps: a custom javascript function returning undefined

I'm trying to return the variable coord from GetLocation, but it only returns undefined.
Any help appreciated!
var coord = "";
function GetLocation(address) {
var geocoder = new google.maps.Geocoder();
geocoder.geocode( { "address": address }, function (results, status) {
if (status == google.maps.GeocoderStatus.OK) {
coord = ParseLocation(results[0].geometry.location);
// This alert shows the proper coordinates
alert(coord);
}
else{ }
});
// this alert is undefined
alert(coord);
return coord;
}
function ParseLocation(location) {
var lat = location.lat().toString().substr(0, 12);
var lng = location.lng().toString().substr(0, 12);
return lat+","+lng;
}
When you are returning coords from the outer function it is still in fact undefined. The inner function executes later when the asynchronous operation (if it wasn't asynchronous, the API would just give the result to you normally) is done.
Try passing a callback:
function GetLocation(address, cb) {
var geocoder = new google.maps.Geocoder();
geocoder.geocode( { "address": address }, function (results, status) {
if (status == google.maps.GeocoderStatus.OK) {
cb(ParseLocation(results[0].geometry.location));
}
else{ }
});
}
You can then use it like so:
GetLocation( "asd", function(coord){
alert(coord);
});

Categories