I'm on my third day working with Protractor and I'm constantly hitting bric walls in regards to waiting around for pages to load and elements to appear. This test case in particular has grown ugly and I would like to solve the issues without having to rely on sleeps.
I am currently "outside of the land of AngularJS"
it('it should reflect in both the field and the title when the personnel name is changed', function() {
var inputField, personnelHeader, personnelName;
personnelName = element(By.css(".overlay.editnameoverlay")).click();
personnelHeader = element(By.id("personnel_name_header"));
inputField = element(By.css("input[name='newvalue']"));
inputField.clear();
inputField.sendKeys("Test 123");
element(By.css("input[name='ok_button']")).click();
// browser.driver.sleep(2000); This test only works with this sleep added
browser.wait(function() {
console.log("Waiting for header to change...");
return personnelHeader.getText().then(function(text) {
return text === "Test 123";
});
}, 5000);
return expect(personnelHeader.getText()).toBe(personnelName.getText());
});
So the test here changes the name in an input field. submits it and waits for the changes to become reflected in the header of the modal. The problem is that without the browser.driver.sleep(2000) I get an error saying
Stacktrace:
StaleElementReferenceError: stale element reference: element is not attached to the page document
How do I go about solving this in this particular case?
From the documentation for Expect Conditions:
var EC = protractor.ExpectedConditions;
// Waits for the element with id 'abc' to contain the text 'foo'.
browser.wait(EC.textToBePresentInElement($('#abc'), 'foo'), 5000);
When you use Protractor to test for non-angular pages you're on your own regarding waiting for elements to be ready for interaction.
StaleElementReferenceError is probably the most useless selenium error, it happens when the element got removed from the DOM but is still cached somehow, I also suffered this problem when started with Protractor and even tried to convince it should be automatically retried Protractor-side.
The solution for me is to always explicitly wait for an element to appear on the page using a custom function waitReady() that browser.wait for elements ready, i.e: waits for the element to be ready for interaction:
expect($('#login_field').waitReady()).toBeTruthy();
First integrate this snippet in your code: https://gist.github.com/elgalu/2939aad2b2e31418c1bb
Not only the custom waitReady() waits for the element but it also swallows any unrelated useless webdriver error like StaleElementReferenceError and will simply retry up until finding the element or it will timeout.
So waitReady() each element before interacting, i.e. before clear() or sendKeys() or click() ...
// TODO: Move to Page Objects module
var personnelNameElm = $(".overlay.editnameoverlay");
var personnelHeaderElm = $("#personnel_name_header");
var inputFieldElm = $("input[name='newvalue']");
var okBtnElm = $("input[name='ok_button']");
it('it should reflect in both the field and the title when the ' +
'personnel name is changed', function() {
expect(personnelNameElm.waitReady()).toBeTruthy();
personnelNameElm.click();
expect(inputFieldElm.waitReady()).toBeTruthy();
inputFieldElm.clear().sendKeys("Test 123");
expect(okBtnElm.waitReady()).toBeTruthy();
okBtnElm.click();
browser.wait(function() {
console.log("Waiting for header to change...");
// Using waitReady() before getText() avoids Stale element errors
return personnelHeaderElm.waitReady().then(function() {
return personnelHeaderElm.getText().then(function(text) {
return text === "Test 123";
});
});
}, 5000);
expect(personnelHeaderElm.getText()).toEqual(personnelNameElm.getText());
});
Related
This question is closely related to the solutions given in this question
In my test script, I need to go to login script and need to logout in case the browser logs in automatically in the app. So following the solutions provided in the question How to create a condition in protractor for when an element exists or not, I have created this script:
beforeEach(function () {
browser.driver.manage().window().maximize();
browser.get(globalVariables.loginMain);
globalVariables.User_Menu_Dropdown.isDisplayed().then(function(Login_Menu) {
if (Login_Menu) {
globalVariables.User_Menu_Dropdown.click();
browser.wait(globalVariables.until.presenceOf(globalVariables.logOut_Button), 3000, 'The Logout menu too long to appear in the DOM');
globalVariables.logOut_Button.click();
browser.wait(globalVariables.until.presenceOf(globalVariables.Email_Input_box), 3000, 'The User Input box too long to appear in the DOM');
} else {
console.log("the app is on the login page")//do nothing
}
});
But when I run the script, I still get the following error
"Failed: No element found using locator: By(css selector, img[class="img-thumb-xs mr-1 align-middle"])". What am I doing wrong here? What is the best approach to achieve it?
you can use the ExpectedConditions in your case.
var EC = protractor.ExpectedConditions;
// Waits for the element with id 'abc' to be no longer visible on the dom.
browser.wait(EC.invisibilityOf($('#abc')), 5000);
or you can use the not condition which will lead to the same result
var EC = protractor.ExpectedConditions;
// Waits for the element with id 'abc' to be no longer visible on the dom.
browser.wait(EC.not(EC.visibilityOf($('#abc'))), 5000);
My test steps to test a logout flow are,
1) click a logout button
2) wait for url change to login.html
3) wait for login page text fields to be loaded.
Code looks like,
//wait for logout menu/button
browser.wait(function(){
return element(by.buttonText('Log out')).isPresent()
})
element(by.buttonText('Log out')).click()
//wait for url to change to login.html
browser.wait(function(){
return browser.getCurrentUrl().then(function(url){
return url.indexOf("login") != -1
})
})
//wait for login page text boxes
browser.wait(function(){
return element(by.css('[type=text]')).isPresent()
})
This makes my code lengthy since, i am wrapping each action inside a browser.wait call.
Is there a way i can avoid browser.wait. I tried adding,
browser.manage().timeouts().implicitlyWait(5000)
But then, i get No element found using locator: by.buttonText("Log out") error.
Protractor has inbuilt ExpectedConditions checks, you don't have to write a custom function yourself all the time. Here's how to wait for an element to be visible -
var EC = protractor.ExpectedConditions;
browser.wait(EC.presenceOf(element(by.buttonText('Log out'))), 10000); //Checks only if element is present in DOM
browser.wait(EC.visibilityOf(element(by.buttonText('Log out'))), 10000); //Checks if element is present in DOM and visible to user on page
You can also create a custom function as shown in another answer. Hope this helps.
If you are doing the same thing more than once, it tends to help to make a helper function for it. Add to your helper file something like:
this.waiterFunc = function(element){
browser.wait(function() {
return element.isPresent();
})
};
then in your main class you can do things like:
helper.waiterFunc(element(by.buttonText('Log out')));
Im not sure about avoiding browser.wait as its an integral step in the process of waiting for the elements to be accessible at a particular time. If you dont wait, then you are going to start getting nasty "element not clickable at time x" errors
I'm writing automation tests for a website using Mocha + SeleniumServer + wd.js + chai-as-promised.
The website uses JavaScript for the front-end which seems to refresh the elements on the page when certain action is performed. i.e. Upon selecting an element in a grid, the "next" button is enabled to allow user to move on to the next page. It seems that this changes the reference to the button element resulting in the StaleElementReference error.
describe('1st step', function () {
it('should select an element is grid', function () {
return browser
.waitForElementByCss('#grid', wd.asserters.isDisplayed, 20000)
.elementByCss('#grid .elementToBeSelected')
.click()
.sleep(1000)
.hasElementByCss('#grid elementToBeSelected.active')
.should.eventually.be.true;
});
it('should proceed next step', function () {
return browser
.waitForElementByCss('.btnGrid .btn.nextBtn:not(.disabled)', wd.asserters.isDisplayed, 20000)
.elementByCss('.btnGrid .btn.nextBtn:not(.disabled)')
.click()//Error thrown here
.sleep(2000)
.url()
.should.eventually.become('http://www.somewebsite.com/nextpage');
});
});
With my limited experience with JavaScript, I have tried all that i could think off, but to no avail. So is there anyway I can avoid this StaleElementReference error? Also, the error is only sometimes thrown during execution.
You might want to read some more on the Stale Element Reference exception. From what you are describing, it sounds like you get a reference to an element, do something on the page which then changes/removes the referenced element. When you do something with the variable reference you get this error. The solution really depends on the code you are using to do your tests and your framework for accessing elements. In general, you need to be aware of when you perform an action that changes the page and refetch the element before you access it. You could always refetch an element before you access it, you could refetch all elements that are affected by a page change, and so on...
You code probably looks something like this
WebElement e = driver.findElement(...); // get the element
// do something that changes the page which, in turn, changes e above
e.click(); // throws the StaleElementReference exception
What you probably want is something more like one of these...
Don't fetch the element until you need it
// do something that changes the page which, in turn, changes e above
WebElement e = driver.findElement(...); // get the element
e.click(); // throws the StaleElementReference exception
...or fetch it again right before you need it...
WebElement e = driver.findElement(...); // get the element
// do something that changes the page which, in turn, changes e above
e = driver.findElement(...); // get the element
e.click(); // throws the StaleElementReference exception
I would prefer the first fix... just fetch what you need when you need it. That should be the most efficient way to solve this problem. The second fix might have performance issues because you might be refetching a bunch of elements over and over and either never using them or refetching them 10 times only to reference the element once at the end.
I am new to Protractor. I think I have this down when dealing with an Angular page, but can't figure it out for a non-Angular page. Any help would be appreciated.
describe('Search', function() {
it('should click Search button and wait for results', function() {
browser.driver.findElement(by.id('search')).click();
});
});
Testing non-angular pages with Protractor can be tricky regarding waiting for stuff.
I suggest you upgrade Protractor to latest (1.5.0 as of now), use a custom function waitReady() that browser.wait for elements ready and rewrite your test like below. Note you can put everything within 1 spec if you like so.
// TODO: use page objects
var searchBtnElm = $('#search'); // use element(by.id('search')) if you prefer
it('waits for the elements present and visible (non-angular)', function() {
expect(searchBtnElm.waitReady()).toBeTruthy();
});
it('should click Search button', function() {
searchBtnElm.click();
});
it('wait for more results', function() {
// keep using waitReady() before interacting with the elements
// and before performing expectations on them
});
More details of why waitReady here.
Note: remember to set ignore synchronization for testing a non-angular page:
browser.ignoreSynchronization = true;
You can set it before browser.get the non-angular page.
I've suggested setting a high implicit wait in the past, e.g.
browser.manage().timeouts().implicitlyWait(5000);
That hack allows to you avoid waitReady and keep using the standard
expect(searchBtnElm.isPresent()).toBeTruthy();
But has an ugly disadvantage when testing for elements NOT present, i.e. when testing for absent or non visible elements in which case it will wait 5 seconds (5000ms) in vane, e.g. when doing
expect(someNonExistingElm.isPresent()).toBeFalsy();
Figured this out. I simply added the code below, after the click method:
describe('Search', function() {
it('should click Search button and wait for results', function() {
browser.driver.findElement(by.id('search')).click();
dvr.wait(function() {
return dvr.isElementPresent(by.xpath(
'/html/body/div/div[4]/div/div[2]/div/div/div/span'));
}, 20000);
});
});
Another Neat approach is to use "Expected Conditions" inside browser.wait - something like this:
var EC = protractor.ExpectedConditions;
var search = element(by.id('search'))
browser.wait(EC.visibilityOf(search), 2000).then(function(){
search.click()
})
You can get more details here: https://angular.github.io/protractor/#/api?view=ExpectedConditions
In protractor there are two types terms for on the page. isPresent ask if the element is exists on the page. isDisplayed asks if the element is visible. If you are waiting for a page to load you need to wait for isDisplayed, but that will error if it is not present, so wait for isPresent first. I use a function to wait for an element.
function waitForElement(el, waitTimeoutMilliseconds){
return browser.wait(function() { return el.isPresent(); }, waitTimeoutMilliseconds)
.then(function(){
return browser.wait(function() { return el.isDisplayed(); }, waitTimeoutMilliseconds);
});
}
Then just call that function in your test.
describe('Search', function() {
it('should click Search button and wait for results', function() {
var el = element(by.id('search'));
waitForElement(el, 5000);
el.click();
});
});
I am trying to scrape a webpage which has a form with many dropdowns and values in the form are interdependent. At many point I need the code to wait till the refresh of the page complete. Eg after selecting an option from the list, the code should wait till the next list is populated based on this selection. It would be really helpful if someone could give pointers because strangely my code is working only after I gave so much unnecessary logging statements which in-turn created some delay. Any suggestions to improve the code would be very helpful.
var casper = require('casper').create({
verbose: true,
logLevel: 'debug',
userAgent: 'Mozilla/5.0 poi poi poi (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.172 Safari/537.22',
pageSettings: {}
});
casper.start('http://www.abc.com', function () {
console.log("casper started");
this.fill('form[action="http://www.abc.com/forum/member.php"]', {
quick_username: "qwe",
quick_password: "qwe"
}, true);
this.capture('screen.png');
});
casper.thenOpen("http://www.abc.com/search/index.php").then(function () {
this.click('input[type="checkbox"][name="firstparam"]');
this.click('a#poi');
casper.evaluate(function () {
document.getElementsByName("status")[0].value = 1;
document.getElementsByName("state")[0].value = 1078;
changeState(); //This function is associated with the dropdown ie state
and the page reloads at this point. Only after complete refresh the code shoud execute! How can this be achieved?
return true;
});
this.echo('Inside the first thenOpen' + this.evaluate(function () {
return document.search.action;
}));
});
casper.then(function () {
this.capture("poi.png");
console.log('just before injecting jquery');
casper.page.injectJs('./jquery.js');
this.click('input[type="checkbox"][name="or"]');
this.evaluate(function () {
$('.boxline .filelist input:checkbox[value=18127]').attr("checked", true);
});
this.echo('Just before pressing the add college button' + this.evaluate(function () {
return document.search.action;
}));
this.capture('collegeticked.png');
if (this.exists('input[type="button"][name="niv"]')) {
this.echo('button is there');
} else {
this.echo('button is not there');
}
this.echo("Going to print return value");
this.click('input[type="button"][name="poi"]'); // This click again causes a page refresh. Code should wait at this point for completion.
this.echo('Immediately after pressing the add college btn getPresentState()' + this.evaluate(function () {
return getPresentState();
}));
this.echo('Immediately after pressing add colleg button' + this.evaluate(function () {
return document.search.action;
}));
this.capture('iu.png');
});
casper.then(function () {
console.log('just before form submit');
this.click('form[name="search"] input[type="submit"]'); //Again page refresh. Wait.
this.echo('Immediately after search btn getPresentState()' + this.evaluate(function () {
return getPresentState();
}));
this.echo('Immediately after search button-action' + this.evaluate(function () {
return document.search.action;
}));
this.capture("mnf.png");
});
casper.then(function () {
casper.page.injectJs('./jquery.js');
this.capture("resultspage.png");
this.echo('Page title is: ' + this.evaluate(function () {
return document.title;
}), 'INFO');
var a = casper.evaluate(function () {
return $('tbody tr td.tdbottom:contains("tye") ').siblings().filter($('td>a').parent());
});
console.log("ARBABU before" + a.length);
});
casper.run();
I've been using the waitForSelector 'workaround' mentioned by Arun here:
https://stackoverflow.com/a/22217657/1842033
It's the best solution I've found; the 'drawback' as it were is that you need to be aware of what element you're expecting to load. I say drawback, personally I don't think I've encountered a situation where I've not had some kind of feedback saying that whatever I'm waiting for has happened
this.waitForSelector("{myElement}",
function pass () {
test.pass("Found {myElement}");
},
function fail () {
test.fail("Did not load element {myElement}");
},
20000 // timeout limit in milliseconds
);
Although I'd guess you could use waitForResource() or something like that if you didn't have visual feedback.
What I've taken to doing to get around this issue, when there isn't anything specific to target and wait for in the reloaded page, is to use the following:
var classname = 'reload-' + (new Date().getTime()),
callback = function(){},
timeout = function(){};
/// It happens when they change something...
casper.evaluate(function(classname){
document.body.className += ' ' + classname;
}, classname);
casper.thenClick('#submit'); /// <-- will trigger a reload of the page
casper.waitWhileSelector('body.' + classname, callback, timeout);
This way I don't have to rely on a specific expected element in the next page, I've basically done the inverse. I've created a specific selector to watch out for, and execution moves on once that selector fails to match.
For my intents and purposes it was enough to know the page had begun reloading, I didn't need to wait until the next page had fully reloaded. This is so that I could then trigger certain waitForSelector calls on elements that may have existed both before and after the reload. Waiting until the temporary class has been removed lets me know that anything that existed before has since been destroyed, so no fear of selecting elements prior to the reload.
Seems there are no real solutions.
https://casperjs.readthedocs.io/en/latest/modules/casper.html#waitforselector is an available workaround which may not work always.
I have the same experience doing the same thing as you. script these way in user perspective never gone well. it crash in middle of nowhere and very unreliable. I was doing search from salesforce that also require login.
You need to keep your step as minimum as possible. script in a cron job way. don't do form fill/button click unless you are doing UI testing. I would advice you to break the process into two parts
// this part do search and find out the exact url of your screen capture.
// save it in a db/csv file
1 - start by POST to http://www.abc.com/forum/member.php with username password in body.
2 - POST/GET to http://www.abc.com/search/index.php with your search criteria, you look at what the website require. if they do POST, then POST.
// second part read your input
1 - login same as first part.
2 - casper forEach your input save your capture. (save the capture result in db/csv)
my script now is pure phantomjs, casper script just keep crashing for no reason. even phantomjs is unreliable. I save the result/status on each successful search/download, whenever there is error I exit the script if not the rest of result is unpredictable(good result in chrome turn out bad in phantomjs).
I found this question when searching for solution to a problem where click() or fill() action reloads exactly the same data in a child iframe. Here is my improvement to Pebbl answer:
casper.clickAndUnload = function (click_selector, unload_selector, callback, timeout) {
var classname = 'reload-' + (new Date().getTime());
this.evaluate(function (unload_selector, classname) {
$(unload_selector).addClass(classname);
}, unload_selector, classname);
this.thenClick(click_selector);
this.waitWhileSelector(unload_selector + '.' + classname, callback, timeout);
};
casper.fillAndUnload = function (form_selector, data, unload_selector, callback, timeout) {
var classname = 'reload-' + (new Date().getTime());
this.evaluate(function (unload_selector, classname) {
$(unload_selector).addClass(classname);
}, unload_selector, classname);
this.fill(form_selector, data, true);
this.waitWhileSelector(unload_selector + '.' + classname, callback, timeout);
};
This solution assumes that page uses jQuery. It should not be hard to modify it for pages that don't. unload_selector is an element that is expected to be reloaded after click or form submission.
Since Casperjs is written for developers, it's expected one knows what state the page loaded should be in, and what elements should be available to define a page-loaded state.
One option is to check for the presence of, for example, a javascript resource that is loaded at the end of the page.
When running any type of test, results must be reproducable each time and therefore idempotency is essential. For this to happen, the tester must be able to control the environment enough to make this happen.
Just evaluate document.readyState to be complete or interactive. Then it's loaded.
This is an implementation with a while, but maybe can be done with interval...
this.then(function () {
while(this.evaluate(function () { return document.readyState != 'complete' && document.readyState != 'interactive'; })) {}
});