AngularJs ui-router. Protractor test failing - angularjs

I'm creating a SPA using AngularJs and the ui-router from Angular-UI. Now I'm trying to create the authentication logic.
$rootScope.$on("$stateChangeStart", function (event, toState) {
if(toState.authenticate && !MainService.isAuthenticated()) {
if($cookieStore.get('authToken')) {
MainService.loginWithToken($cookieStore.get('authToken'))
.then(function() {
$state.go(toState.name);
event.preventDefault();
});
}
$rootScope.requestPath = toState.name;
$state.go('public.login');
event.preventDefault();
}
if(toState.url == '/login' && MainService.isAuthenticated()) {
$state.go('private.main');
event.preventDefault();
}
});
On state change, this checks if state requires authentication and transfer to login state if necessary. Also if user is logged in it prevents from reaching the login state. Authentication is done by token stored in cookie.
This is my protractor test scenario:
describe('Routes', function() {
it('Should go to the selected path if user logged in', function() {
browser.get('/');
expect(browser.getLocationAbsUrl()).toMatch("/login");
browser.manage().addCookie("authToken", "aaa");
browser.manage().getCookie("authToken").then(function(cookie) {
expect(cookie.name).toBe('authToken');
expect(cookie.value).toBe('aaa');
});
browser.get('/');
expect(browser.getLocationAbsUrl()).toMatch("/main");
browser.get('/#/main');
expect(browser.getLocationAbsUrl()).toMatch("/main");
/* This part fails, because, when the user is logged in,
he should be transfered to main state, if he is trying to reach the
login page. In this failing case, the user is able to reach the
/login even if he is logged in. */
browser.get('/#/login');
expect(browser.getLocationAbsUrl()).toMatch("/main");
browser.manage().deleteCookie("authToken");
browser.get('/#/login');
expect(browser.getLocationAbsUrl()).toMatch("/login");
browser.get('/#/main');
expect(browser.getLocationAbsUrl()).toMatch("/login");
});
});
When I try to simulate the test behaviour myself, everything is ok, but when I run protractor:
Message:
Expected 'http://localhost/#/login' to match '/main'.
Stacktrace:
Error: Failed expectation

I bumped into another question which resolved this issue.
Basically, you wait for an element in the new page to appear instead of relying on protractor to wait for state/page finish loading. At the time of writing this answer, protractor is still unreliable on waiting page fully loaded for ui-router. Protractor waits for $timeout and $http to be done.
official doc
so if you are using websocket, it might not be covered (at least according my observation).
The api you need to use is browser.wait
browser.wait(function() {
return $('#test321').isPresent(); // keeps waiting until this statement resolves to true
},
timeToWaitInMilliseconds,
'message to log to console if element is not present after that time'
);
expect($('#test321').isPresent()).toBe(true);
You can find details in the following link
Protractor, AngularJS, Parse -- Protractor does not wait for Angular to resolve

You might need to wait for the page to get loaded:
browser.get('/#/main');
var ptor = protractor.getInstance();
ptor.waitForAngular();
expect(browser.getLocationAbsUrl()).toMatch("/main");

Note that in Angular version 1.3 browser.getLocationAbsUrl() returns only the relative path. See the issue in https://github.com/angular/protractor/issues/1436:
Using angular 1.3 in the app under test and protractor 1.3.1 browser.getLocationAbsUrl() returns a relative url instead of the absUrl due to using angular.getTestability().getLocation() instead of $location.absUrl(). It's probably as easy as adding a getLocationAbs() to $$testability, but that goes into architectural questions I don't have context for.

Related

What are the methods we can use to wait for an angular site to be loaded in order to test it with protractor?

What are the methods we can use to wait for an Angular site to be loaded in order to test it with protractor in order to avoid this error caused by jasmine : A Jasmine spec timed out. Resetting the WebDriver Control Flow ?
I'm able to make the login and go to home page that test is passed, but from the second test i have problems of jasmine.
I have configured this problem by adding this function into my config file :
onPrepare: function() {
return browser.getProcessedConfig().then(function(config) {
var browserName = config.capabilities.browserName;
browser.manage().timeouts().setScriptTimeout(60000);
});
});
You can use the browser object of Protractor to wait for angular.
As soon as you load your page add the following :
browser.waitForAngular();
This error means that your test took too much time and exceeded the default Jasmine spec timeout interval which is 30 seconds by default (It looks like you've configured the timeout to be 60 seconds). It can be configured in the jasmineNodeOpts object in your Protractor config:
jasmineNodeOpts: {defaultTimeoutInterval: timeout_in_millis},
The solution is usually use-case specific and it usually indicates there is an error in the test code. In order to fully understand what is going, we would need to see the code itself.
In your particular case, for starters, you should try moving the "ignore synchronization" and the browser.get() part into the beforeEach. Also, since you are turning the sync off, you need to wait for the element to be present on the page before interacting with it:
describe("my app", function () {
beforeEach(function () {
browser.ignoreSynchronization = true;
browser.get("...");
});
it("should make the login test", function () {
// ...
var EC = protractor.ExpectedConditions;
var username = element(by.model("credentials.username"));
browser.wait(EC.presenceOf(username), 10000);
username.sendKeys("RET02");
// ...
});
});
And, I am not sure if you really need to turn the synchronization off since this is an AngularJS page you are working with.
Can you wait for a url?
Let's assume that when you click on the login button your page is redirected to another url. So you can wait for the expected url. Example:
browser.driver.wait(function() {
return browser.driver.getCurrentUrl().then(function(url) {
// Dashboard is the loaded url after login (in this example)
return /Dashboard/.test(url);
});
}, 60000);
This code waits for the page browser.baseUrl/Dashboard to be loaded, for 60 seconds

How do I use $httpBackend (e2e) without refreshing the page i.e without browser.get

I am using Protractor and ngMockE2E to mock api responses. It works fine if I use browser.get to navigate to the next page. The expected mock data is returned, however when I navigate to the next page using a click() on the search button, $httpBackend does not pick up the GET and live data is returned.
I need it to return mock data on a button click because the application starts a session when the application loads. Browser.get causes a page refresh so although the mock data gets returned, a new session is created also which is not what happens in a live environment.
Can $httpBackend invoke a whenGET after a button click?
I am using TypeScript to write the code and http-backend-proxy for the mock module which uses ngMockE2E under the hood:
//httpBackEndMocks.ts
var HttpBackend = require('http-backend-proxy');
var proxy = new HttpBackend(browser);
export class HttpBackendMocks {
/**
* Builds a mock module passing any number of the responses below
*/
buildMockModule = (funcs: Function[]) => {
if (Array.isArray(funcs)) {
for (var arrayIndex in funcs) {
funcs[arrayIndex];
}
}
/** Allow all other GET requests to pass through e.g. html pages */
proxy.onLoad.whenGET(/.*/).passThrough();
}
/**
* ...
*/
buildSimpleResponse() {
var response = require('../responses/simple-respsonse.json');
return proxy.onLoad.whenGET('the request url').respond(response);
}
otherResponse() {whenGET...}
otherResponse() {whenGET...}
....
I can then require:
//search.page.spec
var httpBackEndMocks: app.test.HttpBackendMocks = require('../httpBackEndMocks.js');
and load which ever responses I need:
beforeEach:
var responsesToLoad = [
httpBackEndMocks.buildSimpleResponse(),
httpBackEndMocks.otherResponse()
];
httpBackEndMocks.buildMockModule(responsesToLoad);
When I transition to the next page with this:
//search.object.page
browser.get('the url ...');
It Works. It returns the mock data BUT causes a page refresh initialising a new session.
When I transition to the next page with this:
this.submitButton.click(); //Code to cause button click omitted
It transitions to the next page without a page refresh, therefore in the same session but returns live data.
I figured out a solution myself for anyone who has a similar issue:
The mock module with any required responses should be added BEFORE protractor starts the application. This way whenever the initial browser.get(...) is called to load the homepage, all the whenGETs will be registered.
open(responsesToLoad: any[]): void {
this.buildMockModuleWithRequiredResponses(responsesToLoad);
browser.get('/home');
}

Wait for page redirect to evaluate `expect()` statement

So, I'm trying to script a login form for automation through protractor, but running into some problems when I'm trying to verify the cookies after the page redirect.
Here's my sample code:
describe('login', function () {
var app;
var LoginPage = require('./login.page.e2e.js');
// Before each test, refresh page
beforeEach(function () {
LoginPage.get();
app = element(by.css('body'));
});
// Check route, make sure it hasn't been redirected somewhere strange
it('should be at path: /login', function () {
expect(browser.getCurrentUrl()).toContain('/login');
});
/**
* Login as a provider (phoenix.e2e.login.test#leadingreach.com)
*/
it('should be able to login', function () {
// Fill out fields
LoginPage.populate_provider_form();
// Login as provider
// Clicking this button fires off an AJAX request that logs in the user, and populates a few browser cookies
element(by.css('#provider-login-form-container #login_btn')).click();
// These two statements work fine. They seem to wait for the redirect and end up passing.
expect(browser.getCurrentUrl()).toContain('/dashboard');
expect(app.evaluate('currentUser.username')).toEqual('phoenixe2elogin');
// The following statements are executed before the page redirects, and therefore fail
expect(!!browser.manage().getCookies().lrrt).toBe(true);
expect(!!browser.manage().getCookies().lrco).toBe(true);
expect(browser.manage().getCookies().lrrm).toBe('false');
});
});
It's been about 3 or 4 months since I created my first Protractor tests, so I'm re-learning all the new syntax, etc. I'm currently under the impression that using waitsFor and methods like that aren't really encouraged (or supported) anymore, so I'm wondering how someone could go about scripting something like this.
In order to get protractor to wait for the element to appear, you have to use the following syntax:
ptor.findElement(by.id('my-elt')).then(function (elt) {
expect(elt.evaluate('my.binding')).toBe('someValue');
});
Took me a while to figure this out, hope it ends up helping someone :D

angularJS unit testing where run contains a HTTP request?

I am fairly new to AngularJS and am trying to learn some best practices. I have things working, but would like to start adding some unit tests to my modules and controllers. The first one I am looking to tackle is my AuthModule.
I have an AuthModule. This Module registers a Factory called "AuthModule" and exposes things like "setAuthenticatedUser" and also fields like "isLoggedIn" and "currentUser". I think this is a fairly common pattern in an AngularJS application, with some variations on the specific implementation details.
authModule.factory(
'AuthModule',
function(APIService, $rootScope) {
var _currentUser = null;
var _isLoggedIn = false;
return {
'setAuthenticatedUser' : function(currentUser) {
_currentUser = currentUser;
_isLoggedIn = currentUser == null ? false : true;
$rootScope.$broadcast('event:authenticatedUserChanged',
_currentUser);
if (_isLoggedIn == false) {
$rootScope.$broadcast('event:loginRequired')
}
$rootScope.authenticatedUser = _currentUser;
$rootScope.isLoggedIn = _isLoggedIn;
},
'isLoggedIn' : _isLoggedIn,
'currentUser' : _currentUser
}
});
The module does some other things like register a handler for the event "loginRequired" to send the person back to the home screen. These events are raised by the AuthModule factory.
authModule.run(function($rootScope, $log, $location) {
$rootScope.$on("event:loginRequired", function(event, data) {
$log.info("sending him home. Login is required");
$location.path("/");
});
});
Finally, the module has a run block which will use an API service I have to determine the current logged in user form the backend.
authModule.run(
function(APIService, $log, AuthModule) {
APIService.keepAlive().then(function(currentUser) {
AuthModule.setAuthenticatedUser(currentUser.user);
}, function(response) {
AuthModule.setAuthenticatedUser(null);
});
});
Here are some of my questions:
My question is how would you setup tests for this? I would think that I would need to Mock out the APIService? I'm having a hard time because I keep getting unexpected POST request to my /keepalive function (called within APIService.keepAlive())?
Is there any way to use $httpBackend in order to return the right response to the actual KeepAlive call? This would prevent me from having to mock-out the API service?
Should I pull the .run() block out which obtains the current logged in user out of the AuthModule and put it into the main application? It seems no matter where I put the run() block, I can't seem to initialize the $httpbackend before I load the module?
Should the AuthModule even be its own module at all? or should I just use the main application module and register the factory there?
Run blocks are the closest thing in Angular to the main method. A run block is the code which needs to run to kickstart the application. It is executed after all of the service have been configured and the injector has been created. Run blocks typically contain code which is hard to unit-test, and for this reason should be declared in isolated modules, so that they can be ignored in the unit-tests.angularjs docs
I suggest you take a look at this authentication service, using a service is the way to go.
Hopefully this would help ... Good luck

AngularJS HTML5 mode degrade to full page reloads in lieu of hashbang

By enabling HTML5 mode in AngularJS, the $location service will rewrite URLs to remove the hashbang from them. This is a great feature that will help me with my application, but there is a problem with its fallback to hashbang mode. My service requires authentication, and I am forced to use an external authentication mechanism from my application. If a user attempts to go to a URL for my app with a hashbang in it, it will first redirect them to the authentication page (won't ever touch my service unless successfully authenticated), and then redirect them back to my application. Being that the hash tag is only seen from the client side, it will drop off whatever parts of the routes come after by the time they hit my server. Once they are authenticated, they may re-enter the URL and it will work, but its that one initial time that will cause a disruption to the user experience.
My question is then, is there any way to go from $location.html5Mode(true) to the fallback of full page reloads for un-supportive browsers, skipping the hashbang method of routing entirely in AngularJS?
The best comparison of available implementations of what I'm aiming for would be something such as browsing around folders on github.com. If the browser supports rewriting the URL without initiating a page refresh, the page will asynchronously load the necessary parts. If the browser does not support it, when a user clicks on a folder, a full-page refresh occurs. Can this be achieved with AngularJS in lieu of using the hashbang mode?
DON'T overwrite the core functionality.
Use Modernizr, do feature detection, and then proceed accordingly.
check for history API support
if (Modernizr.history) {
// history management works!
} else {
// no history support :(
// fall back to a scripted solution like History.js
}
Try to wrap $location and $routeProvider configuration in browser's HTML5 History API checking, like this:
if (isBrowserSupportsHistoryAPI()) {
$location.html5Mode(true)
$routeProvider.when(...);
}
Also may be you need to create a wrapper to $location if you use it to change path.
(Sorry for terrible english)
Why not handle the un-authenticated redirect on the client side for this situation? I'd need to know a bit more about exactly how your app works to give you a more specific solution but essentially something like:
User goes to a route handled by AngularJS, server serves up the AngularJS main template and javascript
User is not authenticated, AngularJS detects this and redirects to the authentication page
You could have something in the module's run block for when the AngularJS application starts:
module('app',[])
.configure(...yadda...yadda...yadda...)
.run(['$location', 'authenticationService', function($location, auth) {
if (!auth.isAuthenticated()) {
$location.url(authenticationUrl)
}
});
I've subbed in a service which would find out if you were authenticated somehow, up to you how, could be checking a session cookie, could be hitting your API to ask. Really depends on how you want to continue to check authentication as the client application runs.
You can try and override the functionality of the $location service. The general idea would be to rewrite the URL according to whether someone is already authenticated or not, or just use a single approach (without hashbangs) for all URLs, regardless to whether html5mode is on or not.
I'm not sure that I fully understand the use-case so I can't write the exact code that you need. Here is a sample implementation of how to overrides/implements and registers the $location service, just making sure that hashbang is always eliminated:
app.service('$location', [function() {
var DEFAULT_PORTS = {
ftp: 21,
http: 80,
https: 443
};
angular.extend(this, {
absUrl: function() {
return location.href;
},
hash: function(hash) {
return location.hash.substr(1);
},
host: function() {
return location.host;
},
path: function(path) {
if (!path) {
return location.pathname;
}
location.pathname = path;
return this;
},
port: function() {
return location.port ? Number(location.port) : DEFAULT_PORTS[this.protocol()] || null;
},
protocol: function() {
return location.protocol.substr(0, location.protocol.length - 1);
},
replace: function() {
return this;
},
search: function(search, paramValue) {
if (search || paramValue) {
return this;
}
var query = {};
location.search.substr(1).split("&").forEach(function(pair) {
pair = pair.split("="); query[pair[0]] = decodeURIComponent(pair[1]);
});
return query;
},
url: function(url, replace) {
return this.path();
}
});
}]);

Resources