Reputation: 3172
I am in the process of developing an automated test suite for an AngularJS app using Protractor.
While developing my test script, I have been using browser.pause()
so that I manually have to tell it to continue with each step of the tests while I'm executing the script. I am now at the point where I'm happy that my tests are executing correctly, and want to remove the calls to browser.pause()
, so that I can just let the script run through to completion on its own.
I am aware however, that I won't be able to just remove the calls to browser.pause()
without adding something in to allow my tests to pause/ wait for the browser to load before executing the next step (currently, the time taken for me to tell the script to continue after a call to browser.pause()
is run is enough time for the browser to have loaded the elements required for the next step of the test).
I am trying to use browser.wait()
to do this, passing the last line of each test as a parameter to browser.wait()
, along with a timeout value (i.e. 10 seconds). For example:
it('should log the user in to the application', function() {
browser.wait(loginAsUser(username, password), 10000);
});
where that test was originally just:
browser.pause();
it('should log the user in to the application', function() {
loginAsUser(username, password);
});
i.e. the call to browser.pause()
outside of the test case would cause the browser to pause in between each step of each test.
The loginAsUser()
function is defined with:
function loginAsUser(username, password) {
usernameInputField.sendKeys(username);
passwordInputField.sendKeys(password);
loginBtn.click();
}
When I currently run my test script, having added browser.wait()
to the last executable line of each test as with the log in test, I get the following failure on the first test (the log in one above), and the remaining tests all fail because they are dependent on that one passing:
Failed: Wait condition must be a promise-like object, function, or a Condition object
I don't understand why I'm getting this error, since the wait
condition is a function... it's being given the loginAsUser()
function that I have defined above...? Can anyone explain to me what I'm doing wrong here?
Edit
So, it seems the problem is actually with the rest of my tests (i.e. the login
test is run first, then a number of other tests are run sequentially after it).
With my login test as it was originally, the test case currently logs in correctly, however the next test to be run fails, giving the error:
Failed: No element found using locator: By(link text, Pages)
It seems that this is failing because the page hasn't had time to load following the log in, which it did have when I was running the tests with the calls to browser.pause()
.
The next test to be run is:
it('should display the Pages menu', function() {
browser.waitForAngularEnabled(false);
browser.actions().mouseMove(pagesMenuBtn).perform();
expect(pageTagBrowserBtn.isDisplayed()).toBeTruthy();
browser.actions().mouseMove(userCircle).perform();
expect(pageTagBrowserBtn.isDisplayed()).toBeFalsy();
browser.waitForAngularEnabled(true);
});
The pagesMenuBtn
is defined as a global variable with:
var pagesMenuBtn = element(by.linkText("Pages"));
So, it seems that I need to somehow give my application time for the page to load following the login, before running this next test, or else the element won't be found.
Edit
I tried adding a call to browser.wait()
within the 'Pages' test, so that the test would wait for the button to be displayed before hovering the cursor over it:
it('should display the Pages menu', function() {
browser.waitForAngularEnabled(false);
browser.wait(function() {
pagesMenuBtn.isDisplayed().then(function(isDisplayed){
/* if(!isDisplayed) {
console.log("Display Pages menu test returning false ");
return false;
}
console.log("Display Pages menu test returning true "); */
//return true;
browser.actions().mouseMove(pagesMenuBtn).perform().then(function(isDisplayed){
expect(pageTagBrowserBtn.isDisplayed()).toBeTruthy();
});
browser.actions().mouseMove(userCircle).perform().then(function(isDisplayed){
expect(pageTagBrowserBtn.isDisplayed()).toBeFalsy();
});
});
}, 5000);
});
but I still get the same error:
Failed: No element found using locator: By(link text, Pages)
indicating that the button that the test is trying to hover over can't be found (i.e. it doesn't appear to have loaded in the browser at the point at which the test script is trying to click on it).
Edit
Ok, so I've updated my test again- it now looks like this:
it('should display the Pages menu', function() {
browser.waitForAngularEnabled(false);
browser.wait(EC.visibilityOf(pagesMenuBtn), 5000).then(
browser.actions().mouseMove(pagesMenuBtn).perform().then(function(){
expect(pageTagBrowserBtn.isDisplayed()).toBeTruthy();
})).then(
browser.actions().mouseMove(userCircle).perform().then(function(){
expect(pageTagBrowserBtn.isDisplayed()).toBeFalsy();
}));
});
My intention for this was that the browser would wait for the pagesMenuBtn
element to be displayed, and then, once it was, the cursor would move to the button, and once that had happened, it should check whether the pageTagBrowserBtn
element was displayed (expecting it to return a 'true' value). The cursor would then move to another element on the page (userCircle
), and check again whether the the pageTagBrowserBtn
was displayed (this time expecting it to return a 'false' value).
However, when I now run my test, it fails, stating that:
Expected false to be truthy
I'm not sure why this is...? As I understand, the test should wait for the condition of EC.visibilityOf(pagesMenuBtn)
to return true before it tries to continue with the test... so I wouldn't expect it to be failing due to the value being false at all- if the value is false, it should wait until it's true before continuing- at least that's my intention from what I've written.
Can anyone explain to me what's going wrong here?
Upvotes: 2
Views: 4362
Reputation: 14307
The reason you are getting the error is because, wait method needs a wait for a condition to hold or promise to be resolved.
For example lets say after loginBtn.click();
you are waiting for an element with id
abc
to be visible, you would write something like below. Check ExpectedConditions for detail
var EC = protractor.ExpectedConditions;
// Waits for the element with id 'abc' to be visible on the dom.
browser.wait(EC.visibilityOf($('#abc')), 5000);
Or you want to wait for a certain custom condition (like wait for this element to be displayed),
browser.wait(function(){
element.isDisplayed().then(function(isDisplayed){
if(!isDisplayed) {
return false; //keep looking until its visible;
}
return true; //exits right here unless the timeout is already met
});
},5000);
More information straight from their docs
Example: Suppose you have a function, startTestServer, that returns a promise for when a server is ready for requests. You can block a WebDriver client on this promise with:
Example Code
var started = startTestServer(); browser.wait(started, 5 * 1000, 'Server should start within 5 seconds'); browser.get(getServerUrl());
pageTagBrowserBtn.isDisplayed()
returns a promise and not a boolean, so if you are using chai expect, you would need to do something like below
it('should display the Pages menu', function() {
browser.waitForAngularEnabled(false);
browser.wait(EC.visibilityOf(pagesMenuBtn), 5000).then(
browser.actions().mouseMove(pagesMenuBtn).perform().then(function(){
pageTagBrowserBtn.isDisplayed().then(function(isDisplayed) {
//check isDisplayed is true here
});
})).then(
browser.actions().mouseMove(userCircle).perform().then(function(){
expect(pageTagBrowserBtn.isDisplayed()).toBeFalsy();
}));
});
Upvotes: 2
Reputation: 7108
Generally you should never have to wait for a hardcoded period of time.
At least the waiting should be paired with an expected condition in order to break free as soon as the condition is met.
Example helper method:
public static async waitForPresenceOf(element: ElementFinder, waitMs?: number): Promise<boolean> {
return browser.wait(EC.presenceOf(element), waitMs || 5000).then(() => true, () => false);
}
It is possible in the same manor to wait for visibility of elements and the like.
I will post my current collection of helper methods in case you can use one or more of them (written in TypeScript):
import * as webdriver from 'selenium-webdriver';
import By = webdriver.By;
import {browser, element, protractor, by, WebElement, ElementFinder, ElementArrayFinder} from "protractor";
import * as _ from 'lodash';
import {expect} from "./asserts.config";
let EC = protractor.ExpectedConditions;
export default class BrowserHelper {
public static isAuthenticated: boolean;
public static async getCurrentUrl(): Promise<string> {
return browser.getCurrentUrl();
}
public static async getDesignId(): Promise<string> {
let url = await BrowserHelper.getCurrentUrl();
let designId = '';
let resources = _.split(url, '/');
if (_.includes(resources, 'design')) {
designId = resources[_.indexOf(resources, 'design') + 1];
}
return designId;
}
public static async scrollTo(x: number | string, y: number | string): Promise<void> {
await browser.executeScript(`window.scrollTo(${x},${y});`);
}
public static async scrollToTop(): Promise<void> {
await BrowserHelper.scrollTo(0, 0);
}
public static async scrollToBottom(): Promise<void> {
await BrowserHelper.scrollTo(0, 'document.body.scrollHeight');
}
public static async scrollToLocator(locator: By | Function): Promise<void> {
await browser.executeScript('arguments[0].scrollIntoView(false);', element(locator).getWebElement());
}
public static async scrollToElement(element: ElementFinder): Promise<void> {
await browser.executeScript('arguments[0].scrollIntoView(false);', element);
}
public static async isElementPresent(element: ElementFinder, waitMs?: number): Promise<boolean> {
return browser.wait(element.isPresent(), waitMs || 1000).then(() => true, () => false);
}
public static async getElement(locator: By | Function, waitMs?: number): Promise<ElementFinder | any> {
let isPresent = await BrowserHelper.waitForPresenceOf(element(locator), waitMs || 1000);
return isPresent ? element(locator) : undefined;
}
public static getParent(childElement: ElementFinder, levels?: number) {
let xpath = levels ? '' : '..';
for (let i = 1; i<=levels; i++) {
xpath += (i<levels) ? '../' : '..';
}
return childElement.element(by.xpath(xpath));
}
public static async urlContains(str: string, waitMs?: number): Promise<boolean> {
return browser.wait(EC.urlContains(str), waitMs || 5000).then(() => true, () => false);
}
public static async waitForPresenceOf(element: ElementFinder, waitMs?: number): Promise<boolean> {
return browser.wait(EC.presenceOf(element), waitMs || 5000).then(() => true, () => false);
}
public static async waitForVisibilityOf(element: ElementFinder, waitMs?: number): Promise<boolean> {
return browser.wait(EC.visibilityOf(element), waitMs || 5000).then(() => true, (e) => false);
}
public static async waitForInvisiblityOf(element: ElementFinder, waitMs?: number): Promise<any> {
await browser.wait(EC.invisibilityOf(element), waitMs || 5000);
}
public static async isElementDisplayed(element: ElementFinder): Promise<boolean> {
return element.isDisplayed().then(() => true, () => false);
}
public static async hasElement(locator: By | Function, waitMs?: number): Promise<boolean> {
return !!BrowserHelper.getElement(locator, waitMs || 5000);
}
public static async hasClass(element: ElementFinder, className: string, waitMs?: number): Promise<boolean> {
await BrowserHelper.isElementPresent(element, waitMs || 5000);
return new Promise<boolean>((resolve) => {
element.getAttribute('class').then(function (classes) {
let hasClass = classes.split(' ').indexOf(className) !== -1;
resolve(hasClass);
});
})
}
public static async sendKeys(locator: By | Function, keys: string, clear?: boolean, waitMs?: number): Promise<void> {
let element: ElementFinder = await BrowserHelper.getElement(locator, waitMs);
if (clear) {
await element.clear();
}
await element.sendKeys(keys);
}
public static async click(locator: By | Function, waitMs?: number): Promise<void> {
let element = await BrowserHelper.getElement(locator, waitMs);
await element.click();
}
public static async all(locator: By | Function, waitMs?: number): Promise<ElementFinder[]> {
// verify presence while allowing it to take a short while for element to appear
let isPresent: boolean = await BrowserHelper.waitForPresenceOf(element.all(locator).first(), waitMs || 5000);
if (isPresent) {
return element.all(locator);
}
throw new Error('Could not find Elements matching locator: ' + locator)
}
public static async allSub(parentElement: ElementFinder, locator: By | Function): Promise<ElementFinder[]> {
// assuming element is present (e.g. found using getElement)
return parentElement.all(locator);
}
public static async getElementText(element: ElementFinder): Promise<string> {
return element.getText().then((text) => text, (e) => "");
}
public static async getText(locator: By | Function, waitMs?: number): Promise<string> {
let element: ElementFinder = await BrowserHelper.getElement(locator, waitMs || 5000);
return element.getText();
}
public static async allText(locator: By | Function, waitMs?: number): Promise<string[]> {
let textElements: ElementFinder[] = await BrowserHelper.all(locator, waitMs || 5000);
return BrowserHelper.elementText(...textElements);
}
public static async elementText(...elements: ElementFinder[]): Promise<string[]> {
return Promise.all(_.map(elements, (element: ElementFinder) => {
return BrowserHelper.getElementText(element);
}));
}
public static async clickElementByLabel(clickablesLocator: By | Function, labelsLocator: By | Function, labelToClick: string, waitMs?: number): Promise<void> {
let labels: string[] = await BrowserHelper.allText(labelsLocator);
// remove leading and trailing whitespaces
labels = _.map<string, string>(labels, (label) => _.trim(label));
let elements: ElementFinder[] = await BrowserHelper.all(clickablesLocator);
expect(labels.length).to.be.eq(elements.length, `clickElementByLabel must have equal amount of clickables and labels`);
let clickIndex = _.indexOf(labels, labelToClick);
if (clickIndex >= 0) {
let elem = elements[clickIndex];
await BrowserHelper.waitForVisibilityOf(elem, waitMs || 5000);
await elem.click();
}
else {
throw new Error('Did not find Element with label:' + labelToClick)
}
}
public static async clickElementIfPresent(locator: By | Function): Promise<void> {
let isClickable = await BrowserHelper.waitForVisibilityOf(element(locator), 10);
if (isClickable) {
await element(locator).click();
}
}
public static async getSessionKey(key: string): Promise<any> {
return browser.driver.executeScript(`return window.sessionStorage.getItem("${key}");`);
}
// browser.driver.actions() does currently not properly add typings, so wrapping here for convenience
public static actions(): webdriver.ActionSequence {
return browser.driver.actions();
}
public static dragTo(to: webdriver.ILocation): webdriver.ActionSequence {
let actions = BrowserHelper.actions();
// reduce number of actions sent when testing via external selenium driver
// if (process.env.E2E_EXTERNAL_SERVER) {
actions.mouseMove({x: to.x-50, y: to.y});
// ease in to trigger snap suggestion
for (let i = 0; i < 10; i++) {
actions.mouseMove({x: 5, y: 0});
}
// }
// else {
//
// let pxPerStep = 5;
//
// let movedX = 0;
// let movedY = 0;
//
// while (Math.abs(movedX) < Math.abs(to.x) || Math.abs(movedY) < Math.abs(to.y)) {
// let dx = 0;
// let dy = 0;
//
// if (Math.abs(movedX) < Math.abs(to.x)) {
// dx = (to.x > 0) ? pxPerStep : -pxPerStep;
// }
//
// if (Math.abs(movedY) < Math.abs(to.y)) {
// dy = (to.y > 0) ? pxPerStep : -pxPerStep;
// }
//
// actions.mouseMove({x: dx, y: dy});
//
// movedX += dx;
// movedY += dy;
// }
// }
return actions;
}
}
You need your test cases to wait before proceeding, for this you need to do like this:
it('should log the user in to the application', function(done) {
BrowserHelper.sendKeys(usernameInputField, username)
.then(function() {
return BrowserHelper.sendKeys(passwordInputField, password)
})
.then(function() {
return BrowserHelper.click(loginBtn)
}).then(done)
});
Notice the done
parameter. This tells the test runner to wait for the done
callback to be invoked.
You should also be able to accomplish the same by simply returning a promise (works in my setup at least)
it('should log the user in to the application', function() {
return BrowserHelper.sendKeys(usernameInputField, username)
.then(function() {
return BrowserHelper.sendKeys(passwordInputField, password)
})
.then(function() {
return BrowserHelper.click(loginBtn)
})
});
Upvotes: 5