I'm having trouble with a protractor test.
Overview of the test
Click button to show a form
Fill out the form
Click another button to save the form - this should call a function in one of my controllers that makes an http call and then reloads some models on the page.
When clicking the final "save" button, everything seems to freeze and the attached ng-click function never seems to get called. I don't see any errors in the console when I use browser.pause. I can see that the button is in fact clicked, but at that point, nothing seems to happen.
Final button definition:
this.addConfigFilterLineButton = element(by.css('[ng-click="cflCtrl.create();"]'));
Function that fills out the form:
this.addNewConfigFilterLine = function (cb) {
var self = this;
var deferred = protractor.promise.defer();
browser.executeScript('window.scrollTo(0,10000);')
.then(function(){
self.newConfigFilterLineButton.click();
// code that fills out the form
self.addConfigFilterLineButton.click();
browser.waitForAngular()
.then(function(){
deferred.fulfill();
});
});
return deferred.promise;
};
Spec
it('should allow creating a new ConfigFilterLine', function (done) {
var length;
settingsPage.configFilterLines.count()
.then(function(count){
length = count;
return settingsPage.addNewConfigFilterLine();
})
.then(function(){
expect(settingsPage.configFilterLines.count()).to.eventually.equal(length+1);
done();
});
});
I've tried with browser.waitForAngular and without it, and it doesn't seem to matter. When the button is clicked, nothing happens.
Any ideas would be helpful. Let me know if there's more info I can provide.
Instead of using
this.addConfigFilterLineButton = element(by.css('[ng-click="cflCtrl.create();"]'));
try something more like this:
this.addConfigFilterLineButton = element(by.id('id-of-final-button'));
My guess is that Protractor isn't correctly finding "addConfigFilterLineButton".
Related
I am implementing an advanced search in an application I'm working on. In this search I want the back and next buttons from the browser to work with the search.
To achieve this I added this to my $routeProvider:
.when('/search', {
templateUrl: 'Scripts/App/Views/Search.html',
controller: 'SearchController',
reloadOnSearch: false
})
In my SearchController I have a search function on the $scope that just adds some query string parameters to my url:
$scope.advancedSearch = function(){
$location.search("page", $scope.currentPage);
$location.search("groupingField", $scope.groupingField);
$location.search("topgroup", angular.toJson($scope.filter));
}
I listen to $routeUpdate, get the query string parameters and execute the search with these parameters.
$scope.$on('$routeUpdate', function () {
var qs = $location.search();
var topGroup = qs["topgroup"];
$scope.filter = angular.fromJson(topGroup);
$scope.groupingField = qs["groupingField"];
$scope.currentPage = qs["page"];
performSearch();
});
In my real search method I check the form for errors. If there are any I show a message else I perform the search (do an api call).
var performSearch = function () {
if ($scope.searchForm.$invalid) {
$scope.message = "Please fix the errors before you search.";
} else {
//get search results from database and put them on $scope.
}
}
With this code I can search and get the correct results. If I do a new search it also get's the correct results and I can use the back and next buttons from the browser.
When I do a search and my form is invalid the message shows, but when I go back after an invalid search the $invalid of the form updates after my "performSearch" method is called. This causes "Please fix the errors before you search." message to display even if the form is $valid.
If I click on the next button after this I get even more trouble since the form is $valid now but with the querystring parameters filled in it should be $invalid. Again this only updates after the performSearch has been called. wich is to late.
It might be hard to understand my problem, if something is unclear ask away!
Found the answer!
On the $routeupdate I waited for a digest cycle to finish and update the form properties. This can be done with a $timeout without a time.
$scope.$on('$routeUpdate', function () {
var qs = $location.search();
var topGroup = qs["topgroup"];
$scope.filter = angular.fromJson(topGroup);
$scope.groupingField = qs["groupingField"];
$scope.currentPage = qs["page"];
$timeout(function () {
performSearch();
});
});
I'm using a module from the UI Boostrap extensions (http://angular-ui.github.io/bootstrap). The module actually serves as a loading dialog and is automatically closed when a set of web service data is returned to my Angular code. As the data on this page is loaded automatically the dialog comes up immediately.
All this works great when I hit the page in question for the the first time or simply refresh it. The problem occurs when I go to a deeper page and then try and navigate back to the original page (with the dialog) via the browser's back button. The dialog never goes away despite all the fact that all the data is returned and the module's dismiss() call has been made.
I've traced this down to the promise to open the dialog appears to be happening after the dismiss call but, again, only when the page is loaded via the back button. The dismiss call never closes anything because it hasn't been added yet (I've confirmed this in the debugger).
The question I have is how could I handle this? Is there a solid way to catch the completion of the page loading via Angular and double check that the dialog closed? Is there a better way via UI Bootstrap's api?
I know this is rather unusual case but any thoughts on it would be great.
Thanks!
#HankScorpio's solution is good, but I think there may be a simplified option now.
There is no need to store the current modal anymore, if you register either a $locationChangeStart or $routeChangeStart listener with $uibModalStack injected and call $uibModalStack.dismissAll(). $locationChangeStart has the benefit of working for both ngRoute and uiRoute.
i.e. If only for the one page, then in your controller you'd have:
angular.module('app')
.controller('ctrl', ['$scope', '$uibModalStack', ctrl]);
function ctrl($scope, $uibModalStack) {
$scope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
If you want to do this for all pages then define this in a factory that is always loaded or just an app.run code segment:
angular.module('app')
.run(['$rootScope', '$uibModalStack', setupUibModal]);
setupUibModal($rootScope, $uibModalStack) {
$rootScope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
Here is the simple solution when using ui-router for state change
Closing modal popup on the back button click in angularjs
App.run(['$rootScope', '$modalStack', function ($rootScope, $modalStack) {
$rootScope.$on('$stateChangeStart', function (event) {
var top = $modalStack.getTop();
if (top) {
$modalStack.dismiss(top.key);
}
})
}]);
hope this will save lot of time for people who are breaking heads
I've run into this same problem. Here's how I fixed it.
1) Create a service to abstract the opening and closing of a modal and track which one is open (necessary for step 2). Instead of calling $modal.open() directly, call ModalService.open().
Here you go, you can have the one I wrote:
(function () {
'use strict';
var theModule = angular.module('services.modalService', ['ui.bootstrap']);
theModule.factory('ModalService', function ($modal) {
var service = {};
var currentModal;
var clearModal = function () {
currentModal = undefined;
};
service.getCurrentModal = function () {
return currentModal;
};
service.open = function (options) {
currentModal = $modal.open(options);
currentModal.result['finally'](clearModal);
return currentModal;
};
return service;
});
}());
2) In a controller, add an event listener to $routeChangeStart, this event will fire whenever someone hits the back button.
$scope.$on('$routeChangeStart', function(){
var currentModal = ModalService.getCurrentModal();
if(angular.isDefined(currentModal)){
currentModal.dismiss('cancel');
}
});
3) Your modals should now close when a user hits back.
4) Enjoy.
IMPROVEMENT:
I found the answer from HankScorpio to be the best out there. I wanted to include this snippet for those using ui-router and their recommendation for stateful modals.
1) I wanted the result.finally(...) to jump to a parent state;
2) I wanted to control the closing of the modal from $stateProvider config, NOT through rigging a controller and adding a listener to $routeChangeStart
Here is an example of a state that opens (and closes) it's modal:
.state('product.detail', {
url: '/detail/{productId}',
onEnter: /*open-modal logic*/,
onExit: ['ModalService', function (ModalService) { ModalService.close()} ]
})
I made ModalService aware of $state so that the result of closing a modal could jump to a parent view:
a. Add an isStateful flag to modalService.open(...):
service.open = function (options, isStateful) {
currentModal = $uibModal.open(options);
currentModal.result.finally(function () {
clearModal(isStateful);
});
return currentModal;
};
so that clearModal will return to previous state:
var clearModal = function (isStateful) {
currentModal = undefined;
if (isStateful)
$state.go('^');
};
Finally, add the closeModal() function called above (not a "stateful" close, simply a dismissal):
service.close = function() {
if (currentModal) {
currentModal.dismiss().then(function () {
clearModal();
})
}
}
The benefits of this are that back button functionality is controlled at state config level, not through a listener.
Here is my problem, I have two views (View1 and View2) and a controller for each view (Ctrl1 and Ctrl2). In View1 I'm trying to warn the user before he leaves the page accidentally without saving changes.
I'm using window.onbeforeunload, which works just fine, but my problem is that even though I have onbeforeunload only on one controller, the event seems to be triggered in the second controller as well! But I don't have even have that event in there. When the user leaves a page and there are unsaved changes, he gets warned by the onbeforeunload, if the user dismiss the alert and leaves the page, is he taken back to the second view, if the user tries to leave now this other view, we would also get warned about unsaved changes! When that's not even the case! Why is this happening?
I'm also using $locationChangeStart, which works just fine, but only when the user changes the route, not when they refresh the browser, hit 'back', or try to close it, that's why I'm forced to use onbeforeunload (If you a better approach, please let me know). Any help or a better approach would be greatly appreciated!
//In this controller I check for window.onbeforeunload
app.controller("Ctrl1", function ($scope, ...)){
window.onbeforeunload = function (event) {
//Check if there was any change, if no changes, then simply let the user leave
if(!$scope.form.$dirty){
return;
}
var message = 'If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?';
if (typeof event == 'undefined') {
event = window.event;
}
if (event) {
event.returnValue = message;
}
return message;
}
//This works only when user changes routes, not when user refreshes the browsers, goes to previous page or try to close the browser
$scope.$on('$locationChangeStart', function( event ) {
if (!$scope.form.$dirty) return;
var answer = confirm('If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?')
if (!answer) {
event.preventDefault();
}
});
}
//This other controller has no window.onbeforeunload, yet the event is triggered here too!
app.controller("Ctrl2", function ($scope, ...)){
//Other cool stuff ...
}
I tried checking the current path with $location.path() to only warn the user when he is in the view I want the onbeforeunload to be triggered, but this seems kinda 'hacky' the solution would be to not execute onbeforeunload on the other controller at all.
I added $location.path() != '/view1' in the first which seems to work, but it doesn't seem right to me, besides I don't only have two views, I have several views and this would get tricky with more controllers involved:
app.controller("Ctrl1", function ($scope, ...)){
window.onbeforeunload = function (event) {
//Check if there was any change, if no changes, then simply let the user leave
if($location.path() != '/view1' || !$scope.form.$dirty){
return;
}
var message = 'If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?';
if (typeof event == 'undefined') {
event = window.event;
}
if (event) {
event.returnValue = message;
}
return message;
}
//This works only when user changes routes, not when user refreshes the browsers, goes to previous page or try to close the browser
$scope.$on('$locationChangeStart', function( event ) {
if (!$scope.form.$dirty) return;
var answer = confirm('If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?')
if (!answer) {
event.preventDefault();
}
});
}
Unregister the onbeforeunload event when the controller which defined it goes out of scope:
$scope.$on('$destroy', function() {
delete window.onbeforeunload;
});
I just attempted the above solution, and that wasn't working for me. Even manually typing delete window.onbeforeunload in the console wouldn't remove the function. I needed to set the property to undefined instead.
$scope.$on('$destroy', function(e){
$window.onbeforeunload = undefined;
});
For those of you using angular-ui-router you would use is $stateChangeStart instead of $locationChangeStart, e.g.
$scope.$on('$stateChangeStart', function (event){
if (forbit){
event.preventDefault()
}
else{
return
}
})
As an update to this, for anyone used angular-ui-router I'd now pass $transitions into your controller and use;
$transitions.onStart({}, function ($transition)
{
$transition.abort();
//return false;
});
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.
I am writing a small Angular web application and have run into problems when it comes to loading the data. I am using Firebase as datasource and found the AngularFire project which sounded nice. However, I am having trouble controlling the way the data is being displayed.
At first I tried using the regular implicit synchronization by doing:
angularFire(ref, $scope, 'items');
It worked fine and all the data was displayed when I used the model $items in my view. However, when the data is arriving from the Firebase data source it is not formatted in a way that the view supports, so I need to do some additional structural changes to the data before it is displayed. Problem is, I won't know when the data has been fully loaded. I tried assigning a $watch to the $items, but it was called too early.
So, I moved on and tried to use the angularfireCollection instead:
$scope.items = angularFireCollection(new Firebase(url), optionalCallbackOnInitialLoad);
The documentation isn't quite clear what the "optionalCallbackOnInitialLoad" does and when it is called, but trying to access the first item in the $items collection will throw an error ("Uncaught TypeError: Cannot read property '0' of undefined").
I tried adding a button and in the button's click handler I logged the content of the first item in the $items, and it worked:
console.log($scope.items[0]);
There it was! The first object from my Firebase was displayed without any errors ... only problem is that I had to click a button to get there.
So, does anyone know how I can know when all the data has been loaded and then assign it to a $scope variable to be displayed in my view? Or is there another way?
My controller:
app.controller('MyController', ['$scope', 'angularFireCollection',
function MyController($scope, angularFireCollection) {
$scope.start = function()
{
var ref = new Firebase('https://url.firebaseio.com/days');
console.log("start");
console.log("before load?");
$scope.items = angularFireCollection(ref, function()
{
console.log("loaded?");
console.log($scope.items[0]); //undefined
});
console.log("start() out");
};
$scope.start();
//wait for changes
$scope.$watch('items', function() {
console.log("items watch");
console.log($scope.items[0]); //undefined
});
$scope.testData = function()
{
console.log($scope.items[0].properties); //not undefined
};
}
]);
My view:
<button ng-click="testData()">Is the data loaded yet?</button>
Thanks in advance!
So, does anyone know how I can know when all the data has been loaded
and then assign it to a $scope variable to be displayed in my view? Or
is there another way?
Remember that all Firebase calls are asynchronous. Many of your problems are occurring because you're trying to access elements that don't exist yet. The reason the button click worked for you is because you clicked the button (and accessed the elements) after they had been successfully loaded.
In the case of the optionalCallbackOnInitialLoad, this is a function that will be executed once the initial load of the angularFireCollection is finished. As the name implies, it's optional, meaning that you don't have to provide a callback function if you don't want to.
You can either use this and specify a function to be executed after it's loaded, or you can use $q promises or another promise library of your liking. I'm partial to kriskowal's Q myself. I'd suggest reading up a bit on asynchronous JavaScript so you get a deeper understanding of some of these issues.
Be wary that this:
$scope.items = angularFireCollection(ref, function()
{
console.log("loaded?");
console.log($scope.items[0]); //undefined
});
does correctly specify a callback function, but $scope.items doesn't get assigned until after you've ran the callback. So, it still won't exist.
If you just want to see when $scope.items has been loaded, you could try something like this:
$scope.$watch('items', function (items) {
console.log(items)
});
In my project I needed to know too when the data has been loaded. I used the following approach (implicit bindings):
$scope.auctionsDiscoveryPromise = angularFire(firebaseReference.getInstance() + "/auctionlist", $scope, 'auctionlist', []);
$scope.auctionsDiscoveryPromise.then(function() {
console.log("AuctionsDiscoverController auctionsDiscoveryPromise resolved");
$timeout(function() {
$scope.$broadcast("AUCTION_INIT");
}, 500);
}, function() {
console.error("AuctionsDiscoverController auctionsDiscoveryPromise rejected");
});
When the $scope.auctionsDiscoveryPromise promise has been resolved I'm broadcasting an event AUCTION_INIT which is being listened in my directives. I use a short timeout just in case some services or directives haven't been initialized yet.
I'm using this if it would help anyone:
function getAll(items) {
var deferred = $q.defer();
var dataRef = new Firebase(baseUrl + items);
var returnData = angularFireCollection(dataRef, function(data){
deferred.resolve(data.val());
});
return deferred.promise;
}