Angular 1.5 component unit testing with webpack - angularjs

I'm trying to test a component with controller, with some bindings:
class AppSpecificController {
constructor() {
this.text = this.initialText ? this.initialText : 'No initial text has been specified.';
}
}
export const AppSpecificComponent = {
bindings: {
'initialText': '<bindInitialText'
},
templateUrl: '/app/components/app-specific/app-specific.html',
controller: AppSpecificController,
controllerAs: 'appSpecific'
};
export default AppSpecificComponent;
In my unit test file I don't want to load the complete application, just the things I need. So I figured to mock a module or just create a new one named something with mock, add the component to that module, and load that module:
import {AppSpecificComponent} from './app-specific.component';
describe('AppSpecificComponent', () => {
let controller;
let scope;
let $componentController;
beforeEach(() => {
angular.module('mock-module', [])
.component('appSpecific', AppSpecificComponent);
// this fails
module('mock-module');
inject((_$componentController_, $rootScope) => {
scope = $rootScope.$new();
$componentController = _$componentController_;
});
controller = $componentController('appSpecific', {$scope: scope}, {initialText: 'Some text'});
});
it('should test something', () => {
expect(true).toBeTruthy();
});
});
Creating the module mock-module is fine, but loading it fails, and obviously injecting stuff in the not-so-much-loaded-module fails, as well as creating a controller on which I can start testing. It would be nice to be able to test the components individually, separate from the application in which it runs.
Just using the new operator for the class AppSpecificController doesn't work since then the bindings you receive from the component are not there:
// fails, no bindings available in controller
controller = new AppSpecificController();

I found the answer somewhere else on StackOverflow, not sure where anymore. The answer however I was looking for:
angular.mock.module($provide => {
$provide.controller('SomeController', () => { ... });
$provide.constant('someConstant', 'some constant');
});

Related

Receiving an unknown provider error while trying to unit test angular 1.5 components

So I'm unit testing a component I made with Angular 1.5. For security purposes, I can't copy and paste that code here, but I can give some mocks.
It's a simple component that has some basic selection action, with an binding event when something is selected.
So the code would look like this:
angular
.module( "myModule" )
.component( "myComponent", {
template: "Some random HTML Template",
bindings: {
onSelect: "&"
},
controller: function () {
var ctrl = this;
ctrl.$onInit = init;
ctrl.selection = selection;
function init() {} function selection() {
ctrl.onSelect(ctrl.selected) }
} } );
That's the basic component. I'm trying to unit test it, with a format such as this:
describe( "Component Test", function() {
beforeEach (module( "myModule") );
var ctrl, onSelectSpy;
beforeEach (inject( function( _$componentController_) {
ctrl = {}
onSelectSpy = jasmine.createSpy("onSelect");
var $componentController = _$componentController_;
ctrl = $componentController("myComponent", null, { onSelect:
onSelectSpy});
} ) );
it ("Controller defined", function() {
ctrl.$onInit();
expect ( ctrl ).toBeDefined();
});
} );
When I try running a test that looks very similar to this, I get this error:
Unknown provider: myComponentDirectiveProvider <- myComponentDirective
And it gives an angular url about the line where it fails.
Not sure why it's not getting defined and why this isn't working. I thought components were directives.
Using Angular 1.5.8 and Angular-mocks 1.5.8

Unit test angular 1.x components with Jasmine (using Typescript, Webpack)

I am writing an app using angular 1.6, typescript, webpack, karma and jasmine. I was able to create unit tests for angular services, but now I am having troubles for testing components. On SO(1) and (2) and on the net I found different examples (like this), but not a clear guide explaining how to test angular 1 components with the above technology set.
My component (HeaderComponent.ts):
import {IWeatherforecast} from '../models/weather-forecast';
import WeatherSearchService from '../search/weather-search.service';
import WeatherMapperService from '../common/mapping/weatherMapper.service';
export default class HeaderComponent implements ng.IComponentOptions {
public bindings: any;
public controller: any;
public controllerAs: string = 'vm';
public templateUrl: string;
public transclude: boolean = false;
constructor() {
this.bindings = {
};
this.controller = HeaderComponentController;
this.templateUrl = 'src/header/header.html';
}
}
export class HeaderComponentController {
public searchText:string
private weatherData : IWeatherforecast;
static $inject: Array<string> = ['weatherSearchService',
'$rootScope',
'weatherMapperService'];
constructor(private weatherSearchService: WeatherSearchService,
private $rootScope: ng.IRootScopeService,
private weatherMapperService: WeatherMapperService) {
}
public $onInit = () => {
this.searchText = '';
}
public searchCity = (searchName: string) : void => {
this.weatherSearchService.getWeatherForecast(searchName)
.then((weatherData : ng.IHttpPromiseCallbackArg<IWeatherforecast>) => {
let mappedData = this.weatherMapperService.ConvertSingleWeatherForecastToDto(weatherData.data);
sessionStorage.setItem('currentCityWeather', JSON.stringify(mappedData));
this.$rootScope.$broadcast('weatherDataFetched', mappedData);
})
.catch((error:any) => console.error('An error occurred: ' + JSON.stringify(error)));
}
}
The unit test:
import * as angular from 'angular';
import 'angular-mocks';
import HeaderComponent from '../../../src/header/header.component';
describe('Header Component', () => {
let $compile: ng.ICompileService;
let scope: ng.IRootScopeService;
let element: ng.IAugmentedJQuery;
beforeEach(angular.mock.module('weather'));
beforeEach(angular.mock.inject(function (_$compile_: ng.ICompileService, _$rootScope_: ng.IRootScopeService) {
$compile = _$compile_;
scope = _$rootScope_;
}));
beforeEach(() => {
element = $compile('<header-weather></header-weather>')(scope);
scope.$digest();
});
To me is not clear how to access the controller class, in order to test the component business logic. I tried injecting $componentController, but i keep getting the error "Uncaught TypeError: Cannot set property 'mock' of undefined", I think this is related to angular-mocks not properly injected.
Can anyone suggest an approach of solution or a site where to find further details about unit testing angular 1 components with typescript and webpack?
I was able to found a solution for my question. I post the edited code below, so others can benefit from it and compare the starting point (the question above) with the final code for the unit test(below, splitted in sections for the sake of explanation).
Test the component template :
import * as angular from 'angular';
import 'angular-mocks/angular-mocks';
import weatherModule from '../../../src/app/app.module';
import HeaderComponent, { HeaderComponentController } from '../../../src/header/header.component';
import WeatherSearchService from '../../../src/search/weather-search.service';
import WeatherMapper from '../../../src/common/mapping/weatherMapper.service';
describe('Header Component', () => {
let $rootScope: ng.IRootScopeService;
let compiledElement: any;
beforeEach(angular.mock.module(weatherModule));
beforeEach(angular.mock.module('templates'));
beforeEach(angular.mock.inject(($compile: ng.ICompileService,
_$rootScope_: ng.IRootScopeService) => {
$rootScope = _$rootScope_.$new();
let element = angular.element('<header-weather></header-weather>');
compiledElement = $compile(element)($rootScope)[0];
$rootScope.$digest();
}));
As for directives, also for components we need to compile the relative template and trigger a digest loop.
After this step, we can test the generated template code:
describe('WHEN the template is compiled', () => {
it('THEN the info label text should be displayed.', () => {
expect(compiledElement).toBeDefined();
let expectedLabelText = 'Here the text you want to test';
let targetLabel = angular.element(compiledElement.querySelector('.label-test'));
expect(targetLabel).toBeDefined();
expect(targetLabel.text()).toBe(expectedLabelText);
});
});
Test the component controller :
I created two mocked objects with jasmine.createSpyObj. In this way it is possible to create an instance of our controller and pass the mocked objects with the desired methods.
As the mocked method in my case was returning a promise, we need to use the callFake method from the jasmine.SpyAnd namespace and return a resolved promise.
describe('WHEN searchCity function is called', () => {
let searchMock: any;
let mapperMock: any;
let mockedExternalWeatherData: any;
beforeEach(() => {
searchMock = jasmine.createSpyObj('SearchServiceMock', ['getWeatherForecast']);
mapperMock = jasmine.createSpyObj('WeatherMapperMock', ['convertSingleWeatherForecastToDto']);
mockedExternalWeatherData = {}; //Here I pass a mocked POCO entity (removed for sake of clarity)
});
it('WITH proper city name THEN the search method should be invoked.', angular.mock.inject((_$q_: any) => {
//Arrange
let $q = _$q_;
let citySearchString = 'Roma';
searchMock.getWeatherForecast.and.callFake(() => $q.when(mockedExternalWeatherData));
mapperMock.convertSingleWeatherForecastToDto.and.callFake(() => $q.when(mockedExternalWeatherData));
let headerCtrl = new HeaderComponentController(searchMock, $rootScope, mapperMock);
//Act
headerCtrl.searchCity(citySearchString);
//Assert
expect(searchMock.getWeatherForecast).toHaveBeenCalledWith(citySearchString);
}));
});
});
Thanks for this post! I worked at the same time at the same problem and also found a solution. But this hero example doesn't require compiling the component (also no digest required) but uses the $componentController where also bindings can be defined.
The my-components module - my-components.module.ts:
import {IModule, module, ILogService} from 'angular';
import 'angular-material';
export let myComponents: IModule = module('my-components', ['ngMaterial']);
myComponents.run(function ($log: ILogService) {
'ngInject';
$log.debug('[my-components] module');
});
The hero component - my-hero.component.ts
import {myComponents} from './my-components.module';
import IController = angular.IController;
export default class MyHeroController implements IController {
public hero: string;
constructor() {
'ngInject';
}
}
myComponents.component('hero', {
template: `<span>Hero: {{$ctrl.hero}}</span>`,
controller: MyHeroController,
bindings: {
hero: '='
}
});
The hero spec file - my-hero.component.spec.ts
import MyHeroController from './my-hero.component';
import * as angular from 'angular';
import 'angular-mocks';
describe('Hero', function() {
let $componentController: any;
let createController: Function;
beforeEach(function() {
angular.mock.module('my-components');
angular.mock.inject(function(_$componentController_: any) {
$componentController = _$componentController_;
});
});
it('should expose a hero object', function() {
let bindings: any = {hero: 'Wolverine'};
let ctrl: any = $componentController('hero', null, bindings);
expect(ctrl.hero).toBe('Wolverine');
})
});
Note: It took some time to fix an error in testing the binding:
$compileProvider doesn't have method 'preAssignBindingsEnabled'
The reason was a version difference between angular and angular-mock. The solution was provide by: Ng-mock: $compileProvider doesn't have method 'preAssignBindingsEnabled`

Implementing component require property in Angular 1.5 components

I am having no joy with implementing require: {} property as part of an angular component. Allow me to demonstrate with an example I have.
This is the component/directive that supposed to fetch a list of judgements. Nothing very fancy, just a simple factory call.
// judgements.component.js
function JudgementsController(GetJudgements) {
var ctrl = this;
ctrl.Get = function () {
GetJudgements.get().$promise.then(
function (data) {
ctrl.Judgements = data.judgements;
}, function (error) {
// show error message
});
}
ctrl.$onInit = function () {
ctrl.Get();
};
}
angular
.module('App')
//.component('cJudgements', {
// controller: JudgementsController,
//});
.directive('cJudgements', function () {
return {
scope: true,
controller: 'JudgementsController',
//bindToController: true,
};
});
I am trying to implement component require property to give me access to ctrl.Judgements from the above component/directive as follows:
// list.component.js
function ListController(GetList, GetJudgements) {
var ctrl = this;
ctrl.list = [];
ctrl.Get = function () {
GetList.get().$promise.then(
function (data) {
ctrl.list = data.list;
}, function (error) {
// show error message
});
};
//ctrl.GetJudgements = function () {
// GetJudgements.get().$promise.then(
// function (data) {
// ctrl.Judgements = data.judgements;
// }, function (error) {
// // show error message
// });
//}
ctrl.$onInit = function () {
ctrl.Get();
//ctrl.GetJudgements();
};
}
angular
.module('App')
.component('cTheList', {
bindings: {
listid: '<',
},
controller: ListController,
controllerAs: 'ctrl',
require: {
jCtrl: 'cJudgements',
},
template: `
<c-list-item ng-repeat="item in ctrl.list"
item="item"
judgements="ctrl.Judgements"></c-list-item>
<!--
obviously the reference to judgements here needs to change
or even better to be moved into require of cListItem component
-->
`,
});
Nice and simple no magic involved. A keen reader probably noticed GetJudgement service call in the ListController. This is what I am trying to remove from TheList component using require property.
The reason? Is actually simple. I want to stop database being hammered by Judgement requests as much as possible. It's a static list and there is really no need to request it more than once per instance of the app.
So far I have only been successful with receiving the following error message:
Error: $compile:ctreq
Missing Required Controller
Controller 'cJudgements', required by directive 'cTheList', can't be found!
Can anyone see what I am doing wrong?
PS: I am using angular 1.5
PSS: I do not mind which way cJudgement is implemented (directive or component).
PSSS: If someone wonders I have tried using jCtrl: '^cJudgements'.
PSSSS: And multiple ^s for that matter just in case.
Edit
#Kindzoku posted a link to the article that I have read before posting the question. I hope this also helps someone in understanding $onInit and require in Angular 1.5+.
Plunker
Due to popular demand I made a plunker example.
You should use required components in this.$onInit = function(){}
Here is a good article https://toddmotto.com/on-init-require-object-syntax-angular-component/
The $onInit in your case should be written like this:
ctrl.$onInit = function () {
ctrl.jCtrl.Get();
};
#iiminov has the right answer. No parent HTML c-judgements was defined.
Working plunker.

How To Test route.navigate on Backbone View

I'm currently developing a Backbone.js application which uses the Mocha, Chai, and Sinon libraries for testing. I'm struggling to code the following test: When a user clicks a button it should redirect the user to home.
Here's how the view looks like:
events: {
'click #navigate-home': 'goHome'
},
initialize: function(options) {
this.router = options.router;
},
goHome: function(event) {
event.preventDefault();
this.router.navigate('home', { trigger: true });
}
And here's the test:
beforeEach(function() {
this.router = new Backbone.Router();
this.myView = new MyView({
router: this.router
});
this.myView.render();
});
afterEach(function() {
this.router = null;
this.myView = null;
});
it('redirects to home', sinon.test(function() {
var spy = sinon.spy();
this.router.on({
'home': spy
});
this.myView.$el.find('#navigate-home').trigger('click');
expect(spy.called).to.be.true;
}));
The code works as expected on the browser, but I can't get the test to pass. I would highly appreciate any help.
Based on my limited experience with Backbone and Unit Testing, I consider this kind of test as an invalid test case.
What you are trying to test in not actually your code but you are trying to add a test case for Backbone library. Similar test cases should actually be part of your Automation (Read about Selenium).
I think the correct way to write a Unit Test in your case would be:
it('should redirect to home', sinon.test(function() {
var spy = sinon.spy();
this.router.on({
'home': spy
});
// This is the difference
this.myView.goHome({preventDefault: function() { } });
expect(spy.called).to.be.true;
});

Missing injection for test (unkown provider)

I am trying to write unit tests for my Angular.js application but I cannot manage to inject what I need (it is not able to find a suitable provider).
Does anyone see what I missed?
Firefox 21.0 (Linux) filter staticList should convert static list object into its display value FAILED
Error: Unknown provider: staticListProvider <- staticList in /path/to/my-app/public/third-party/angular/angular.js (line 2734)
createInjector/providerInjector<#/path/to/my-app/public/third-party/angular/angular.js:2734
getService#/path/to/my-app/public/third-party/angular/angular.js:2862
createInjector/instanceCache.$injector<#/path/to/my-app/public/third-party/angular/angular.js:2739
getService#/path/to/my-app/public/third-party/angular/angular.js:2862
invoke#/path/to/my-app/public/third-party/angular/angular.js:2880
workFn#/path/to/my-app/test/lib/angular/angular-mocks.js:1778
angular.mock.inject#/path/to/my-app/test/lib/angular/angular-mocks.js:1764
#/path/to/my-app/test/unit/filtersSpec.js:19
#/path/to/my-app/test/unit/filtersSpec.js:16
#/path/to/my-app/test/unit/filtersSpec.js:3
The application:
angular.module('myApp', ['myAppFilters', 'ui.bootstrap', '$strap.directives']).
// Some other stuff
The filters:
"use strict";
angular.module('myAppFilters', []).
filter('staticList', function () {
return function (listItem) {
if (!listItem) {
return '';
}
return listItem.value;
};
});
The test:
'use strict';
describe('filter', function () {
beforeEach(angular.module('myAppFilters'));
describe('staticList', function () {
it('should convert static list object into its display value',
inject(function (staticList) {
expect(undefined).toBe('');
expect({key: 'A', value: 'B'}).toBe('B');
}));
});
});
The Karma configuration:
basePath = '../';
files = [
JASMINE,
JASMINE_ADAPTER,
'public/third-party/jquery/*.js',
'public/third-party/angular/angular.js',
'public/third-party/angular/i18n/angular-*.js',
'public/third-party/moment/moment.min.js',
'public/third-party/moment/moment-*.js',
'public/js/**/*.js',
'test/lib/**/*.js',
'test/unit/**/*.js'
];
colors = true;
autoWatch = true;
browsers = ['Firefox'];
junitReporter = {
outputFile: 'test_out/unit.xml',
suite: 'unit'
};
If anybody wants to see the full code, the application repository is here: https://github.com/adericbourg/GestionCourrier
Thanks a lot,
Alban
In your inject code
it('should convert static list object into its display value',
inject(function (staticList) {
expect(undefined).toBe('');
expect({key: 'A', value: 'B'}).toBe('B');
}));
replace "inject(function (staticList)" with "inject(function (staticListFilter)". This is some random convention angular is following. You can check comments on this page for more info http://docs.angularjs.org/tutorial/step_09
I faced similar problem, but in my case my filter name contained suffix 'Filter' which caused the same injection problem.
.filter('assetLabelFilter', function(){
return function(assets, selectedLabels){
// Implementation here
};
});
I was finally able to solve the problem by manually injecting the filter to my test
'use strict';
describe('assetLabelFilter', function() {
beforeEach(module('filters.labels'));
var asset1 = {labels: ['A']};
var asset2 = {labels: ['B']};
var asset3 = {labels: []};
var asset4 = {labels: ['A', 'B']};
var assets = [asset1, asset2, asset3, asset4];
var assetLabelFilter;
beforeEach(inject(function($filter) {
assetLabelFilter = $filter('assetLabelFilter');
}));
it('should return only assets with selected label', function() {
var selectedLabels = ['B'];
expect(assetLabelFilter(assets, selectedLabels)).toEqual([asset2, asset4]);
});
});
The nice answer above made me realize that in order to use the angular tutorial way:
it('should ', inject(function(assetLabelFilter) {
var selectedLabels = ['B'];
expect(assetLabelFilter(assets, selectedLabels)).toEqual([asset2, asset4]);
}));
The filter name can't have the suffix 'Filter' because it is magic word as mentioned in the answer above.
To demonstrate the scenario I also created a Plunker

Resources