UI-Router 1.X - How to test $transition service hooks with karma? - angularjs

I'm trying to migrate to ui-router 1.0.5 and have done most of the work but there are no examples of how to test new transition hooks that replaced $stateChangeXXX events listeners.
Code before:
scope.$on('$stateChangeSuccess', this.hideSpinner_.bind(this));
After:
this.transitions_ is a $transitions service exposed by ui-router
this.transitions_.onSuccess({}, this.hideSpinner_.bind(this));
Before I was able to test it by using scope.$broadcast($stateChangeSuccess) and then scope.$apply(). This worked with ui-router 0.x:
expect(ctrl.loading).toBe(true);
expect(ctrl.showLoadingSpinner).toBe(true);
// when
scope.$broadcast('$stateChangeSuccess');
scope.$apply();
// then
expect(ctrl.loading).toBe(false);
expect(ctrl.showLoadingSpinner).toBe(false);
Any idea how to rewrite tests to work with new version of ui-router?

Well,
I faced exactly the same problem migrating from ui-router 0.3.x to 1.0.5
App
Before :
scope.$on('$stateChangeSuccess', someFunction);
After :
$transitions.onSuccess( {}, someFunction);
Tests
Before :
scope.$broadcast('$stateChangeSuccess');
scope.$apply();
After :
I call directly the callback function with a mocked transition because I just want to test what the callback does (and here it only needs trans.$to().name and trans.$from().name) :
var mockTransition = {
$to: function() { return {name: 'foo'}; },
$from: function() { return {name: 'bar'}; }
};
service.someFunction(mockTransition);
$scope.$digest();
And in some places in my tests, I want to simulate the whole transition process so I do a real one, that way the events are properly called :
it('should handle transitions error properly when trying to make transition to an abstract state', function (done) {
spyOn(console, 'error');
spyOn(transitions, 'onError');
transitions.onError({}, function(transition) {});
$stateTest.transitionTo("c3.app.offre").then(function() {
}, function() {
expect(console.error).toHaveBeenCalled();
expect(transitions.onError).toHaveBeenCalled();
done();
});
scope.$apply();
});

I've got the same question. And I chose a stupid solution that is broadcasting the 'stateChangeStart' event in a onStart hook:
$transitions.onStart({}, function($transition$){
$scope.$broadcast('$myStateChangeStart', $transition$.to(), $transition$.params('to'), $transition$.from(), $transition$.params('from'), $transition$.options(), $transition$)
})
then I do not need to change so much of the legacy code, just handling some 'concurrency' logic.
Hope it makes sense. If you've got a good practice, pls tell me.

Related

How to test transition hooks with ui-router 1.0.x

I am migrating to the latest stable release of ui-router and am making use of the $transitions life cycle hooks to perform certain logic when certain state names are being transitioned to.
So in some of my controllers I have this kinda thing now:
this.$transitions.onStart({ }, (transition) => {
if (transition.to().name !== 'some-state-name') {
//do stuff here...
}
});
In my unit tests for the controller, previously I would broadcast a state change event on the $rootScope with the certain state names as the event args to hit the conditions I needed to test.
e.g.
$rootScope.$broadcast('$stateChangeStart', {name: 'other-state'}, {}, {}, {});
Since these state events are deprecated, whats the correct way to now trigger the $transitions.onStart(...) hooks in the tests?
I have tried just calling $state.go('some-state-name') in my tests but I can never hit my own logic within the transition hook callback function. According to the docs here, calling state.go programatically should trigger a transition, unless I am misreading?
Has anyone else managed to get unit tests for transition hooks in their controllers working for the new ui-router 1.0.x?
Full example of my controller code using a transition hook:
this.$transitions.onSuccess({ }, (transition) => {
this.setOpenItemsForState(transition.to().name);
});
test spec:
describe('stateChangeWatcher', function() {
beforeEach(function() {
spyOn(vm, 'setOpenItemsForState').and.callThrough();
});
it('should call the setOpenItemsForState method and pass it the state object', function() {
$state.go('home');
$rootScope.$apply();
expect(vm.setOpenItemsForState).toHaveBeenCalledWith('home');
});
});
My spy is never getting hit, when running the application locally this hook does get invoked as expected, so it must be something I have got setup incorrectly in my tests. Is there something extra I need to make the transition succeed in the test, since I am hooking into the onSuccess event?
Thanks
UPDATE
I raised this in the ui-router room on gitter and one of the repo contributors came back to me suggesting I check the call to $state.go('home') in my tests actually ran by adding expect($state.current.name).toBe('home'); in my test spec.
This does pass for me in my test, but I am still unable to hit the call to my function in the transition hook callback:
I'm unsure how to proceed on this, other than installing the polyfill for the legacy $stateChange events so I can use my previous code, but I'd rather not do this and figure out the proper way to test $transition hooks.
UPDATE 2
Following estus' answer, I have now stubbed out the $transitions service and also refactored my transition hook handler into a private named function in my controller:
export class NavBarController {
public static $inject = [
'$mdSidenav',
'$scope',
'$mdMedia',
'$mdComponentRegistry',
'navigationService',
'$transitions',
'$state'
];
public menuSection: Array<InterACT.Interfaces.IMenuItem>;
private openSection: InterACT.Interfaces.IMenuItem;
private openPage: InterACT.Interfaces.IMenuItem;
constructor(
private $mdSidenav,
private $scope,
private $mdMedia,
private $mdComponentRegistry,
private navigationService: NavigationService,
private $transitions: any,
private $state
) {
this.activate();
}
private activate() {
this.menuSection = this.navigationService.getNavMenu();
if (this.isScreenMedium()) {
this.$mdComponentRegistry.when('left').then(() => {
this.$mdSidenav('left').open();
});
}
this.setOpenItemsForState(this.$state.$current.name);
this.$transitions.onSuccess({ }, this.onTransitionsSuccess);
}
private onTransitionsSuccess = (transition) => {
this.setOpenItemsForState(transition.to().name);
}
private setOpenItemsForState(stateName: string) {
//stuff here...
}
}
Now in my test spec I have:
describe('Whenever a state transition succeeds', function() {
beforeEach(function() {
spyOn(vm, 'setOpenItemsForState').and.callThrough();
$state.go('home');
});
it('should call the setOpenItemsForState method passing in the name of the state that has just been transitioned to', function() {
expect($transitions.onSuccess).toHaveBeenCalledTimes(1);
expect($transitions.onSuccess.calls.mostRecent().args[0]).toEqual({});
expect($transitions.onSuccess.calls.mostRecent().args[1]).toBe(vm.onTransitionsSuccess);
});
});
These expectations pass, but Im still not able to hit my inner logic in my named hook callback onTransitionsSuccess function that make a call to setOpenItemsForState
What am I doing wrong here?
UPDATE 3
Thanks again to estu, I was forgetting I can just call my named transition hook function is a separate test:
describe('and the function bound to the transition hook callback is invoked', function(){
beforeEach(function(){
spyOn(vm, 'setOpenItemsForState');
vm.onTransitionsSuccess({
to: function(){
return {name: 'another-state'};
}
});
});
it('should call setOpenItemsForState', function(){
expect(vm.setOpenItemsForState).toHaveBeenCalledWith('another-state');
});
});
And now I get 100% coverage :)
Hopefully this will serve as a good reference to others who may be struggling to figure out how to test their own transition hooks.
A good unit test strategy for AngularJS routing is to stub a router entirely. Real router prevents units from being efficiently tested and provides unnecessary moving parts and unexpected behaviour. Since ngMock behaves differently than real application, such tests zcan't be considered proper integration tests either.
All router services in use should be stubbed. $stateProvider stub should reflect its basic behaviour, i.e. it should return itself on state call and should return $state stub on $get call:
let mockedStateProvider;
let mockedState;
let mockedTransitions;
beforeEach(module('app'));
beforeEach(module(($provide) => {
mockedState = jasmine.createSpyObj('$state', ['go']);
mockedStateProvider = jasmine.createSpyObj('$stateProvider', ['state', '$get']);
mockedStateProvider.state.and.returnValue(mockedStateProvider);
mockedStateProvider.$get.and.returnValue(mockedState);
$provide.provider('$state', function () { return mockedStateProvider });
}));
beforeEach(module(($provide) => {
mockedTransitions = jasmine.createSpyObj('$transitions', ['onStart', 'onSuccess']);
$provide.value('$transitions', mockedTransitions);
}));
A test-friendly way is to provide bound methods as callbacks instead of anonymous functions:
this.onTransitionStart = (transition) => { ... };
this.$transitions.onStart({ }, this.onTransitionStart);
Then stubbed methods can be just tested that they were called with proper arguments:
expect($transitions.onStart).toHaveBeenCalledTimes(1);
$transitions.onStart.mostRecent().args[0].toEqual({});
$transitions.onStart.mostRecent().args[1].toBe(this.onTransitionStart);
A callback function can be tested directly by calling it with expected arguments. This provides full coverage yet leaves some place for human error, so unit tests should be backed up with integration/e2e tests with real router.

How to unit test a function in a component that calls method to parent component

Hello stackoverflow community.
I am working on an Angular project (1.5.6), using a component structure and currently writing some unit tests. I am still learning a lot about unit tests – especially in relation with Angular – and was hoping I can ask you for help for the following issue:
I try to test a component, that receives a callback method from it's parent component. I am trying to mock the method foo (see below the code example). And unfortunately does this method call the parent controller.
So when I try to test it, it complains, that the method is undefined. Then I thought I could mock it with spyOn, but then I get the error Error: <spyOn> : foobar() method does not exist
So I think I am unable to mock that method.
Module:
angular.module("myApp")
.component("sample", {
"templateUrl": "components/sample/sample.html",
"controller": "SampleController",
"controllerAs": "sampleCtrl",
"bindings": {
"config": "<",
"foobar": "&"
}
})
.controller("SampleController",
["$scope",
function($scope) {
this.isActive = true;
this.foo = function() {
// do stuff
this.isActive = false;
// also do
this.foobar();
};
}
);
Unit Test
describe("Component: SampleComponent", function() {
beforeEach(module("myApp"));
var sampleComponent, scope, $httpBackend;
beforeEach(inject(function($componentController, $rootScope, _$httpBackend_) {
scope = $rootScope.$new();
sampleComponent = $componentController("sample", {
"$scope": scope
});
$httpBackend = _$httpBackend_;
}));
it("should do set isActive to false on 'foo' and call method...", function() {
spyOn(sampleComponent, "foobar")
expect(sampleComponent.isActive).toBe(true);
expect(sampleComponent).toBe("");
expect(sampleComponent.foobar).not.toHaveBeenCalled();
sampleComponent.foo();
expect(sampleComponent.foobar).toHaveBeenCalled();
expect(sampleComponent.foobar.calls.count()).toBe(1);
expect(sampleComponent.isActive).toBe(false);
});
});
I hope I didn't add any bugs to this, but this above is approximately what I am trying to do. Any suggestions are welcome and if the approach is wrong or more information needed, please let me know!
After the help from #estus (see comments in question) - I learned that I can use createSpy to resolve this issue.
it("should do set isActive to false on 'foo' and call method...", function() {
sampleComponent.foobar = jasmine.createSpy();
expect(sampleComponent.isActive).toBe(true);
expect(sampleComponent).toBe("");
expect(sampleComponent.foobar).not.toHaveBeenCalled();
sampleComponent.foo();
expect(sampleComponent.foobar).toHaveBeenCalled();
expect(sampleComponent.foobar.calls.count()).toBe(1);
expect(sampleComponent.isActive).toBe(false);
});
Some additional sources I used were:
http://angular-tips.com/blog/2014/03/introduction-to-unit-test-spies/
How does the createSpy work in Angular + Jasmine?

AngularJS: jQuery functions in jqLite

I just want to ask for some help with converting the following code from jQuery to jqLite (angular jQuery):
$(window).on("load", function() {
setTimeout(function(){
#some funcs
}, 100)
});
Thanks in advance.
Use this:
angular.element(document).ready(function () {
// your code here
});
The answer to that question depends on the context and use case and how it relates to the AngularJS framework and the phases of the app.
To start something in the AngularJS run phase:
app.run(function($timeout) {
$timeout(function() {
//Startup code
},100);
});
To start something in an AngularJS service:
app.service("something", function($timeout) {
$timeout(function() {
//Startup code
},100);
});
Of course the $timeout may not be necessary.
Or to start third-party code before bootstrapping AngularJS:
angular.element(function() {
//Third-party startup code
angular.bootstrap(document,['myApp']);
});
The choice really depends on the context and how the third-party code interacts with the AngularJS framework.

AngularJS with SignalR

I am playing with Angular and SignalR, I have tried to create a service which will act as a manager.
dashboard.factory('notificationsHub', function ($scope) {
var connection;
var proxy;
var initialize = function () {
connection = $.hubConnection();
proxy = connection.createHubProxy('notification');
proxy.on('numberOfIncidents', function (numOfIncident) {
console.log(numOfIncident);
$scope.$emit('numberOfIncidents', numOfIncident);
});
connection.start()
.done(function() {
console.log('Connected');
})
.fail(function() { console.log('Failed to connect Connected'); });
};
return {
initialize: initialize
};
});
however I get the error Error: Unknown provider: $scopeProvider <- $scope <- notificationsHub.
How can I use pubsub to pass all the notifications to the controllers? jQuery maybe?
$scope does not exist in this context as that's something injected when a controller is created and a new child scope is made. However, $rootScope is available at the time you need.
Also, be aware $emit() goes upward and your controller scopes wont see it. You would either need to switch to $broadcast() so the event goes downwards or inject $rootScope as well to the controllers you want to be able to subscribe to 'numberOfIncidents'
Check out the angular docs and a useful wiki on scopes.
Here is a great example showing how to wrap the proxy in a service and use $rootScope for event pub/sub.
http://sravi-kiran.blogspot.com/2013/09/ABetterWayOfUsingAspNetSignalRWithAngularJs.html
As already noted in johlrich's answer, $scope is not avaliable inside proxy.on. However, just switching to $rootScope will most likely not work. The reason for this is because the event handlers regisrered with proxy.on are called by code outside the angular framework, and thus angular will not detect changes to variables. The same applies to $rootScope.$on event handlers that are triggered by events broadcasted from the SignalR event handlers. See https://docs.angularjs.org/error/$rootScope/inprog for some more details.
Thus you want to call $rootScope.$apply() from the SignalR event handler, either explicitly
proxy.on('numberOfIncidents', function (numOfIncident) {
console.log(numOfIncident);
$scope.$apply(function () {
$rootScope.$emit('numberOfIncidents', numOfIncident);
});
});
or possibly implicitly through $timeout
proxy.on('numberOfIncidents', function (numOfIncident) {
console.log(numOfIncident);
$timeout(function () {
$rootScope.$emit('numberOfIncidents', numOfIncident);
}, 0);
});
I tried to use $apply() after changing value, i tried to use $apply(functuin() {value = 3}), and also i tried to use $emit and $broadcast for changing value and it doesn't help.
But i found solution we need in html after in controller you can use
var scope2 = angular.element("#test").scope();
scope2.point.WarmData.push(result);
$scope.$apply();
P.s. I understand that it is very old question, but may by smb, as i, need this solution.

Jasmine: Backbone.history.loadUrl Breaks Unrelated Tests

I've found the only way to navigate to different URLs to do view and router behavior tests is to use Backbone.history.loadUrl(). Backbone.history.navigate('#something', true) and router.navigate('#something, {trigger: true, replace: true} and any combination thereof do not work within the test. My application does NOT use pushstate.
This works correctly within the context of a single test.
describe('that can navigate to something as expected', function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
//helper method does my responds to fetches, etc. My router in my app is what starts Backbone.history
this.router = initializeBackboneRouter(this.server, this.fixtures, false);
});
afterEach(function(){
this.server.restore();
Backbone.history.stop();
Backbone.history.loadUrl('#');
});
it('should create the view object', function(){
Backbone.history.loadUrl('#something');
expect(this.router.myView).toBeDefined();
});
});
During testing you can see that backbone is appending hashes as expected to the URL: localhost:8888/#something Depending on the test.
Unfortunately, loadUrl seems to be introducing a lot of inconsistencies in the way the tests behave. During one of my tests that involves some asynchronous JS where I need to wait for an AJAX call to complete, the fails about 50% of the time with a timeout or sometimes Expected undefined to be defined. If I console out the data I expect to be there it is, so I know it's not a BS test.
it('should add the rendered html to the body', function(){
runs(function(){
Backbone.history.loadUrl('#something');
});
waitsFor(function(){
var testEl = $('#title');
if(testEl.length > 0){ return true; }
}, 1000, 'UI to be set up');
runs(function(){
var testEl = $('#title');
expect(testEl.text()).toEqual(this.router.model.get(0).title);
});
});
The important note here is that it only fails when all tests are run; run by itself it passes 100% of the time.
My question then is: is Backbone.history.loadUrl a bad way to do programatic navigation around a backbone app in jasmine? I feel like I've tried everything to get this to simulate a user going to a specific URL. Is my teardown incorrect? I've tried it without the Backbone.history.loadUrl('#'); and got different behavior but not passing tests.
The core problem seems to be that in the context of several, hundreds, or even a few jasmine tests, Backbone.history is not clearing itself out and is sticking around as one instance of itself instead of being completely re-initialized at each test.
This sucked.
The solution was to edit my code a bit to add a loading complete flag that was set to true when i was sure that the DOM was 100% finished loading.
Then I wrote a helper function that waited for that flag to be true in my beforeEach function in the root of each test.
var waitForLoadingComplete = function(view){
waitsFor(function(){
if(view.loadingComplete == true){return true;}
}, 100, 'UI Setup Finished');
}
After that I refactored my setup into a helper function:
var setupViewTestEnvironment = function(options) {
var temp = {};
temp.server = sinon.fakeServer.create();
temp.router = initializeBackboneRouter(temp.server, options.fixture);
waitForLoadingComplete(temp.router.initialview);
runs(function(){
Backbone.history.loadUrl(options.url);
temp.view = temp.router[options.view];
temp.model = temp.router[options.model];
waitForLoadingComplete(temp.view);
});
return temp;
}
Example use:
beforeEach(function() {
this.testEnv = setupViewTestEnvironment({
url: '#profile',
view: 'profileIndex',
model: 'myModel',
fixture: this.fixtures
});
});
After which I had a view that i had loaded which I could be assured was finished loading so I could test stuff on the DOM or anything else I wanted to do.

Resources