cant get my angularJS app to work with phantomJS - angularjs

Using latest phantomJS (1.9.7) on Win8 on my AngularJS (v. 1.2.9) application, I'm not able to interact with my application.
I keep getting the message
InvalidElementStateError: {"errorMessage":"Element is not currently interactable and may not be manipulated".
When I call render(), I get an empty image;
If I add a timeout to my script before calling render(), I get a completely mangled view of my page:
for comparison, this is what it looks like in any other browser:
The script i've used to take the screen capture-
var page = require('webpage').create();
page.open('http://localhost/app/account#/login', function() {
setTimeout(function() {
page.render('login-phantom.png');
phantom.exit();
}, 1000);
});
has anyone else encountered anything like that?
what should I check for?

Timeout won't really help (unless you set it for a super long time) because network response speeds vary from time to time. The best option would be to have a variable that notifies when data loading (on the angular side) has completed e.g. $scope.dataStatus = 'ready'. Then you could add a data-status attribute to an element on the html and poll it from phantomjs side to see if data has loaded. You would do the polling inside page.evaluate on phantomjs.
Matsko has written a blog along those lines and some sample code
So your phantomjs code could look something like:
var page = require('webpage').create();
page.open('http://localhost/app/account#/login', function (status) {
var delay, checker = (function() {
var html = page.evaluate(function () {
var status = document.getElementById('status');
if(status.getAttribute('load-status') == 'ready') {
return document.getElementsByTagName('html')[0].outerHTML;
}
});
if(html) {
clearTimeout(delay);
page.render('login-phantom.png');
phantom.exit();
}
});
delay = setInterval(checker, 100);
});
While your html could be:
<html>
<head>...</head>
<body>
<div id="status" load-status="{{ dataStatus }}"></div>
</body>
</html>
Hope that helps

Related

$templateCache from file undefined? When is the accessible by other js code? (with np-autocomplete)

I'm rather new to angular and I'm trying to integrate np-autocomplete in my application (https://github.com/ng-pros/np-autocomplete). However I can only get it to work when I'm passing a html string as a template inside the $scope.options and it doesn't work when I want to load it from a separate html.
the Code for my app looks as follows:
var eventsApp = angular.module('eventsApp',['ng-pros.directive.autocomplete'])
eventsApp.run(function($templateCache, $http) {
$http.get('test.html', {
cache: $templateCache
});
console.log($templateCache.get('test.html')) // --> returns undefined
setTimeout(function() {
console.log($templateCache.get('test.html')) // --> works fine
}, 1000);
//$templateCache.put('test.html', 'html string') //Would solve my issue in the controller,
//but I would rather prefer to load it from a separate html as I'm trying above
Inside my controller I am setting the options for autocomplete as follows:
controllers.createNewEventController = function ($scope) {
$scope.options = {
url: 'https://api.github.com/search/repositories',
delay: 300,
dataHolder: 'items',
searchParam: 'q',
itemTemplateUrl: 'test.html', // <-- Does not work
};
//other stuff...
}
however, it seems that test.html is undefined by the time np-autocomplete wants to use it (as it is also in first console.log above).
So my intuition tells me that the test.html is probably accessed in the controller before it is loaded in eventsApp.run(...). However I am not sure how to solve that?
Any help would be highly appreciated.
You are most likely correct in your assumption.
The call by $http is asynchronous, but the run block will not wait for it to finish. It will continue to execute and the execution will hit the controller etc before the template has been retrieved and cached.
One solution is to first retrieve all templates that you need then manually bootstrap your application.
Another way that should work is to defer the execution of the np-autocomplete directive until the template has been retrieved.
To prevent np-autocomplete from running too early you can use ng-if:
<div np-autocomplete="options" ng-if="viewModel.isReady"></div>
When the template has been retrieved you can fire an event:
$http.get('test.html', {
cache: $templateCache
}).success(function() {
$rootScope.$broadcast('templateIsReady');
});
In your controller listen for the event and react:
$scope.$on('templateIsReady', function () {
$scope.viewModel.isReady = true;
});
If you want you can stop listening immediately since the event should only fire once anyway:
var stopListening = $scope.$on('templateIsReady', function() {
$scope.viewModel.isReady = true;
stopListening();
});

Testing the contents of a temporary element with protractor

I'm trying to test the login page on my site using protractor.
If you log in incorrectly, the site displays a "toast" message that pops up for 5 seconds, then disappears (using $timeout).
I'm using the following test:
describe('[login]', ()->
it('should show a toast with an error if the password is wrong', ()->
username = element(select.model("user.username"))
password = element(select.model("user.password"))
loginButton = $('button[type=\'submit\']')
toast = $('.toaster')
# Verify that the toast isn't visible yet
expect(toast.isDisplayed()).toBe(false)
username.sendKeys("admin")
password.sendKeys("wrongpassword")
loginButton.click().then(()->
# Verify that toast appears and contains an error
toastMessage = $('.toast-message')
expect(toast.isDisplayed()).toBe(true)
expect(toastMessage.getText()).toBe("Invalid password")
)
)
)
The relevant markup (jade) is below:
.toaster(ng-show="messages.length")
.toast-message(ng-repeat="message in messages") {{message.body}}
The problem is the toastMessage test is failing (it can't find the element). It seems to be waiting for the toast to disappear and then running the test.
I've also tried putting the toastMessage test outside the then() callback (I think this is pretty much redundant anyway), but I get the exact same behaviour.
My best guess is that protractor sees that there's a $timeout running, and waits for it to finish before running the next test (ref protractor control flow). How would I get around this and make sure the test runs during the timeout?
Update:
Following the suggestion below, I used browser.wait() to wait for the toast to be visible, then tried to run the test when the promise resolved. It didn't work.
console.log "clicking button"
loginButton.click()
browser.wait((()-> toast.isDisplayed()),20000, "never visible").then(()->
console.log "looking for message"
toastMessage = $('.toaster')
expect(toastMessage.getText()).toBe("Invalid password")
)
The console.log statements let me see what's going on. This is the series of events, the [] are what I see happening in the browser.
clicking button
[toast appears]
[5 sec pass]
[toast disappears]
looking for message
[test fails]
For added clarity on what is going on with the toaster: I have a service which essentially holds an array of messages. The toast directive is always on the page (template is the jade above), and watches the messages in the toast service. If there is a new message, it runs the following code:
scope.messages.push(newMessage)
# set a timeout to remove it afterwards.
$timeout(
()->
scope.messages.splice(0,1)
,
5000
)
This pushes the message into the messages array on the scope for 5 seconds, which is what makes the toast appear (via ng-show="messages.length").
Why is protractor waiting for the toast's $timeout to expire before moving on to the tests?
I hacked around this using the below code block. I had a notification bar from a 3rd party node package (ng-notifications-bar) that used $timeout instead of $interval, but needed to expect that the error text was a certain value. I put used a short sleep() to allow the notification bar animation to appear, switched ignoreSynchronization to true so Protractor wouldn't wait for the $timeout to end, set my expect(), and switched the ignoreSynchronization back to false so Protractor can continue the test within regular AngularJS cadence. I know the sleeps aren't ideal, but they are very short.
browser.sleep(500);
browser.ignoreSynchronization = true;
expect(page.notification.getText()).toContain('The card was declined.');
browser.sleep(500);
browser.ignoreSynchronization = false;
It turns out that this is known behaviour for protractor. I think it should be a bug, but at the moment the issue is closed.
The workaround is to use $interval instead of $timeout, setting the third argument to 1 so it only gets called once.
you should wait for your toast displayed then do other steps
browser.wait(function() {
return $('.toaster').isDisplayed();
}, 20000);
In case anyone is still interested, this code works for me with no hacks to $timeout or $interval or Toast. The idea is to use the promises of click() and wait() to turn on and off synchronization. Click whatever to get to the page with the toast message, and immediately turn off sync, wait for the toast message, then dismiss it and then turn back on sync (INSIDE the promise).
element(by.id('createFoo')).click().then(function () {
browser.wait(EC.stalenessOf(element(by.id('createFoo'))), TIMEOUT);
browser.ignoreSynchronization = true;
browser.wait(EC.visibilityOf(element(by.id('toastClose'))), TIMEOUT).then(function () {
element(by.id('toastClose')).click();
browser.ignoreSynchronization = false;
})
});
I hope this can help who has some trouble with protractor, jasmine, angular and ngToast.
I create a CommonPage to handle Toast in every pages without duplicate code.
For example:
var CommonPage = require('./pages/common-page');
var commonPage = new CommonPage();
decribe('Test toast', function(){
it('should add new product', function () {
browser.setLocation("/products/new").then(function () {
element(by.model("product.name")).sendKeys("Some name");
var btnSave = element(by.css("div.head a.btn-save"));
browser.wait(EC.elementToBeClickable(btnSave, 5000));
btnSave.click().then(function () {
// this function use a callback to notify
// me when Toast appears
commonPage.successAlert(function (toast) {
expect(toast.isDisplayed()).toBe(true);
});
});
});
})
});
And this is my CommonPage:
var _toastAlert = function (type, cb) {
var toast = null;
switch (type) {
case "success":
toast = $('ul.ng-toast__list div.alert-success');
break;
case "danger":
toast = $('ul.ng-toast__list div.alert-danger');
break;
}
if (!toast) {
throw new Error("Unable to determine the correct toast's type");
}
browser.ignoreSynchronization = true;
browser.sleep(500);
browser.wait(EC.presenceOf(toast), 10000).then(function () {
cb(toast);
toast.click();
browser.ignoreSynchronization = false;
})
}
var CommonPage = function () {
this.successAlert = function (cb) {
_toastAlert("success", cb);
};
this.dangerAlert = function(cb) {
_toastAlert("danger", cb);
}
}
module.exports = CommonPage;
Chris-Traynor's answer worked for me but i've got an update.
ignoreSynchronization is now deprecated.
For those using angular and protractor to test this, the below works nicely for me.
$(locators.button).click();
await browser.waitForAngularEnabled(false);
const isDisplayed = await $(locators.notification).isPresent();
await browser.waitForAngularEnabled(true);
expect(isDisplayed).toEqual(true);
I've simplified this to make it easier to see, I would normally place this inside a method to make the locators dynamic.

get text of element during angular $timeout in protractor

Note: using MEAN - Mocha, Express, Angular, and Node for the app
I am learning how to do e2e test and I built a little app to test against. One thing it does is whenever you perform an action it displays a message for X seconds and then goes away. I'm using angulars $timeout.
$scope.displayMessage = function(messageIn, borderColor){
var timeoutTime = 5000; //clear the message after x seconds
$scope.borderType = "1px solid " + borderColor;
$scope.messages = messageIn;
$scope.visibility = true; //reveal the div
$timeout.cancel($scope.timeout); //cancel any previous timers
$scope.timeout = $timeout(function(){ //Set and start new timer.
$scope.visibility = false;
$scope.messages = "";
}, timeoutTime);
};
Now I'm trying to test this. Currently I have
it("should display message", function(done){
var button = element(by.id("getStudents"));
console.log("clicking button");
var messageElem = element(by.binding("messages"));
button.click().then(function () {
console.log("button clicked");
messageElem.getText().then(function(text){
console.log("The text should equal: " + text);
});
//expect(messageElem.getText()).toBe("Students retrieved");
console.log("test executed");
});
done();
});
But it seems to be waiting until after the timeout to do the messageElem.getText().then() at which point the div has been cleared. The "test executed" log will print during the timeout but not the "getText()" function. I'm guessing this has something to do with angulars life cycle?
Oh and here's the HTML (in Jade format)
div(ng-model="messages",ng-show = "visibility", style = "clear:both;padding-top:3em;border:{{borderType}};text-align:center;")
h3
{{messages}}
ignore my style, I was too lazy to make a css file for this test app :D
Try using $interval instead of $timeout.
Protractor attempts to wait until the page is completely loaded before
performing any action (such as finding an element or sending a command
to an element).
Read here. Details here.
I hacked around this using the below code block. I had a notification bar from a 3rd party node package (ng-notifications-bar) that used $timeout instead of $interval, but needed to expect that the error text was a certain value. I put used a short sleep() to allow the notification bar animation to appear, switched ignoreSynchronization to true so Protractor wouldn't wait for the $timeout to end, set my expect(), and switched the ignoreSynchronization back to false so Protractor can continue the test within regular AngularJS cadence. I know the sleeps aren't ideal, but they are very short.
browser.sleep(500);
browser.ignoreSynchronization = true;
expect(page.notification.getText()).toContain('The card was declined.');
browser.sleep(500);
browser.ignoreSynchronization = false;

Controller with ngResource method called indefinitely

I decided to start learning AngularJS by making a simple app.
The server-side application is built with ExpressJs, but the resource used below (/movie/:id) are not implemented yet, so pointing to this URL will result in a 404 (Not Found) error. So only getting '/' works.
I wanted to see how a $resource behaved so I made this simple test :
var app = angular.module("app", ["ngResource"]);
app.factory("Movie", function ($resource) {
return $resource("/movie/:id");
})
app.controller("MovieCtrl", function($scope, Movie) {
$scope.test = function () {
Movie.query();
return 42;
}
});
And my template file :
<div ng-app="app">
<div ng-controller="MovieCtrl">
{{ test() }}
</div>
</div>
As expected the template is rendered and '42' is properly displayed, but if I watch the console in the Chrome developer tools I keep seeing the following error (also as expected):
GET http://localhost/movie 404 (Not Found)
But this message is printed indefinitely and never stops, as if my Movie resource keeps trying to reach /movie even though after a hundred tries it still keeps failing.
Thank you in advance.
This is because Movie.query() calls $scope.$apply() after it gets response from the server.
Everytime $scope.$apply() is called, angular does dirty checking (which again invokes test and therefore calls Movie.query() again) to find out if anything has changed. This causes an infinite loop.
move Movie.query() out from the test(), and this should work.
Let me make myself clear - take look at this pseudo code:
var watches = ['$scope.test()'];
var previous = {};
var values = {};
$rootScope.$apply = function(){
previous = values;
values = {};
var dirty = false;
for (var i =0;i<watches.length;i++){
var expression = watches[i];
values[expression] = value = eval(expression);
if(value!=previous)dirty=true;
}
if(dirty)$rootScope.$apply();
}
Movie.query = function(){
setTimeout(function(){
$rootScope.$apply();
},300);
}
$scope.test = function(){
Movie.query();
return 42;
}
so the flow is following:
$scope.apply();
$scope.test();
Movie.query(); -> setTimeout($scope.apply,100) (back to beginning );
and so on..

AngularJS: Is it possible to watch a global variable (I.e outside a scope)?

Let me start by saying that what I am trying to do is probably not considered good practice. However, I need to do something like this in order to migrate a large web app to AngularJs in small incremental steps.
I tried doing
$scope.$watch(function () { return myVar; }, function (n, old) {
alert(n + ' ' + old);
});
Where myVar is a global variable (defined on window)
And then changing myVar from the console.
But it only fires when first setting up the watcher.
It works if I update myVar from within the controller (see http://jsfiddle.net/rasmusvhansen/vsDXz/3/, but not if it is updated from some legacy javascript
Is there any way to achieve this?
Update
I like Anders' answer if the legacy code is completely off limits. However, at the moment I am looking at this approach which seems to work and does not include a timer firing every second:
// In legacy code when changing stuff
$('.angular-component').each(function () {
$(this).scope().$broadcast('changed');
});
// In angular
$scope.$on('changed', function () {
$scope.reactToChange();
});
I am awarding points to Anders even though I will go with another solution, since his solution correctly solves the problem stated.
The issue here is probably that you're modifying myVar from outside of the Angular world. Angular doesn't run digest cycles/dirty checks all the time, only when things happen in an application that should trigger a digest, such as DOM events that Angular knows about. So even if myVar has changed, Angular sees no reason to start a new digest cycle, since nothing has happened (at least that Angular knows about).
So in order to fire your watch, you need to force Angular to run a digest when you change myVar. But that would be a bit cumbersome, I think you would be better of to create a global observable object, something like this:
<!doctype html>
<html ng-app="myApp">
<head>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://code.angularjs.org/1.0.5/angular.min.js"></script>
<script>
// Outside of Angular
window.myVar = {prop: "value1"};
var myVarWatch = (function() {
var watches = {};
return {
watch: function(callback) {
var id = Math.random().toString();
watches[id] = callback;
// Return a function that removes the listener
return function() {
watches[id] = null;
delete watches[id];
}
},
trigger: function() {
for (var k in watches) {
watches[k](window.myVar);
}
}
}
})();
setTimeout(function() {
window.myVar.prop = "new value";
myVarWatch.trigger();
}, 1000);
// Inside of Angular
angular.module('myApp', []).controller('Ctrl', function($scope) {
var unbind = myVarWatch.watch(function(newVal) {
console.log("the value changed!", newVal);
});
// Unbind the listener when the scope is destroyed
$scope.$on('$destroy', unbind);
});
</script>
</head>
<body ng-controller="Ctrl">
</body>
</html>

Resources