Unit testing the output of $sce.trustAsHtml in Angular - unit-testing

I am writing a REST app in Angular and I want to write unit tests for it (of course!). I have a controller which gets a list of blog posts from a REST service in json and puts the summaries into the $scope, so I can display them in the view.
At first the blog posts were just displaying as text ie <p>Blog body</p>, rather than rendering as parsed HTML, until I discovered that you can use ng-bind-html in conjunction with the $sce service. This now works fine in terms of displaying the blog posts correctly.
The problem arises when unit testing. I am trying to mock a json response with some HTML and then test that my controller is correctly dealing with the HTML. Here is my code:
Controller
.controller( 'HomeCtrl', function HomeController( $scope, $http, $sce ) {
$scope.posts = {};
$http.get('../drupal/node.json').success(function (data) {
var posts;
posts = data.list;
for(var i = 0; i < posts.length; i ++) {
posts[i].previewText = $sce.trustAsHtml(posts[i].body.summary);
posts[i].created = posts[i].created + '000'; // add milliseconds so it can be properly formatted
}
$scope.posts = posts;
});
})
unit test
describe('HomeCtrl', function() {
var $httpBackend, $rootScope, $sce, createController;
beforeEach(inject(function ($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
$sce = $injector.get('$sce');
createController = function() {
return $controller('HomeCtrl', {
'$scope': $rootScope
});
};
}));
it('should get a list of blog posts', function() {
var rawResponse = {
"list": [
{
"body": {
"value": "\u003Cp\u003EPost body.\u003C\/p\u003E\n",
"summary": "\u003Cp\u003ESummary.\u003C\/p\u003E\n"
},
"created": "1388415860"
}
]};
var processedResponse = [{
"body": {
"value": "\u003Cp\u003EPost body.\u003C\/p\u003E\n",
"summary": "\u003Cp\u003ESummary.\u003C\/p\u003E\n"
},
"created": "1388415860000",
previewText: $sce.trustAsHtml("\u003Cp\u003ESummary.\u003C\/p\u003E\n")
}];
$httpBackend.when('GET', '../drupal/node.json').respond(rawResponse);
$httpBackend.expectGET("../drupal/node.json").respond(rawResponse);
var homeCtrl = createController();
expect(homeCtrl).toBeTruthy();
$httpBackend.flush();
expect($rootScope.posts).toEqual(processedResponse);
});
});
When I run the above through the Karma test runner, I get the following response:
Chrome 31.0.1650 (Windows) home section HomeCtrl should get a list of blog posts FAILED
Expected [ { body : { value : '<p>Post body.</p>
', summary : '<p>Summary.</p>
' }, created : '1388415860000', previewText : { $$unwrapTrustedValue : Function } } ] to equal [ { body
: { value : '<p>Post body.</p>
', summary : '<p>Summary.</p>
' }, created : '1388415860000', previewText : { $$unwrapTrustedValue : Function } } ].
I suspect the problem is due to the fact that $sce.trustAsHtml returns an object containing a function, rather than a string.
My question is, firstly, am I approaching this problem in the correct way?
Secondly, if so, how should I go about testing the output of $sce.trustAsHtml?

Since the answer given by michael-bromley didn't work for me I want to point out another solution. In my case I was using a filter that wraps each occurrence of a string in another string with a span that has a class of 'highlight'. In other words, I want words to be highlighted. Here is the code:
angular.module('myModule').filter('highlight', function ($sce) {
return function (input, str) {
return $sce.trustAsHtml((input || '').replace(new RegExp(str, 'gi'), '<span class=\"highlighted\">$&</span>'));
};
});
I use the $sce service to trust the resulting value as HTML. To test this I need to use the $$unwrapTrustedValue function on the resulting value to get my test working:
it('01: should add a span with class \'highlight\' around each mathing string.', inject(function ($filter) {
// Execute
var result = $filter('highlight')('this str contains a str that will be a highlighted str.', 'str');
// Test
expect(result.$$unwrapTrustedValue()).toEqual('this <span class="highlighted">str</span> contains a <span class="highlighted">str</span> that will be a highlighted <span class="highlighted">str</span>.');
}));
UPDATE:
As #gugol kindly pointed out it is preferred not to use Angular internal methods like $$unwrapTrustedValue. A better approach is to use the public getTrustedHtml method on the $sce service. Like so:
it('01: should add a span with class \'highlight\' around each mathing string.', inject(function ($sce, $filter) {
// Execute
var result = $filter('highlight')('this str contains a str that will be a highlighted str.', 'str');
// Test
expect($sce.getTrustedHtml(result)).toEqual('this <span class="highlighted">str</span> contains a <span class="highlighted">str</span> that will be a highlighted <span class="highlighted">str</span>.');
}));

You have to disable $sce using its provider before each test.
When $sce is disabled all $sce.trust* methods just return original value instead of a wrapper function.
beforeEach(module(function ($sceProvider) {
$sceProvider.enabled(false);
}));
it('shall pass', inject(function($sce){
expect($sce.trustAsHtml('<span>text</span>')).toBe('<span>text</span>');
}));
In your particular example just do this:
describe('HomeCtrl', function() {
var $httpBackend, $rootScope, $sce, createController;
beforeEach(module(function ($sceProvider) {
$sceProvider.enabled(false);
}));
// rest of the file
});

I discovered that you can use $sce.getTrusted which will return the value originally passed to $sce.trustAsHtml, in this case a string containing HTML, which you can then test for equality in the usual way.
So my test now looks like this:
it('should create a previewText property using $sce.trustAsHtml', function() {
// confirms that it is an object, as should be the case when
// it has been through $sce.trustAsHtml
expect(typeof result.previewText === 'object').toEqual(true);
expect($sce.getTrusted($sce.HTML, result.previewText))
.toEqual('<p>Original HTML content string</p>');
});

Another option is to use the getTrustedHtml() function to get the html string value from $$unwrapTrustedValue.
vm.user.bio = $sce.getTrustedHtml(vm.user.bio);

Related

How can I test computed properties in VueJS?

I'm using VueJS from Vue CLI. So all my components are in .vue format.
In one of my components, I have an array called fields in the data section.
//Component.vue
data() {
return {
fields : [{"name" : "foo", "title" : "Foosteria"}, {"name" : "bar", "title" : "Barrista"}]
}
}
I have a computed property that is a subset of fields
//Component.vue
computed : {
subsetOfFields () {
// Something else in component data determines this list
}
}
I've set up all of my unit tests in jasmine like this and they work fine.
//Component.spec.js
import Vue from 'vue'
import MyComponent from 'Component.vue'
describe("Component test", function() {
var myComponentVar = new Vue(MyComponent);
var vm = myComponentVar.$mount();
beforeEach(function() {
vm = myComponentVar.$mount();
);
afterEach(function() {
vm = myComponentVar.$destroy();
});
it("First spec tests something", function() {
...
});
});
For everything else, doing something inside the spec, then running assertions on the data objects works just fine. However, running an assertion on subsetOfFields always returns an empty array. Why so? What should I do, in order to be able to test it?
FYI, I even tried nesting the spec inside another describe block and then adding a beforeEach which initializes the fields array. It did not work.
However, initializing fields inside the generic beforeEach function worked. But I don't want to initialize the fields array with that mock data for the other specs.
I came across this link that talks about testing and the section you'll need to look at is the Vue.nextTick(...) section
https://alligator.io/vuejs/unit-testing-karma-mocha/
The block I'm talking about is below:
import Vue from 'vue';
// The path is relative to the project root.
import TestMe2 from 'src/components/TestMe2';
describe('TestMe2.vue', () => {
...
it(`should update when dataText is changed.`, done => {
const Constructor = Vue.extend(TestMe2);
const comp = new Constructor().$mount();
comp.dataProp = 'New Text';
Vue.nextTick(() => {
expect(comp.$el.textContent)
.to.equal('New Text');
// Since we're doing this asynchronously, we need to call done() to tell Mocha that we've finished the test.
done();
});
});
});

Angular JS directive "#" $scope in-$digestion

Given:
var myApp = angular.module('myApp', []);
myApp.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
classToAdd: '#'
},
template:
'<div class="{{classToAdd}}"></div>'
};
});
I'm testing a spec where classToAdd is being statically coded in the template:
<my-directive class-to-add="foo"></my-directive>
and the classToAdd attribute is only being recognized if I $digest $rootScope, and not $scope.
Why this is the case?
Working fiddle.
The reason the shown fiddle was failing, is that "foo" is bound to the $rootScope, not the local $scope.
The solution is to set the scope variable, interpolate it using {{foo}} (since we're using "#" in the isolate scope)
it('should bind to the class-to-add attribute when we $digest $scope', function () {
// Arrange
template = '<my-directive class-to-add="{{foo}}"></my-directive>';
compiledDirective = $compile(template)($scope);
directiveEl = compiledDirective.find('div');
$scope.foo = "bar"
// Act
$scope.$digest();
// Assert
expect(directiveEl.hasClass('bar')).toBe(true);
});
you can write your code like this:
angular.module('app',[])
.controller('ctrl',['$scope',function($scope){
$scope.myClass="myClass";
}])
.directive('myDirective',function(){
return {
restrict:'E',
scope:{ classToAdd: '#' },
transclude:true,
replace: true,
template:'<div class="{{classToAdd}}" ng-transclude></div>',
link: function(scope, iElement, iAttrs){
console.log(scope);
console.log(iAttrs);
}
}
})
and here is a working DEMO
notice the console for logs and under the hood stuff.

AngularJS controller unit testing with $http.get() on load

I'm having difficulties understanding how to resolve the problem of the unit testing the controller, which makes a GET call during initialisation.
When testing the controller method, that performs the POST request, because of the initial GET call I'm getting the following error in my tests:
Error: Unexpected request: GET
The main part of the controller looks like this:
.controller('someController', function($scope, $http, $log) {
$scope.posts = [];
$scope.content = '';
$scope.read = function() {
$http.get('/read.php')
.success(function(data) {
$scope.posts = data;
})
.error(function(data, status, headers, config) {
throw new Error('Something went wrong with reading data');
});
};
$scope.read();
$scope.write = function() {
$http({
method: 'POST',
url: '/write.php',
data: "content=" + $scope.content,
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
})
.success(function(data) {
$scope.posts.push({ id : data.id, task : $scope.content })
})
.error(function(data, status, headers, config) {
throw new Error('Something went wrong with writing data');
});
};
});
As you can see I'm calling the read() method right after its definition - so that all records are fetched from the database on page load.
I've tried the same with the .config() or other service, but obviously the result is the same.
Now - my test is as follow:
describe('someController tests', function() {
var $scope,
$http,
$httpBackend,
$log;
beforeEach(function() {
module('myApp');
inject(function($rootScope, _$http_, _$httpBackend_, _$log_) {
$scope = $rootScope.$new();
$http = _$http_;
$httpBackend = _$httpBackend_;
$log = _$log_;
});
$httpBackend.expectGET("/mod/read.php").respond({});
});
it('should add new record', inject(function($controller) {
$controller('ToDoController', {
$scope : $scope,
$http : $http,
$log : $log
});
$scope.posts = [];
$scope.content = 'Some content';
$httpBackend
.whenPOST('/write.php')
.respond({ id : 1, content : $scope.content });
$scope.write();
$httpBackend.flush();
expect($scope.posts.length).toBe(1);
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
}));
});
Now - there are a few questions I have about this code.
Is calling the method within the controller on page load considered a good practice? I know that if I used ng-init within the document to call it - this would solve all my problems, but I just don't like this approach.
In the beforeEach() method of the test I'm calling:
$httpBackend.expectGET("/mod/read.php").respond({});
to reflect the call of the read() method during initialisation, but again - I'm not sure this is the right thing to do here. I know that if I remove it - the 'should add new record' test will fail with the error mentioned above.
I know that afterEach() should be placed outside of the it() block, but if I do this - it will cause the same problem, but error will apply to all of the tests within this describe block. Is putting it inside of the it() block considered a bad practice / incorrect?
How would I perform the read() test - if the call is already triggered in the beforeEach() loop?
Lastly - what would your suggestion be to improve / rewrite the controller and the test to make sure it works its best?
I'm pretty new to the TDD - so I'm sure it's just my limited understanding of the topic, but would appreciate some constructive help.

Angularjs Unit Testing: Am I doing it right?

I started to write unit tests for my angular app.
However it seems to me that I use a lot of boilerplate code to init and test the controller.
In this Unit Test I want to test if a model from the scope is sent to the Api when I execute a function.
I needed 20 lines of code for this. This makes it inconvenient to write unit tests that do only one thing.
Do you have any tips on getting the code size to a smaller chunk?
This is my current unit test:
'use strict';
describe('controllers', function(){
beforeEach(module('kronos'));
describe('CustomerSignupCtrl', function() {
it('should send customer to Api on submit', inject(function($controller) {
var scope = {};
var $location = {};
var Api = {
signupCustomer: function(customer) {
expect(customer).toEqual({attrs: "customerdata"});
return {
success: function() { return this; },
error: function() { return this; }
};
}
};
var ctrl = $controller('CustomerSignupCtrl', {
$scope: scope,
$location: location,
Api: Api});
scope.customer = {attrs: "customerdata"};
scope.signup();
}));
});
});
What I don't like in particular are the following points
I need to init the all dependencies and it doesn't matter if I use them or not
The Api returns a promise that I only need because the controller is expecting the promise
I need to init the controller.
How can I make this code shorter and more explicit?
Edit: I just noticed I can ignore the $location Service for this unit test. Great
Edit2: I found out about angular-app, which serves as a good practice example app. There you can find specs with jasmine, which are really nice written.
Use another beforeEach method in your describe scope to set up scope, $location, controller etc, then just change them in your test as you need to. Js is dynamic so all should be fine.
You can also extract each object that you set up into a function so that you can reinitialise them in a test if you need to.
describe('controllers', function(){
beforeEach(module('kronos'));
describe('CustomerSignupCtrl', function() {
var controller, scope, $location, Api;
beforeEach(function(){
scope = {};
$location = {};
Api = {
signupCustomer: function(customer) {
expect(customer).toEqual({attrs: "customerdata"});
return {
success: function() { return this; },
error: function() { return this; }
};
}
};
controller = makeController();
})
function makeController(){
inject(function($controller){
controller = $controller('CustomerSignupCtrl', {
$scope: scope,
$location: location,
Api: Api});
});
}
it('should send customer to Api on submit', function() {
scope.customer = {attrs: "customerdata"};
scope.signup();
});
});
});
You can not shorten your code much. Things like initialization, mocking and assertion have to be done at some place. But you can improve the readability of your code by decoupling initialization and test code. Something like this:
describe('CustomerSignupCtrl', function(){
var controller, scope, location, api;
beforeEach(module('kronos'));
// initialization
beforeEach(inject(function($controller, $rootScope, $location, Api){
scope = $rootScope.$new();
location = $location;
api = Api;
controller = $controller('CustomerSignupCtrl', {
$scope: scope, $location: location, Api: api});
}));
// test
it('should send customer to Api on submit', function() {
scope.customer = {attrs: "customerdata"};
spyOn(api,'signupCustomer').andCallFake(function(customer) {
return {
success: function() { return this; },
error: function() { return this; }
};
});
scope.signup();
expect(api.signupCustomer).toHaveBeenCalledWith(scope.customer);
});
});

AngularJS : Need help to unit test a factory with promise

I have a factory that uses a promise to resolve a json file.
It should only resolve this file the first time and return the result when called again.
Here is the factory
app.factory('getServerConfig', function ($q, $http, serverConfig) {
return function (fileName) {
var deferred = $q.defer();
if (serverConfig.loaded) {
deferred.resolve("alreadyLoaded");
} else {
$http.get(fileName).then(function (result) {
serverConfig.setConfig(result.data);
deferred.resolve("loaded");
}, function (result) {
deferred.reject();
});
}
return deferred.promise;
};
})
And how I test it :
it('should say hallo to the World', inject(function(getServerConfig) {
var promiseResult;
getServerConfig("server-config.json").then(function (result) {
promiseResult = result;
});
rootScope.$apply();
expect(promiseResult).toBe('loaded');
}));
Unfortunately, it looks likes promiseResult is never set.
Here is a plunker with the code : http://plnkr.co/edit/uRPCjuUDkqPRAv07G5Nx?p=preview
The problem is that $httpBackend demands a flush() (take a look at Flushing HTTP requests), so you can mimic $http asynchronous behavior in your test.
To solve it, store a reference of $httpBackend (or inject it again) and call $httpBack.flush() after the request has been made.
Look:
it('should say hallo to the World', inject(function(getServerConfig, $httpBackend) {
var promiseResult;
getServerConfig("server-config.json").then(function (result) {
promiseResult = result;
});
$httpBackend.flush();
expect(promiseResult).toBe('loaded');
}));
If you gonna use $httpBackend in most of your specs, consider storing it in a local variable, and set it in beforeEach.