AIon AIon - 4 months ago 35
Javascript Question

How to start an Angular digest cycle using PhantomJS?

I'm using PhantomJs (virtual browser) and JQuery to automatically navigate a website. But I'm stuck on the Login page - trying to submit username and password.

This website is an angular app. I use ng-click to call the login() function - which get's the user object.

Html:

<form>
<label>Username:</label>
<input type="text", ng-model="user.username" ><br>
<label>Password:</label>
<input type="text", ng-model="user.password" >
<input type="button", ng-click="login(user)" value="Login">
</form>


LoginCtrl:

$scope.login = function(user){
var route = Restangular.all("/auth/login");
route.post(user)
.then(function(response){
console.log(response);
$window.localStorage.token = response.token;
$state.go('level1');
}, function(err){
console.log("ERROR in Logging in!");
});
};


When I use the app from a normal browser - by typing on keyboard - the login functionality works. But when I use PhantomJs and Jquery - to automate the login process - is not.

PhantomJs renders the page like any other browser - and using the page.evaluate method with the following code it fills the username (123) and the password (123) and clicks on the Login Button. This theoretically should login the user. But practically is not.

page.evaluate(function() {
$("form").find('input').eq(0).val("123"); // fill username
$("form").find('input').eq(1).val("123"); // fill password
setTimeout(function(){
$("form").find('input').eq(2).click(); // click the Login Button.
},100);
});


After little debugging I figured that data binding is the problem. The above code does not trigger the angular digest cycle.

When the Login Button is clicked by jquery - the user.username and user.password even if they are filled, remain empty form the angular perspective. ng-model it's not doing it's magic. The user is empty, the controller submits empty data to the server - and that is why i get 401 Unauthorized!

I tried using this:

page.evaluate(function() {
$("form").find('input').eq(0).val("123"); // fill username
$("form").find('input').eq(1).val("123"); // fill password

// trying to start an angular digest cycle
$myscope = angular.element(document.querySelector("body")).scope();
$myscope.$apply();

setTimeout(function(){
$("form").find('input').eq(2).click();
},100);
});


Basically nothing happens. What I'm doing wrong here?
I also think this is not an obscure issue. PhantomJs is used for unitTesting all day so how normal people - who test angular apps - do: automatic login, registration, navigation all that - how they start the digest cycle - from within the JQuery code placed inside the page.evaluate method?

Answer

I found the answer. Problem was angular binding as I expected. In order to update the ng-model form outside of the angular world - ex JQuery - you need to use trigger("input") event.

ng-model basically listens for this "input" events - and internally this will automatically trigger the digest loop. This input events are also sent all the time when typing on the keyboard. ( I found this explanation here: How does AngularJS internally catch events like 'onclick', 'onchange'?)

The solution:

page.evaluate(function() {
    $("form").find('input').eq(0).val("123").trigger("input"); // fill username and update the ng-model
    $("form").find('input').eq(1).val("123").trigger("input"); // fill username and update the ng-model
    setTimeout(function(){
        $("form").find('input').eq(2).click(); // click on Login button -> this is picked up normally by angular.
    },100);
});
Comments