Proctractor is designed for AngularJS applications, it will
wait for web pages to settle down before acting on web elements (so I heard,
I’ve never tried Proctrator on AngularJS applications). Protractor can be used
to test non-AngularJS applications, all you have to do is to put browser.ignoreSynchronization
= true; however, you have to do more to make automation more
stable.
This blog shows some tricks I used to make my automation
framework more stable.
Instability
Any UI action, be it to set text on an input, or click on a
button, has to wait for the element to be settled down. If for example, when
the page is being refreshed, the test case tries to click a button, you might
run into issues such as:
- No such element
- StaleElementReferenceError: stale element reference: element is not attached to the page document
- Element is not clickable at point(…)
It is easy to test this out.
Write a simple html containing a button, it’s clicking
action will cause the page to be refreshed:
<html decorator="blank" title="Hello Apple"><head><script>function refresh(){setTimeout(doRefresh, 300);}function doRefresh(){document.location.reload();}</script></head><body><button type="button" id="refresh" onclick="refresh()">Refresh</button></html>
And write a simple automation test to keep clicking the
button:
You should see such errors logged (not frequently though):beforeEach(function () { browser.ignoreSynchronization = true; }); it('test_clickWhenPageBeingRefreshed', function () { browser.get('http://localhost:8080/examples/test.html') .then(function() { co(function*(){ for(var i=0; i<500; i++) { yield element(by.id("refresh")).click().then(function () { console.log("clicked"); return true; }, function (err) { console.log("Oops!" + err); return false; }) } }) }); }) })
Oops!StaleElementReferenceError: stale element reference: element is not attached to the page document(Session info: chrome=43.0.2357.124)(Driver info: chromedriver=2.14.313457 (3d645c400edf2e2c500566c9aa096063e707c9cf),platform=Windows NT 6.1 SP1 x86_64) (WARNING: The server did not provide any stacktrace information)
Wait until an element is displayed/enabled
browser.wait() can be used to wait for a condition to
become true: the condition will be checked frequently, once the condition is
met or the condition fails to be met after the specified time, browser.wait()
returns.
Since Protractor has a method element.isDisplayed to check
whether an element is visible, my first attempt was to wait for this method to
return true in browser.wait(),
however, this method might throw out errors when the element is not stable:
it('test_clickWhenPageBeingRefreshed', function () { browser.get('http://localhost:8080/examples/test.html') .then(function() { co(function*(){ for(var i=0; i<1000; i++) { var ele= element(by.id("refresh")); var visible=yield browser.driver.wait(function () { return ele.isPresent().then(function(present){ return present && ele.isDisplayed().then(function(isDisplayed){return isDisplayed;}, function(error){ console.log("Oops! isDisplayed met an error:"+error); return false;}); }); }, 100,"wait for element to be visible").then(function(visible0){ console.log("visibility is:"+visible0); return visible0; }, function(error){ console.log("Oops! waiting met an error:"+error); }); if(visible) { yield ele.click().then(function () { }, function (error) { console.log("Oops!Clicking met an error:" + error); }); } } }) }); })
Running this program for a while, you can see element.isDisplayed()
throws out these errors:
Oops! waiting met an error:StaleElementReferenceError: stale element reference: element is not attached to the page document
Oops! waiting met an error:NoSuchElementError: No element found using locator: By.id("refresh")
The same thing happens to element.isEnabled().
So these two methods element.isDisplayed() and element.isEnabled()
can’t be used directly to verify an UI element’s status. The solution is to
capture errors from element.isDisplayed() and element.isEnabled() and swallow
them.
Util.prototype.isElementDisplayedOrEnabled_ = function (element, identity, testEnable) { var uponError = true; var ret = false; var self=this; return co(function* () { var i = 0; while (uponError && i < self.TRY_TIMES) { var p; if (testEnable) { p = element.isEnabled(); } else { p = element.isDisplayed(); } yield p.then(function (testResult) { uponError = false; ret = testResult; return ret; }, function (error) { uponError = true; console.log("Oops!isElementDisplayedOrEnabled_ when waiting for " + identity + "to be visible/enable, met an error:" + error); ret=false; i++; }); } return ret; }); }
isElementDisplayedOrEnabled_ will catch errors and try TRY_TIMES times.
This method is used inside browser.wait() to wait for an element to become visible or
enabled or fail after TRY_TIMES times:
Util.prototype.waitElementUntilVisibleOrEnable_=function(element, identity,
options) {
if(_.isUndefined(identity)){
identity="";
}
if(_.isUndefined(options)){
options={};
}
options.mustBeTrue=this.isDefined(options.mustBeTrue)?options.mustBeTrue:true;
options.expecation=this.isDefined(options.expecation)?options.expecation:true;
options.testEnable=this.isDefined(options.testEnable)?options.testEnable:false;
options.waitTime=this.isDefined(options.waitTime)?options.waitTime:this.TIMEOUT;
var self=this;
var waitFun=function(){
if(options.expecation){
return element.isPresent().then(function(present){
return present && self.isElementDisplayedOrEnabled_(element, identity, options);
});
}else{
return element.isPresent().then(function(present){
return !present || !self.isElementDisplayedOrEnabled_(element, identity, options);
});
}
}
return browser.driver.wait(function () {
return waitFun();
}, options.waitTime, "wait for element to be visible:"+identity).then(function(){
console.log("waitElementUntilVisibleOrEnable_("+JSON.stringify(options)+") on "+identity+":"+options.expecation);
return options.expecation;
}, function(error){
console.log("Oops!waitElementUntilVisibleOrEnable_("+JSON.stringify(options)+") on "+identity+" met an error:"+error);
if(options.mustBeTrue){
throw new Error("Oops!waitElementUntilVisibleOrEnable_("+JSON.stringify(options)+") on "+identity+" met an error:"+error);
}else{
return !options.expecation;
}
});
}
if(_.isUndefined(identity)){
identity="";
}
if(_.isUndefined(options)){
options={};
}
options.mustBeTrue=this.isDefined(options.mustBeTrue)?options.mustBeTrue:true;
options.expecation=this.isDefined(options.expecation)?options.expecation:true;
options.testEnable=this.isDefined(options.testEnable)?options.testEnable:false;
options.waitTime=this.isDefined(options.waitTime)?options.waitTime:this.TIMEOUT;
var self=this;
var waitFun=function(){
if(options.expecation){
return element.isPresent().then(function(present){
return present && self.isElementDisplayedOrEnabled_(element, identity, options);
});
}else{
return element.isPresent().then(function(present){
return !present || !self.isElementDisplayedOrEnabled_(element, identity, options);
});
}
}
return browser.driver.wait(function () {
return waitFun();
}, options.waitTime, "wait for element to be visible:"+identity).then(function(){
console.log("waitElementUntilVisibleOrEnable_("+JSON.stringify(options)+") on "+identity+":"+options.expecation);
return options.expecation;
}, function(error){
console.log("Oops!waitElementUntilVisibleOrEnable_("+JSON.stringify(options)+") on "+identity+" met an error:"+error);
if(options.mustBeTrue){
throw new Error("Oops!waitElementUntilVisibleOrEnable_("+JSON.stringify(options)+") on "+identity+" met an error:"+error);
}else{
return !options.expecation;
}
});
}
options.expecation is to specify whether an UI element is expected to be
visible/enable or the reverse. options.mustBeTrue is to define whether the
expectation must be met, if true, when the expectation fails, waitElementUntilVisibleOrEnable_ throws out an error (error handling is
another complex topic, and I will write another blog to discuss it).
waitElementUntilVisibleOrEnable_ is used before any actions on UI
elements, for example:
Util.prototype.findByID=function(id) {
var xpath='//*[@id="'+id+'"]';
return this.findByXPath(xpath);
}
Util.prototype.findByXPath = function (xpath,last) {
if(last){
xpath="("+xpath+")[last()]";
}
var ele = element(by.xpath(xpath));
this.waitElementUntilVisibleOrEnable_(ele, xpath);
return ele;
}
Util.prototype.findByName=function(name) {
var ele = element(by.name(name));
this.waitElementUntilVisibleOrEnable_(ele, name);
return ele;
}
var xpath='//*[@id="'+id+'"]';
return this.findByXPath(xpath);
}
Util.prototype.findByXPath = function (xpath,last) {
if(last){
xpath="("+xpath+")[last()]";
}
var ele = element(by.xpath(xpath));
this.waitElementUntilVisibleOrEnable_(ele, xpath);
return ele;
}
Util.prototype.findByName=function(name) {
var ele = element(by.name(name));
this.waitElementUntilVisibleOrEnable_(ele, name);
return ele;
}
Check post conditions
My system under test is really clunky: sometimes after
clicking an UI element, for various reasons, the actions supposed to happen
(e.g. forwarding to another page) didn’t happen, and the UI element has to be
clicked again. Before the system under test solves this issue, I have to use
some trick to handle it.
The solution is to define a post condition, after clicking
on UI element, check if the post condition is met, if not, continue to click on
the UI element.
Util.prototype.clickElement=function(ele,identity, postCondition){
var self=this;
var clickedTimes=0;
var self=this;
var clickedTimes=0;
return co(function*(){
var clickSuccessful=yield self.doClick_(ele, identity, postCondition).then(function(ret){return ret;}, function(err){throw err;});
while(!clickSuccessful && clickedTimes < self.TRY_TIMES ){
console.log("clickElement:click "+identity+" again, try at "+clickedTimes+" times");
browser.sleep(self.TINY_TIMEOUT);
clickedTimes++;
clickSuccessful=yield self.doClick_(ele, identity, postCondition).then(function(ret){return ret;}, function(err){throw err;});
}
if(!clickSuccessful){
console.log("clickElement:click failed at "+identity);
throw new Error ("clickElement:click failed at "+identity);
}
return true;
});
}
Util.prototype.doClick_=function(ele,identity,postCondition){
var self=this;
return co(function*(){
var clickable= yield self.waitElementUntilClickable_(ele, identity);
if(!clickable){
return false;
}
var uponStaleError=true;
var success=false;
var i=0;
while(uponStaleError && i<Util.prototype.TRY_TIMES) {
yield ele.click().then(function(){
console.log("doClick_:clicked "+identity);
uponStaleError=false;
success=true;
}, function(error){
uponStaleError = true;
i++;
console.log("Oops!doClick_:click"+identity+" again at "+i+" time, error is:" + error);
browser.sleep(Util.prototype.TINY_TIMEOUT);
});
}
if(!success ){
return false;
}
if(self.isDefined(postCondition)){
var visible;
if(self.isDefined(postCondition.id)) {
visible=yield self.isElementByIdVisible(postCondition.id, postCondition.visible).then(function(ret){return ret;}, function(){});
}else if(self.isDefined(postCondition.xpath)){
visible=yield self.isElementByXPathVisible(postCondition.xpath, postCondition.visible).then(function(ret){return ret;}, function(){});
}else if(self.isDefined(postCondition.name)){
visible=yield self.isElementByNameVisible(postCondition.name, postCondition.visible).then(function(ret){return ret;}, function(){});
}
if(visible===postCondition.visible) {
console.log("doClick_:click successful, " + JSON.stringify(postCondition) + " matches!");
return true;
}else{
console.log("doClick_:click "+identity+" failed because postCondition doesn't match:"+JSON.stringify(postCondition));
return false;
}
}else{
return success;
}
})
}
As an example, this following statement clicks the button
“Close Task”, if the clicking is
successful, the page should be forwarded to another one and the “Close Task”
button should disappear. It checks if “Close Task” is still visible, if so, it
will continue to click the button, until the button disappears or it throws out
an error after a number of tries:
util.clickToolbarButton("Close Task",{visible:false, xpath:"(//button[text()='Close Task'])[last()]"})
Wait until cursor is not busy
Unlike human action, with automation, when the cursor is
busy (the cursor icon is a spinning circle), Protractor can still drive UI
actions. The following test proves this:
//html
<html
decorator="blank" title="Hello Apple">
<head>
<script>
function wait(){
document.body.style.cursor = 'wait';
}
</script>
</head>
<body>
<button type="button"
id="refresh" name="visible"
onclick="refresh()">Refresh</button>
<button type="button"
id="wait" name="visible"
onclick="wait()">Wait</button>
<input type="text"
id="text1" />
<button type="button"
id="b1" name="visible">B1</button>
</html>
//test case
it('test_whenMouseBusy',
function ()
{
browser.get('http://localhost:8080/examples/test.html')
.then(function() {
co(function*(){
yield util.clickById("wait");
yield browser.sleep(5000);
yield util.setTextById("text1", "abc");
yield util.clickById("b1");
browser.pause();
})
});
})
browser.get('http://localhost:8080/examples/test.html')
.then(function() {
co(function*(){
yield util.clickById("wait");
yield browser.sleep(5000);
yield util.setTextById("text1", "abc");
yield util.clickById("b1");
browser.pause();
})
});
})
When the cursor was spinning, setting text on an input and
clicking on another button still took place.
It makes automation faster, but it caused some problem for
my test cases. On many occasions, I had to wait until the cursor is not busy
before doing the next UI action.
For my system under test, the solution turned to be, not
waiting for the cursor’s class to change, but for the body’ class to change:
Util.prototype.waitUntilPageSettled=function() {
this.switchToMain();
var self=this;
return browser.driver.wait(function () {
return element(by.xpath("//body")).getAttribute("class").then(function(v){
return v.indexOf("x-masked")<0;
})
},
this.TIMEOUT, "waitUntilPageSettled:wait for page to be settled")
.then(function(){
console.log("waitUntilPageSettled:page settled ");
return self.switchToDetailIFrame();
}, function(error) {
this.switchToMain();
var self=this;
return browser.driver.wait(function () {
return element(by.xpath("//body")).getAttribute("class").then(function(v){
return v.indexOf("x-masked")<0;
})
},
this.TIMEOUT, "waitUntilPageSettled:wait for page to be settled")
.then(function(){
console.log("waitUntilPageSettled:page settled ");
return self.switchToDetailIFrame();
}, function(error) {
console.log("Oops!waitUntilPageSettled met an error:"+error);
return self.switchToDetailIFrame();
return self.switchToDetailIFrame();
});
}
}
These tricks help make my automation test cases more stable.
And they also help in verifying performance dowgrade. Waiting happens inside waitElementUntilVisibleOrEnable_ and waitUntilPageSettled, by controlling
the waiting time, I can check whether a version of the system under test is
slower than the previous one.
No comments:
Post a Comment