I have a controller like this.
app.controller('MyCtrl', function() {
let ctrl = this;
if (ctrl.contract) {
ctrl.contract.typeId = ctrl.contract.type.id;
} else {
ctrl.contract = {
name: 'test'
};
}
....
It can either have, or not have contract bound to it.
Problem arises in my test
describe('MyCtrl', () => {
let ctrl;
beforeEach(angular.mock.module('mymodule'));
beforeEach(angular.mock.module('ui.router'));
describe('Update contract', () => {
beforeEach(inject((_$controller_) => {
ctrl = _$controller_('MyCtrl', {}, {
contract: {
type: {
id: 2
}
}
});
}));
it('should set the contract.typeId to be the id of the type of the contract that was sent in', () => {
expect(ctrl.contract.typeId).toBe(ctrl.contract.type.id);
});
})
});
I pass in a contract object, which means it should go into the first if in my controller and set the typeId.
But it always goes into the else no matter what I do.
How can I make sure the controller don't run or start before all my variables are bound to it?
I believe that the issue is that you need to get the $controller service directly and also pass a valid scope (not an empty object)
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('MyCtrl', {
$scope: scope,
}, {
contract: {
type: {
id: 2
}
}
});
}));
Related
Hi I have a Angular service that uses another service that loads data from the local storage on init.
angular
.module('app')
.factory('localStorage', function ($window)
{
if (!$window.localStorage)
{
// throw Error
}
return $window.localStorage;
});
angular
.module('app')
.factory('session', function (localStorage)
{
var container = JSON.parse(localStorage.getItem('sessionContainer'));
return {
getUser: getUser
};
});
Now i want to test the session service.
describe('SessionService', function ()
{
var service;
var localStorageMock;
// Load the module.
beforeEach(module('appRegistration'));
// Create mocks.
beforeEach(function ()
{
logMock = {};
localStorageMock = jasmine.createSpyObj('localStorageServiceMockSpy', ['setItem', 'getItem']);
localStorageMock.getItem.and.returnValue('{}');
module(function ($provide)
{
$provide.value('localStorage', localStorageMock);
});
inject(function (_session_)
{
service = _session_;
});
});
it('should call `getItem` on the `localStorageService` service', function ()
{
expect(localStorageMock.getItem).toHaveBeenCalledWith('sessionContainer');
});
describe('getUser method', function ()
{
it('should return an empty object when the user is not set', function ()
{
var result = service.getUser();
expect(result).toEqual({});
});
it('should return the user data', function ()
{
// localStorageMock.getItem.and.returnValue('{"user":{"some":"data"}}');
var result = service.getUser();
expect(result).toEqual({some: 'user data'});
});
});
});
As you can see in the should return the user data section.
I need a way to update the container so getUser returns the expected data.
I tried to update the getItem spy, but this does not work. The localStorageMock is already injected in the session service when i want to change the spy.
Any help?
The most simple way is to have a variable with mocked value that is common for both function scopes:
var getItemValue;
beforeEach({
localStorage: {
getItem: jasmine.createSpy().and.callFake(function () {
return getItemValue;
}),
setItem: jasmine.createSpy()
}
});
...
it('should return the user data', function ()
{
getItemValue = '{"user":{"some":"data"}}';
inject(function (_session_) {
service = _session_;
});
var result = service.getUser();
expect(result).toEqual({some: 'user data'});
});
Notice that inject should be moved from beforeEach to it for all specs (the specs that don't involve getItemValue may use shorter syntax, it('...', inject(function (session) { ... }))).
This reveals the flaw in service design that makes it test-unfriendly.
The solution is to make container lazily evaluated, so there is time to mock it after the app was bootstrapped with inject:
.factory('session', function (localStorage)
{
var containerCache;
function getUser() {
...
return this.container;
}
return {
get container() {
return (containerCache === undefined)
? (containerCache = JSON.parse(localStorage.getItem('sessionContainer')))
: containerCache;
},
getUser: getUser
};
});
Additionally, this makes possible to test session.container as well. In this case localStorageMock.getItem spy value may be redefined whenever needed.
I'm trying to unit test a function within my controller but am unable to get a $scope variable to be testable. I'm setting the variable in my controller's .then() and want to unit test to make sure this is set appropriately when it hits the .then block.
My test controller code:
function submit() {
myService.submit().then(function(responseData){
if(!responseData.errors) {
$scope.complete = true;
$scope.details = [
{
value: $scope.formattedCurrentDate
},
{
value: "$" + $scope.premium.toFixed(2)
},
];
} else {
$scope.submitError = true;
}
});
}
Where this service call goes is irrelevant. It will return JSON with action: 'submitted', 'response' : 'some response'. The .then() checks if errors are present on responseData, and if not it should set some details. These $scope.details are what I'm trying to test in my unit test below:
it('should handle submit details', function () {
var result;
var premium = 123.45;
var formattedCurrentDate = "2016-01-04";
var promise = myService.submit();
mockResponse = {
action: 'submitted',
response: 'some response'
};
var mockDetails = [
{
value: formattedCurrentDate
},
{
value: "$"+ premium.toFixed(2)
}
];
//Resolve the promise and store results
promise.then(function(res) {
result = res;
});
//Apply scope changes
$scope.$apply();
expect(mockDetails).toEqual(submitController.details);
});
I'm receiving an error that $scope.details is undefined. I'm not sure how to make the test recognize this $scope data changing within the controller.
Before each and other functions in my unit test:
function mockPromise() {
return {
then: function(callback) {
if (callback) {
callback(mockResponse);
}
}
}
}
beforeEach(function() {
mockResponse = {};
module('myApp');
module(function($provide) {
$provide.service('myService', function() {
this.submit = jasmine.createSpy('submit').and.callFake(mockPromise);
});
});
inject(function($injector) {
$q = $injector.get('$q');
$controller = $injector.get('$controller');
$scope = $injector.get('$rootScope');
myService = $injector.get('myService');
submitController = $controller('myController', { $scope: $scope, $q : $q, myService: myService});
});
});
How do I resolve the promise within my unit test so that I can $scope.$digest() and see the $scope variable change?
You should look how to test promises with jasmine
http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy/
using a callFake would do what you try to mock
spyOn(myService, 'submit').and.callFake(function() {
return {
then: function(callback) { return callback(yourMock); }
};
});
I have an Angular controller, which appeared to be working fine. I can console log the user variable inside of the service call, and it contains the correct data. However in my test, I can console log the controller and verify the user object is there, but it is empty. It really seems like initialize is trying to store the variable after the local scope is destroyed, but it is very strange as I have another controller & test written in the exact same way working fine.
I have been iterating over this for two days, so if anyone has any leads, I would be most grateful.
function DetailAccountController (accountsService) {
'use strict';
var user = {};
initialize();
return {
user: user
};
/**
* Initialize the controller,
* & fetch detail for a single user.
*/
function initialize () {
// If the service is available, then fetch the user
accountsService && accountsService.getById('').then(function (res) {
user = res;
});
}
}
and a jasmine test:
describe('DetailAccountController', function () {
var ctrl = require('./detail-account-controller'),
data = [{
"email": "fakeUser0#gmail.com",
"voornaam": "Mr Fake0",
"tussenvoegsel": "van0",
"achternaam": "User0",
"straat": "Mt Lincolnweg0",
"huisnr": 0,
"huisnr_toev": 0,
"postcode": "0LW",
"telefoonr": "0200000000",
"mobielnr": "0680000000",
"plaats": "Amsterdam",
"id": "00000000"
}],
accountsServiceMock,
$rootScope,
$q;
beforeEach(inject(function (_$q_, _$rootScope_) {
$q = _$q_;
$rootScope = _$rootScope_;
accountsServiceMock = {
getById: function () {}
};
}));
it('should call the getById method at least once', function () {
spyOn(accountsServiceMock, 'getById').and.returnValue($q.defer().promise);
ctrl.call({}, accountsServiceMock);
expect(accountsServiceMock.getById.calls.any()).toBe(true);
expect(accountsServiceMock.getById.calls.count()).toBe(1);
});
it('should populate user data in the model', function () {
var deferred = $q.defer();
deferred.resolve(data);
spyOn(accountsServiceMock, 'getById').and.returnValue(deferred.promise);
var vm = ctrl.call({}, accountsServiceMock);
$rootScope.$apply();
expect(vm.user).toEqual(data);
});
});
Updated solution for the curious
function DetailAccountController (accountsService) {
'use strict';
var self = this;
self.user = null;
initialize();
return self;
/**
* Initialize the controller,
* & fetch detail for a single user.
*/
function initialize () {
accountsService && accountsService.getById('').then(function (res) {
self.user = res;
});
}
}
user = res affects local variable and has nothing to do with returned object.
It has to be either
accountsService && accountsService.getById('').then(function (res) {
angular.extend(user, res);
});
or
var obj = {
user: {}
};
initialize();
return obj;
function initialize () {
accountsService && accountsService.getById('').then(function (res) {
obj.user = res;
});
}
I want to test my typescript angular code with jasmine, but I get this error when I'm running it.
TypeError: 'undefined' is not an object (evaluating 'scope.LessonCtrl.statistic')
I'm trying to test this code:
export class LessonCtrl {
scope: angular.IScope;
statistic: Statistic;
constructor($scope) {
this.scope = $scope;
this.statistic = new Statistic();
this.scope.$on("timer-stopped", function(event, data) {
var scope: any = event.currentScope;
scope.LessonCtrl.statistic.calculateTypingSpeed(data.millis);
scope.LessonCtrl.statistic.setTime(data.millis);
});
}
}
With this:
var scope, rootScope, lessonCtrl;
beforeEach(() => inject(($rootScope) => {
scope = $rootScope.$new();
rootScope = $rootScope;
lessonCtrl = new Controllers.LessonCtrl(scope);
}));
it('on test', () => {
rootScope.$broadcast('timer-stopped', [{millis: 1000}]);
expect(true).toBe(true); // i will expect something else but i have errors
});
Can anyone help me with this?
You assign statistic to the context (this), not scope.LessonCtrl. By using an arrow function the context will be preserved inside the .$on callback
Arrow functions capture the this value of the enclosing context...
export class LessonCtrl {
scope: angular.IScope;
statistic: Statistic;
constructor($scope) {
this.scope = $scope;
this.statistic = new Statistic();
this.scope.$on("timer-stopped", (event, data) => {
var scope: any = event.currentScope;
this.statistic.calculateTypingSpeed(data.millis);
this.statistic.setTime(data.millis);
});
}
}
Currently, I am writing a test (using Jasmine) for a directive, and I suspect the link function is not being triggered.
The directive is as follows:
.directive('userWrapperUsername', [
'stringEntryGenerateTemplate',
'stringEntryGenerateLinkFn',
// UserWrapper username column
// Attribute: 'user-wrapper-username'
// Attribute argument: A UserWrapper object with a 'newData' key into an
// object, which contains a 'username' key holding the
// UserWrapper's username
function(stringEntryGenerateTemplate, stringEntryGenerateLinkFn) {
return {
template: stringEntryGenerateTemplate('username'),
restrict: 'A',
scope: true,
link: stringEntryGenerateLinkFn('userWrapperUsername', 'username')
};
}
])
So it makes use of 2 functions provided through factories, namely stringEntryGenerateTemplate and stringEntryGenerateLinkFn.
The stringEntryGenerateTemplate function takes in a string and returns a string.
The stringEntryGenerateLinkFn function, when called returns the actual link function. It mostly consists of event handlers so I shall simplify it to:
function stringEntryGenerateLinkFn(directiveName, key) {
return function(scope, element, attr) {
scope.state = {};
scope.userWrapper = scope.$eval(attr[directiveName]);
}
}
Here is how I use the directive:
<div user-wrapper-username="u"></div>
Here is my test case:
describe('UserWrapper Table string entry', function() {
var $scope
, $compile;
beforeEach(inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
}));
it('should be in stateDisplay if the value is non empty', function() {
var userWrapper = {
orgData: {
student: {
hasKey: true,
value: 'abcdef'
}
},
newData: {
student: {
hasKey: true,
value: 'abcdef',
changed: false
}
}
}
, key = 'student'
, elem
, elemScope;
$scope.userWrapper = userWrapper;
elem = $compile('<div user-wrapper-username="userWrapper"></div>')($scope);
elemScope = elem.scope();
expect(elemScope.userWrapper).toBe(userWrapper);
expect(elemScope.state).toEqual(jasmine.any(Object)); // this fails
});
});
So I get a test failure saying that elemScope.state is undefined. Recall that I had a scope.state = {}; statement and it should be executed if the link function is executed. I tried a console.log inside the link function and it is not executed as well.
So how do I trigger the link function?
Thanks!
It turns out that I have to initialize the module containing the factories stringEntryGenerateTemplate and stringEntryGenerateLinkFn, which is the same module that contains the userWrapperUsername directive by adding this into my test case:
beforeEach(module('userWrapper', function() {}));
where userWrapper is the name of the module.
So the test case becomes:
describe('UserWrapper Table string entry', function() {
var $scope
, $compile;
beforeEach(module('userWrapper', function() {}));
beforeEach(inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
}));
it('should be in stateDisplay if the value is non empty', function() {
var userWrapper = {
orgData: {
student: {
hasKey: true,
value: 'abcdef'
}
},
newData: {
student: {
hasKey: true,
value: 'abcdef',
changed: false
}
}
}
, key = 'student'
, elem
, elemScope;
$scope.userWrapper = userWrapper;
elem = $compile('<div user-wrapper-username="userWrapper"></div>')($scope);
elemScope = elem.scope();
expect(elemScope.userWrapper).toBe(userWrapper);
expect(elemScope.state).toEqual(jasmine.any(Object)); // this fails
});
});
This seems like quite a big oversight on my part. Hopefully this will help anyone facing a similar issue.