Testing controller with resolve dependencies - angularjs

I'm trying to unit test a controller which relies on resolve keys using Jasmine. I am also using the controllerAs syntax. The routing code is as follows:
$routeProvider.when('/questions', {
templateUrl: 'questions/partial/main_question_viewer/main_question_viewer.html',
controller:'MainQuestionViewerCtrl',
controllerAs:'questionCtrl',
resolve: {
default_page_size: ['QuestionService', function (QuestionService) {
//TODO Work out page size for users screen
return 50;
}],
starting_questions: ['QuestionService', function (QuestionService) {
var questions = [];
QuestionService.getQuestions(1).then(
function(response){
questions = response;
}
);
return questions;
}],
},
});
The controller (so far):
angular.module('questions').controller('MainQuestionViewerCtrl',
[
'QuestionService',
'starting_questions',
'default_page_size',
function (QuestionService, starting_questions, default_page_size) {
var self = this;
//Model Definition/Instantiation
self.questions = starting_questions;
self.page_size = default_page_size;
self.filters = [];
//Pagination Getters (state stored by QuestionService)
self.current_page = function(){
return QuestionService.get_pagination_info().current_page_number;
}
self.page_size = function(page_size){
if(page_size != null){
QuestionService.set_page_size(page_size);
}
return QuestionService.get_page_size();
}
}
]
);
And the test code:
describe('MainQuestionViewerCtrl', function () {
//===============================TEST DATA=====================================
var allQuestionsResponsePage1 = {
count: 4,
next: "https://dentest.com/questions/?format=json&page=2&page_size=1",
previous: null,
results: [
{
id: 1,
subtopic: {
topic: "Math",
name: "Algebra"
},
question: "if a=3 and b=4 what is a+b?",
answer: "7",
restricted: false
}
]
};
beforeEach(module('questions'));
beforeEach(module('globalConstants')); //Need REST URL for mocking responses
var ctrl, qService;
var backend,baseURL;
//inject dependencies
beforeEach(inject(function ($controller, $httpBackend,REST_BASE_URL) {
ctrl = $controller('MainQuestionViewerCtrl');
backend = $httpBackend;
baseURL = REST_BASE_URL;
}));
//inject QuestionService and set up spies
beforeEach(inject(function (QuestionService) {
qService = QuestionService;
}));
//Convenience for adding query params to mocked requests
var buildParams = function (page, page_size) {
var params = {
format: 'json',
page: page,
page_size: page_size,
};
var keys = Object.keys(params).sort(); //how angular orders query params
var returnString = '?' + keys[0] + '=' + params[keys[0]] +
'&' + keys[1] + '=' + params[keys[1]] + '&' + keys[2] + '=' + params[keys[2]];
return returnString;
};
describe('Instantiation',inject(function ($controller) {
beforeEach(module($provide){
beforeEach(inject(function ($controller) {
//Make a mock call to the server to set up state for the QuestionService
backend.expectGET(baseURL + '/questions/' + buildParams(1, 1)).respond(200, allQuestionsResponsePage1);
qService.getQuestions(1);
backend.flush();
//Now mock the result of resolve on route
ctrl = $controller('MainQuestionViewerCtrl', {
default_page_size: 1,
starting_questions: allQuestionsResponsePage1,
});
}));
it('should start with the first page of all the questions pulled down', function () {
expect(qService.questions).toEqual(allQuestionsResponsePage1);
});
it('should start on page 1', function () {
expect(qService.current_page).toEqual(1);
});
it('should start with the page size set to the default passed in',function(){
expect(qService.page_size).toEqual(1);
})
}));
When trying to run the tests, Angular is complaining that it cant resolve starting_questions or default_page_size because the providers for them aren't known.
It worth pointing out that the reason for mocking the HTTP request for the QuestionService is that it builds pagination info based on the response, which the controller will then access to determine the paginator size/numbers in the UI.

Solved. I was instantiating the controller in the outer describe without passing in mock values for the resolve key dependecies. That was causing the error: the method of instantiating the controller with the mock dependecies works fine.

Related

How to get id from url in Angularjs

In my angularjs project the rooting is like this
{
name: 'room-edit',
config: {
url: '/:view?id',
templateUrl: function (params) {
var view = params.view || 'index';
return '/general/' + view + '.html?v=' + version;
},
isSecure: true,
parent: 'generalMaster',
}
}
In the html page I am calling a function to get the information of the Room obj
<div data-ng-init="getRoom()">
And the getRoom() is like this
$scope.getRoom = function () {
var roomid = 15344;
$http.get("/rest/room/get/" + roomid + "?format=json").then(function
(result) {
$scope.room = result.data;
});
};
How can i get the room id from the query string?
Import $location like $scope in controller and
Try this
$location.search()['id']
or
$location.search()['roomid']

Angular - view not updating after model changed after http request

I am writing an app with Angular 1.5, Ionic 1.3, and Cordova. Right now I am working on a part where the user will push a button, the app will store the time and geolocation in the localStorage and then post the data back to the server and update the view. The issue I am having is that in one situation the view is not updating after a model change.
I have tried a few things: $timeout, $rootScope, $q and nothing seems to fix the issue. It only happens on iOS and not on Android. I am also using this library to help with the geolocation process: https://github.com/dpa99c/cordova-diagnostic-plugin
I am aware of the 3rd party library issue where it may not be a part of the Angular digest cycle but I have wrapped it with $q and no luck: https://www.searchenginepeople.com/blog/top-5-technical-issues-large-sites-angularjs.html
I am posting the pseudo code here.
view.html
<ion-view cache-view="false">
<ion-content>
<span>{{ data.text }}</span>
<span>{{ data.date }}</span>
<span>{{ data.isSync }}</span>
<button ng-click="showPopup()">Click</button>
</ion-content>
</ion-view>
controller.js
(function () {
'use strict';
angular
.module('myApp')
.controller('ViewController', ViewController);
ViewController.$inject = ['$scope', 'GeoDataModelService'];
function ViewController($scope, GeoDataModelService) {
$scope.data = GeoDataModelService.value;
$scope.showPopup = GeoDataModelService.showPopup();
}
})();
service.js
(function () {
'use strict';
angular
.module('myApp')
.factory('GeoDataModelService', GeoDataModelService);
GeoDataModelService.$inject = [
...
];
function GeoDataModelService(
...
) {
//data model
var dataModel = {
isSync: true,
text: null,
date: null
};
return {
value: dataModel,
showPopup: showPopup
};
function showPopup() { //gets called
$ionicPopup.confirm({
title: 'clock now ?',
buttons: [{
text: 'CONTINUE',
type: 'button-positive',
onTap: function () {
geoData();
}
}, {
text: 'CANCEL',
type: 'button-default'
}]
});
};
function geoData() {
getLocationServicesStatus()
.then(geoServiceSuccessful)
.catch(function(err) {});
};
function geoServiceSuccessful() { //gets called
DataModelService.createRecord();
sendDataToServerAfterGeoData();
}
function getLocationServicesStatus() {
console.log(' getLocationServicesStatus');
var deferred = $q.defer();
//this is outside of angular
cordova.plugins.diagnostic.isLocationAvailable(
function (available) {
if (available) {
deferred.resolve(true);
} else {
deferred.reject(false);
}
}, function (error) {
deferred.reject(false);
}
);
return deferred.promise;
}
function updateDataModel(source) {
console.log('source ', source); //this one is not null
if (source != null) {
dataModel.text = source.text;
dataModel.date = source.date;
console.log(JSON.stringify(dataModel)); //correct
}
}
function sendDataToServerAfterGeoData() {
//if offline just skip the server post
if (!navigator.onLine) {
// trigger digest cycle
$timeout(function () {
updateModelAfterRecord(); //this one works fine
}, 0);
return;
}
var clockins = DataModelService.load(); //load from local storage
console.log(' * * * * * HERE WE GO * * * * * ');
//this service returns an http promise
DataModelService
.sendLocalDataToService(clockins)
.then(sendDataToServerAfterGeoDataSuccess)
.then(getClockDataToServerAfterGeoSuccess)
.catch(handleSendDeviceDataToServerFail);
};
function sendDataToServerAfterGeoDataSuccess() {
console.log(' sendDataToServerAfterGeoDataSuccess ');
//this service returns an http promise
return DataModelService.getDataModelFromServer();
}
function getClockDataToServerAfterGeoSuccess(response) {
console.log(' getClockDataToServerAfterGeoSuccess ', response);
console.log('1 dataModel: ', dataModel);
// $timeout not working here
// $rootScope.asyncEval not working either
// $rootScope.$apply threw an error
console.log(' 2 dataModel: ', dataModel); //correct
dataModel.isSync = true;
updateDataModel(response); //goes through this code
console.log('3 dataModel: ', dataModel); //correct
console.log(' 4 dataModel: ', dataModel); //correct
console.log('5 dataModel: ', dataModel); //correct
return response; //tried to leave this out - no effect
}
function handleSendDeviceDataToServerFail(error) {
console.log('handleSendDeviceDataToServerFail ', error);
var clockins = DataModelService.load();
dataModel.isSync = false;
updateDataModel(clockins); //this works
}
function updateModelAfterRecord() {
dataModel.isSync = false;
var data = DataModelService.load();
updateDataModel(data);
}
}
})();
I added a watcher to see if the data is changing:
$scope.$watch('data.text', function(newVal, oldVal) {
console.log(' new val', newVal); //this is correct
});

Redirection error in angularjs

After adding data in location table, clicking on the save button should redirect it to list of data in location table.But ,it stays in the same page after adding.The same path is given to modify location,it works fine. whereas the same path does not redirect when add location.
function locationController($scope, $state, $rootScope, locationServices,$location, locations, location, primaryLocation, $stateParams,locationTypes, countries) {
var vm = this;
$scope.locations = locations.data;
$scope.location = location.data;
if (primaryLocation.data && primaryLocation.data[0])
$scope.primaryLocation = primaryLocation.data[0];
if (!$scope.location) {
var location = {};
if ($stateParams.accountId) {
$scope.location = {accountId: $stateParams.accountId };
} else {
$scope.location = location;
}
}
$rootScope.title = "Locations";
$scope.locationslist = "views/locations.html";
$scope.addOrModifyLocation = function (location) {
if (location._id) {
locationServices.modifyLocation(location).then(function (response) {
$location.path('/account/locations/contacts/' + location.accountId + '/' +location.accountId);
// $state.reload();
})
} else {
location.status = 'ACTIVE';
locationServices.addLocation(location).then(function (response) {
$location.path('/account/locations/contacts/' + location.accountId + '/' +location.accountId);
})
}
};
If you want angular to know about your $location update, you have to do it like this :
$rootScope.$apply(function() {
$location.path("/my-path"); // path must start with leading /
});
If you're using ui-router, a cleaner approach would be to use
$state.go('stateName', {'accountId' : location.accountId, });
edit :
If you have errors that happen during a state change, you can see it by adding the following code in your app after declaring your module :
angular.module("appName").run([
"$rootScope",
function ($rootScope) {
$rootScope.$on("$stateChangeError", function(error) {
console.log(error);
});
}
]);

$scope.$watch does not seem to watch factory variable

I'm a beginner to angularjs. In my NFC project, I want to be able to GET from the server data based on a changing patientId.
However, I am not able to see my $watch execute correctly, even though I see that the patientId changes each time I scan a new NFC tag.
var nfc = angular.module('NfcCtrl', ['PatientRecordsService'])
nfc.controller('NfcCtrl', function($scope, NfcService, PatientRecordsService) {
$scope.tag = NfcService.tag;
$scope.patientId = NfcService.patientId
$scope.$watch(function() {
return NfcService.patientId;
}, function() {
console.log("Inside watch");
PatientRecordsService.getPatientRecords(NfcService.patientId)
.then(
function(response) {
$scope.patientRecords = response
},
function(httpError) {
throw httpError.status + " : " +
httpError.data;
});
}, true);
$scope.clear = function() {
NfcService.clearTag();
};
});
nfc.factory('NfcService', function($rootScope, $ionicPlatform, $filter) {
var tag = {};
var patientId = {};
$ionicPlatform.ready(function() {
nfc.addNdefListener(function(nfcEvent) {
console.log(JSON.stringify(nfcEvent.tag, null, 4));
$rootScope.$apply(function(){
angular.copy(nfcEvent.tag, tag);
patientId = $filter('decodePayload')(tag.ndefMessage[0]);
});
console.log("PatientId: ", patientId);
}, function() {
console.log("Listening for NDEF Tags.");
}, function(reason) {
alert("Error adding NFC Listener " + reason);
});
});
return {
tag: tag,
patientId: patientId,
clearTag: function () {
angular.copy({}, this.tag);
}
};
});
Not sure what I'm missing here - please enlighten me!
Update
Per rakslice's recommendation, I created an object to hold my data inside the factory, and now the html (with some server side delay) correctly displays the updated values when a new NFC tag is scanned.
var nfc = angular.module('NfcCtrl', ['PatientRecordsService'])
nfc.controller('NfcCtrl', function($scope, NfcService) {
$scope.tagData = NfcService.tagData;
$scope.clear = function() {
NfcService.clearTag();
};
});
nfc.factory('NfcService', function($rootScope, $ionicPlatform, $filter, PatientRecordsServi\
ce) {
var tagData = {
tag: null,
patientId: null,
patientRecords: []
};
$ionicPlatform.ready(function() {
nfc.addNdefListener(function(nfcEvent) {
//console.log(JSON.stringify(nfcEvent.tag, null, 4));
$rootScope.$apply(function() {
tagData.tag = nfcEvent.tag;
tagData.patientId = $filter('decodePayload')(tagData.tag.ndefMessage[0]);
PatientRecordsService.getPatientRecords(tagData.patientId)
.then(
function(response) {
tagData.patientRecords = response
},
function(httpError) {
throw httpError.status + " : " +
httpError.data;
});
});
console.log("Tag: ", tagData.tag);
console.log("PatientId: ", tagData.patientId);
}, function() {
console.log("Listening for NDEF Tags.");
}, function(reason) {
alert("Error adding NFC Listener " + reason);
})
});
return {
tagData: tagData,
clearTag: function() {
angular.copy({}, this.tagData);
}
};
});
Your code doesn't update the patientId value in the returned NfcService, only the local variable patientId inside the factory function.
Try saving a reference to the object you're returning in the factory function as in a local variable and use that to update the patientId.
For instance, change the creation of the object to put it in a local variable:
var nfcService = {
tag: tag,
patientId: patientId,
clearTag: function () {
angular.copy({}, this.tag);
}
};
...
return nfcService
and then change the patientId update to change the value in the object through the variable.
nfcService.patientId = $filter('decodePayload')(tag.ndefMessage[0]);
Update:
The basic fact about JavaScript that you need to understand is that when you assign one variable to another, if the first variable had a primitive data value the second variable gets a copy of that value, so changing the first variable doesn't affect the second variable after that, but if the first variable had an object reference the second variable gets pointed at that same object that the first variable is pointed at, and changing the object in the first variable after that will affect what you see through the second variable, since it's looking at the same object.
A quick experiment in the browser JavaScript console should give you the idea:
> var a = 1;
> a
1
> var b = a;
> b
1
> a = 5;
> a
5
> b
1
vs.
> var a = {foo: 1}
> var b = a
> a.foo = 5
> a.foo
5
> b.foo
5

how to test inner controller which loads data for select control in angular-formly

I have a ui-select field
{
key: 'data_id',
type: 'ui-select',
templateOptions: {
required: true,
label: 'Select label',
options: [],
valueProp: 'id',
labelProp: 'name'
},
controller: function($scope, DataService) {
DataService.getSelectData().then(function(response) {
$scope.to.options = response.data;
});
}
}
How can I access that inner controller in my unit tests and check that data loading for the select field actually works ?
UPDATE:
An example of a test could be as such:
var initializePageController = function() {
return $controller('PageCtrl', {
'$state': $state,
'$stateParams': $stateParams
});
};
var initializeSelectController = function(selectElement) {
return $controller(selectElement.controller, {
'$scope': $scope
});
};
Then test case looks like:
it('should be able to get list of data....', function() {
$scope.to = {};
var vm = initializePageController();
$httpBackend.expectGET(/\/api\/v1\/data...../).respond([
{id: 1, name: 'Data 1'},
{id: 2, name: 'Data 2'}
]);
initializeSelectController(vm.fields[1]);
$httpBackend.flush();
expect($scope.to.options.length).to.equal(2);
});
You could do it a few ways. One option would be to test the controller that contains this configuration. So, if you have the field configuration set to $scope.fields like so:
$scope.fields = [ { /* your field config you have above */ } ];
Then in your test you could do something like:
$controller($scope.fields[0].controller, { mockScope, mockDataService });
Then do your assertions.
I recently wrote some test for a type that uses ui-select. I actually create a formly-form and then run the tests there. I use the following helpers
function compileFormlyForm(){
var html = '<formly-form model="model" fields="fields"></formly-form>';
var element = compile(html)(scope, function (clonedElement) {
sandboxEl.html(clonedElement);
});
scope.$digest();
timeout.flush();
return element;
}
function getSelectController(fieldElement){
return fieldElement.find('.ui-select-container').controller('uiSelect');
}
function getSelectMultipleController(fieldElement){
return fieldElement.find('.ui-select-container').scope().$selectMultiple;
}
function triggerEntry(selectController, inputStr) {
selectController.search = inputStr;
scope.$digest();
try {
timeout.flush();
} catch(exception){
// there is no way to flush and not throw errors if there is nothing to flush.
}
}
// accepts either an element or a select controller
function triggerShowOptions(select){
var selectController = select;
if(angular.isElement(select)){
selectController = getSelectController(select);
}
selectController.activate();
scope.$digest();
}
An example of one of the tests
it('should call typeaheadMethod when the input value changes', function(){
scope.fields = [
{
key: 'selectOneThing',
type: 'singleSelect'
},
{
key: 'selectManyThings',
type: 'multipleSelect'
}
];
scope.model = {};
var formlyForm = compileFormlyForm();
var selects = formlyForm.find('.formly-field');
var singleSelectCtrl = getSelectController(selects.eq(0));
triggerEntry(singleSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(1);
var multiSelectCtrl = getSelectController(selects.eq(1));
triggerEntry(multiSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(2);
});

Resources