Find child elements with locator chaining

KIT518860_broad_chain_closeup_c_wikimedia.org_[1]

Have you ever had to find an element in another element?

Of course you have.

How did you do it?

 

Maybe you used a locator that searches for the parent element first and then for the child element in the parent.

 

Like in the following test:

public class TestClass {

  private String HOME_URL = "http://www.vpl.ca/";
  private String RESULTS_URL = "vpl.bibliocommons.com/search";

  private By SEARCH_BOX_ID = By.id("edit-search");
  private By SEARCH_BUTTON_ID = By.id("edit-submit");

  private By TITLE_LOCATOR = By.xpath(
    "(//div[contains(@data-js, 'bib-list-item')])[1]" +
    "//span[@class='title']");

  //driver variable, setUp() and tearDown() methods are skipped

  @Test
  public void test1() {
    driver.get(HOME_URL);
    assertEquals(driver.getCurrentUrl(), HOME_URL);

    WebElement searchBox = driver.findElement(SEARCH_BOX_ID);
    searchBox.sendKeys("java");

    WebElement searchButton = driver.findElement(SEARCH_BUTTON_ID);
    searchButton.click();

    assertTrue(driver.getCurrentUrl().contains(RESULTS_URL));

    WebElement title = driver.findElement(TITLE_LOCATOR);
    String titleText = title.getText().toLowerCase();

    assertTrue(titleText.contains("java"));
  }

}

 

In this case, the test opens the site, searches for a keyword and then finds the title of the first result.

The locator of the title includes the locator of the first result:

private final By TITLE_LOCATOR = By.xpath(
   "(//div[contains(@data-js, 'bib-list-item')])[1]" + 
   "//span[@class='title']");

(//div[contains(@data-js, ‘bib-list-item’)])[1]  is the locator of the first result.

//span[@class=’title’] is the locator of the first result’s title.

 

This seems like a decent approach but is it not.

If the code generates an exception when trying to find the title element, is the error about the title not being available or about the first result?

Since the locator combines both, it would be difficult to know.

 

We can change the code so that the parent element is found first and stored in a WebElement variable.

 

The child element is then searched inside of the parent element:

public class TestClass {

  private String HOME_URL = "http://www.vpl.ca/";
  private String RESULTS_URL = "vpl.bibliocommons.com/search";

  private By SEARCH_BOX_ID = By.id("edit-search");
  private By SEARCH_BUTTON_ID = By.id("edit-submit");

  private By RESULT_BOX_XPATH  = By.xpath(
    "(//div[contains(@data-js, 'bib-list-item')])[1]");

  private By TITLE_XPATH = By.xpath(
    ".//span[@class='title']"); 

  //driver variable, setUp() and tearDown() methods are skipped

  @Test
  public void test2() {
    driver.get(HOME_URL);   
    assertEquals(driver.getCurrentUrl(), HOME_URL);

    WebElement searchBox = driver.findElement(SEARCH_BOX_ID);
    searchBox.sendKeys("java"); 

    WebElement searchButton = driver.findElement(SEARCH_BUTTON_ID);
    searchButton.click();

    assertTrue(driver.getCurrentUrl().contains(RESULTS_URL));

    WebElement result = driver.findElement(RESULT_BOX_XPATH);
    WebElement title = result.findElement(TITLE_XPATH);
    String titleText = title.getText().toLowerCase();

    assertTrue(titleText.contains("java"));
  }
}

 

This time, the code finds the result element and stores it in a WebElement variable:

WebElement result = driver.findElement(RESULT_BOX_XPATH);

Then, it searches for the title element inside of the result element:

WebElement title = result.findElement(TITLE_INNER_XPATH);

Notice that the title locator starts with a dot.

TITLE_XPATH = By.xpath(".//span[@class='title']");

This is important when searching an element in another element.

When the code works this way, in case of an error, it is very clear which element caused it.

 

This is better than before but it extends poorly.

What if you need to find an element included in another element included in another element included in another element?

For each parent element, you need to search for the element and store it in a variable.

Which leads to lots of duplication.

This duplication can be removed using the ByChained Selenium class.

See how the code looks now:

public class TestClass {

  private String HOME_URL = "http://www.vpl.ca/";
  private String RESULTS_URL = "vpl.bibliocommons.com/search";

  private By SEARCH_BOX_ID = By.id("edit-search");
  private By SEARCH_BUTTON_ID = By.id("edit-submit");

  private By RESULT_BOX_XPATH  = By.xpath(
    "(//div[contains(@data-js, 'bib-list-item')])[1]");

  //driver variable, setUp() and tearDown() methods are skipped

  private By TITLE_XPATH = By.xpath(".//span[@class='title']");

  @Test
  public void test3() {
    driver.get(HOME_URL);    
    assertEquals(driver.getCurrentUrl(), HOME_URL);

    WebElement searchBox = driver.findElement(SEARCH_BOX_ID);
    searchBox.sendKeys("java");  

    WebElement searchButton = driver.findElement(SEARCH_BUTTON_ID);
    searchButton.click();

    assertTrue(driver.getCurrentUrl().contains(RESULTS_URL));

    WebElement title = driver.findElement(
         new ByChained(RESULT_BOX_XPATH,
                TITLE_XPATH));

    String titleText = title.getText().toLowerCase();

    assertTrue(titleText.contains("java"));
  }  
}

The code to find the title looks rather weird:

WebElement title = driver.findElement(
         new ByChained(RESULT_BOX_XPATH,TITLE_XPATH));

It creates a new object for the ByChained class.

The object gets 2 parameters in the constructor for the locators of the parent and child elements.

Finding the element happens as follows:

  • the element for the 1st locator is searched
  • the element for the 2nd locator is searched in the first element

Using this approach scales well even if we have elements included in many other elements.

There are no additional WebElements that should be created and the code stays approximately the same size.

 


Tips and tricks are great.

Keep coming to this blog for more.

However, if you are interested in better and more detailed learning, this Kindle ebook will help you improve your Selenium code:

Improve Selenium Code with Automation Patterns: Page Object Model, Page Factory , Page Elements, Base Page, Loadable Component

I am using all these patterns daily and so can you.

Advertisements

What are the differences between Selenium testers and Selenium developers?

wir[1]

What follows is inspired from reality.

It is not a product of my imagination.

 

Tester: Selenium is a tool like QTP.

Developer: Selenium is a Java library for web test automation.

 

Tester: I will automate test cases by recording them with Selenium IDE.
Developer: Selenium IDE is not an automation tool.

 

Tester: I dont need to write code like developers. I used to write Visual Basic code and that should be enough for automation.
Developer: The automation code should be at the level of production application code.

 

Tester: Every time my test needs a verification, I do it in IF/ELSE statements.
Developer: Verifications are done with JUNIT or TESTNG assertions.

 

Tester: Who cares if all code is in the test method? It works and it looks good to me.
Developer: I will first write an automation framework.
Then I will create the page object classes that are used in the test methods.

 

Tester: Explicit waits and expected conditions are not reliable and dont work well for me. I have created my own.
Developer: Explicit waits and expected conditions work well. I extend them in custom classes when needed.

 

Tester: Look, my test works! That is all I care about! I can start a new test!
Developer: My test should work but also be easy to understand, short and maintainable.

 

Tester: All my libraries are imported to the project. If I need new versions, I remove the old libraries and import the new ones. I used Maven long time ago and dont remember much about it.
Developer: I use Maven for managing dependencies.

 

Tester: All tests run from my computer every night. I run them anytime someone needs them.
Developer: Anyone can run my tests from Jenkins. They are scheduled to run every night.

 

Tester: My tests run in parallel on my computer.
Developer: My tests run in parallel on all available Jenkins slaves.

 

Tester: I love using static methods and static variables.
Developer: Static sucks. I avoided it as much as possible.

 

Tester: My project is very cool. I have a utility class for all common purpose methods. All page classes inherit from the utility class. This makes my page classes shorter.
Developer: I use composition to break the page classes in page element classes.

 

Tester: All my page element locators are in property text files.
Developer: If the page element locators are outside of the page classes, the class encapsulation is broken.

 

Do you have anything else that should be added to the list?

Please tell in the comments so I can add more.

 


 

Want to be more than a Selenium tester?

Start with this Kindle book on automation patterns that can make your Selenium code better:

Improve Selenium Code with Automation Patterns:

Page Object Model, Page Factory, Page Elements, Base Page, Loadable Component

 

amazon book

 

 

Use a custom driver class instead of utility classes

How can I create my own driver class in Selenium?
When learning test automation, you will want your test methods to be executed in multiple browsers such as Firefox and Chrome.

A Firefox driver is created with the FirefoxDriver class:

WebDriver driver = new FirefoxDriver();

A Chrome driver is created with the ChromeDriver class:

WebDriver driver = new ChromeDriver();

Both driver objects use the WebDriver type but are instantiated by different classes.

Now, you dont want to change the class from FirefoxDriver to ChromeDriver and back every time you want to run the tests in another browser.

Instead, you would like to create the driver based on the browser name:

WebDriver driver = new BrowserDriver(browserName);

How do we do this?

We create a custom driver class that implements the WebDriver interface and overrides its methods.

public final class BrowserDriver implements WebDriver {  

   private WebDriver driver;
   private final String browserName;
   private final int timeout = 30; 
   private final String chromeDriverPath = "./src/chromedriver.exe";  

   public BrowserDriver(String browserName) { 
      this.browserName = browserName; 
      this.driver = createDriver(browserName); 
   }  

   private WebDriver createDriver(String browserName) { 
      if (browserName.toUpperCase().equals("FIREFOX") 
         return firefoxDriver(); 

      if (browserName.toUpperCase().equals("CHROME") 
         return chromeDriver();  

      throw new RuntimeException ("invalid browser name"); 
   } 

   private WebDriver chromeDriver() { 
      if (!new File(chromeDriverPath).exists()) 
        throw new RuntimeException
                    ("chromedriver.exe does not exist!"); 

      try { 
        System.setProperty("webdriver.chrome.driver", 
                           chromeDriverPath); 
        return new ChromeDriver(); 
      } 

      catch (Exception ex) { 
        throw new RuntimeException
              ("couldnt create chrome driver"); 
      } 
   } 

   private WebDriver firefoxDriver() { 
      try { 
         return new FirefoxDriver(); 
      } 
      catch (Exception ex) {         
         throw new RuntimeException
              ("could not create the firefox driver"); 
     } 
   } 

   @Override 
   public String toString() { 
      return this.browserName; 
   } 

   public WebDriver driver() { 
      return this.driver; 
   }  

   @Override 
   public void close() { 
      driver().close();   
   }  

   @Override 
   public WebElement findElement(By locator) { 
      return driver().findElement(locator); 
   }  

   @Override 
   public List findElements(By arg0) { 
      return driver().findElements(arg0); 
   }  

   @Override 
   public void get(String arg0) { 
      driver().get(arg0); 
   }  

   @Override 
   public String getCurrentUrl() { 
      return driver().getCurrentUrl(); 
   }  

   @Override 

   public String getPageSource() { 
      return driver().getPageSource(); 
   }  

   @Override 
   public String getTitle() { 
      return driver().getTitle(); 
   }  

   @Override 
   public String getWindowHandle() { 
      return driver().getWindowHandle(); 
   }  

   @Override 
   public Set getWindowHandles() { 
     return driver().getWindowHandles(); 
   }  

   @Override 
   public Options manage() { 
      return driver().manage(); 
   }  

   @Override 
   public Navigation navigate() { 
      return driver().navigate(); 
   }  

   @Override 
   public void quit() { 
     driver().quit(); 
   }  

   @Override 
   public TargetLocator switchTo() { 
      return driver().switchTo(); 
   }  

   @Override 
   public  X getScreenshotAs(OutputType target
   throws WebDriverException { 
      return ((TakesScreenshot) driver())
                    .getScreenshotAs(target);
   } 

}

 

The only method that is not defined in the WebDriver interface is createDriver().
createDriver() creates a driver object based on the browser name and stores it in the driver variable.

All other methods just override the WebDriver interface methods.

In the test method, you can now use the new BrowserDriver class instead of FirefoxDriver and ChromeDriver:

WebDriver driver = new BrowserDriver(“CHROME”);

driver.get(“http://www.bestbuy.com”);

WebElement searchBox = driver.findElement(searchBoxLocator);

 

The custom driver class can add as well new methods.

 

Lets say that you want to have find element methods that use the WebDriverWait and ExpectedConditions classes:

public WebElement findVisibleElement(By locator) {
   WebElement element = new WebDriverWait(driver(), timeout)
                .until( visibilityOfElementLocated (locator));
   return element;
}

public WebElement findClickableElement(By locator) {
   WebElement element = new WebDriverWait(driver(), timeout)
               .until( elementToBeClickable (locator));
   return element;
}

public WebElement findHiddenElement(By locator) {
   WebElement element = new WebDriverWait(driver(), timeout)
              .until( presenceOfElementLocated (locator));
   return element;
}

You may be tempted to create a utility class, move these methods there and then use the utility class as a parent for the page classes.

Resist this temptation as it leads to bad things 🙂

Avoid utility classes in your automation projects.

More about utility classes very soon on this blog.

Instead of the utility class, you can move these methods to your custom driver class.

public final class BrowserDriver implements WebDriver {  

   private WebDriver driver; 
   private final String browserName; 
   private final int timeout = 30; 
   private final String chromeDriverPath = "./src/chromedriver.exe"; 

   public BrowserDriver(String browserName) { 
      this.browserName = browserName; 
      this.driver = createDriver(browserName); 
   }  

......................................................................

   @Override 
   public WebElement findElement(By locator) { 
     WebElement element = new WebDriverWait(driver(), timeout) 
                        .until( visibilityOfElementLocated(locator)); 
     return element; 
   }  

   @Override 
   public List findElements(By arg0) { 
     return driver().findElements(arg0); 
   }

   public WebElement findClickableElement(By locator) { 
      WebElement element = new WebDriverWait(driver(), timeout) 
                       .until( elementToBeClickable(locator)); 
      return element; 
   } 

   public WebElement findHiddenElement(By locator) { 
      WebElement element = new WebDriverWait(driver(), timeout) 
                      .until( presenceOfElementLocated(locator)); 
      return element; 
 }

}

How to deal with windows authentication popups

auth-required-basic

 

If you dont like Bugs Bunny, maybe you should not continue reading.

 

What is the first thing that I do every morning at work?

Check the results of the automation scripts executed in Jenkins over night.

I want to know how many scripts passed or failed because of the work done in the previous day.

Most days, some scripts pass and some fail.

But recently, most scripts failed.

This was very unusual so I looked into it right away.

All failures followed the same pattern: the scripts failed on opening the site in the browser.

 

Bugs-Bunny-Looking-Shocked

 

 

A sample script looks like this:

@Test
public void canBrowseThroughPages() {

  HomePage homePage = new HomePage(driver);
  homePage.open();

  ResultsPage resultsPage = homePage.search(keyword);

  assertTrue(resultsPage.resultCount() > 0);
 
  resultsPage = resultsPage.selectNextPage();

  assertTrue(resultsPage.isCurrentPage(2));
  assertTrue(resultsPage.resultCount() > 0);

}

 

The error happened in all scripts on the second line:
homePage.open();

The error info was

java.lang.object.RuntimeException:
home page didnt load correctly:
expected url = http://www.testsite.com
actual url =

 

I looked at the code of the open() method:

public class HomePage {

  private WebDriver driver;
  private WebDriverWait wait;

  private String url = "http://www.testsite.com";

  public HomePage(WebDriver driver) {
    this.driver = driver;
    wait = new WebDriverWait(this.driver, 30);
  }

  public void open()

    try {
      this.driver.get(url);
    }
    catch (Exception ex) {
     throw new RuntimeException("home page cannot be opened!");
    }

    try {
     wait.until(urlContains(url));
    }
    catch (Exception ex) {
     throw new RuntimeException("home page didnt load correctly - " +
             "expected url = " + url +
             " - " +
             "actual url = " + this.driver.getCurrentUrl());
    }
  }

 

I could not find anything wrong in the method.

It tries to open the url.
If it cannot, it throws an exception.

If the url is opened, it tries to verify the page url.
If it cannot, it throws an exception.

 
So, I looked again at the exception info:

java.lang.object.RuntimeException:
home page didnt load correctly;
expected url = http://www.testsite.com
actual url =

 

The actual url is empty which means that the site was not loaded.

But, why is the second exception thrown instead of the first one?

If driver.get(url) does not work, why doesn’t it generate an exception?

 

bugs bunny puzzled

Strange, isn’t it?

 

Next, I execute the script locally in Eclipse and it works.

So, the script works locally but has an error when executed in Jenkins.

 

What is the issue?

I look at the Jenkins execution details and get the name of the Jenkins slave where the script ran.

I start a remote connection to that server, open Chrome and load the site url in it.

And surprise!

A windows authentication popup is displayed!!!

That’s why the second exception was thrown instead of the first.

When executing driver.get(url), before the url is launched in the browser, the windows authentication popup is displayed.

driver.get() passes and the code continues with the url validation.

Since the authentication popup is still displayed, the page is not loaded and the url validation fails.

So the authentication popup is the culprit.

 

How can I disable the windows authentication popup?

Resetting the chrome browser settings does not help.

I can try creating the chrome driver with options and adding the site url to the chrome authentication whitelist.

This works manually.

When added to the code, the authentication popup is still there:

ChromeOptions options = new ChromeOptions();
options.addArguments("auth-server-whitelist='www.testsite.com'");
ChromeDriver driver = new ChromeDriver(options);

 
What else can I do?

Nothing goes right ……………….

So …………..

 

nothing goes right

 

Lets go left 🙂

 

I can include the account’s username and password in the url:

public class HomePage {

  private WebDriver driver;
  private WebDriverWait wait;

  private String url = "http://www.testsite.com";

  public HomePage(WebDriver driver) {
    this.driver = driver;
    wait = new WebDriverWait(this.driver, 30);
  }

  public void open() {

    try {
      this.driver.get(urlWithAccount(url));
    }
    catch (Exception ex) {
      throw new RuntimeException("home page cannot be opened");
    }

    try {
      wait.until(ExpectedConditions.urlContains(url);
    }
    catch (Exception ex) {
      throw new RuntimeException("home page didnt load correctly - " +
                                 "expected url = " + url +
                                 " - " +
                                 "actual url = " +                                                             this.driver.getCurrentUrl());
    }

  }

  private urlWithAccount(String url) {
     String username = System.getProperty("username");
     String password = System.getProperty("password");

     String newUrl = url.replace("http://",
                                 "http://" +
                                 username +
                                 ":" +
                                 password +
                                 "@");

     return newUrl;
  }

 

With the new method, the url would changes from

http://www.testsite.com

to

http://username:password@www.testsite.com

 

Rerunning the Jenkins scripts again showed that the problem is resolved.

 

thats all folks

How to use locators with parameters

Sometimes, you need to interact in automation scripts with web elements that have similar HTML code.

For example, you may want to automate the following test case:

  1. open site
  2. search for keyword
  3. check that the results page is displayed
  4. select the 2nd page of results
  5. check that the 2nd page is displayed
  6. select the 3rd page of results
  7. check that the 3rd page is displayed

 

The elements for the 2nd and 3rd pages have similar HTML code (A tag, HREF and DATE-PAGE attributes):

page 2
<a href=”/catalog/category.aspx?category=29059;page=2″ data-page=”2″>2</a>

page 3
<a href=”/catalog/category.aspx?category=29059;page=3″ data-page=”3″>3</a>

 

There are a few options for selecting similar page elements:

  1. use a locator for each page element
  2. use a method that creates the locator for each page locator
  3. use a locator pattern with a parameter

 

The code for option 1 is below:

import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.openqa.selenium.By;
import static org.openqa.selenium.By.*;
import org.openqa.selenium.WebElement;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;

public class BestBuyTest extends BaseTest {

String homePageUrl = "http://www.bestbuy.ca/";

By searchTextBoxLocator = By.id("ctl00_MasterHeader_ctl00_uchead_GlobalSearchUC_TxtSearchKeyword");

By searchButtonLocator = By.className("icon-search");

String resultsPageUrl = "http://www.bestbuy.ca/en-CA/category/";

By page2Locator = By.xpath(
"//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" + "//a[@data-page='2']");

By page3Locator = By.xpath(
"//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='3']");

@Test
public void TestPaging1() throws Exception {

  openSite();
  searchBy("ipad");

  WebElement page = wait.until(
                     elementToBeClickable
                      (page2Locator));

  page.click();

  assertTrue(isPageDisplayed(2));

  page = wait.until(
          elementToBeClickable
           (page3Locator));

  page.click();

  assertTrue(isPageDisplayed(3));

}

private void openSite() {
  driver.get(homePageUrl);

  if (wait.until(
       urlToBe(homePageUrl))== false )

    throw new RuntimeException
      ("The home page is not loaded correctly!");
}

private void searchBy(String keyword) {
  WebElement searchTextBox = wait.until(
                               elementToBeClickable
                                (searchTextBoxLocator));

  searchTextBox.sendKeys("ipad");

  WebElement searchButton = wait.until(
                             elementToBeClickable
                              (searchButtonLocator));

  searchButton.click();

  if (wait.until(
       urlContains
        (resultsPageUrl))== false )

     throw new RuntimeException
     ("The results page is not loaded correctly!");
}

private boolean isPageDisplayed(int pageNumber) {
  return wait.until(
          urlContains
           ("page=" + pageNumber));
}

}

This option is not good because of the duplicated page locators.

 

Option 2 uses a method that generates the page locator:

@Test
public void TestPaging2() throws Exception {

  openSite();
  searchBy("ipad");

  WebElement page = wait.until(
                     elementToBeClickable
                      (pageLocator(2)));

  page.click();

  assertTrue(isPageDisplayed(2));

  page = wait.until(
           elementToBeClickable
            (pageLocator(3)));

  page.click();

  assertTrue(isPageDisplayed(3));

}

private By pageLocator(int page) {
String locatorText = "//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='" +page"']";

return By.xpath(locatorText);
}

The pageLocator() method creates the locator using a page number parameter.

The locator is created by concatenating 3 parts:

  • the beginning of the xpath expression (before the page number)
  • the page number
  • the ending of the xpath expression

 

private By pageLocator(int page) {
String locatorText = "//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='"page"']";

return By.xpath(locatorText);
}

With the pageLocator() method, there is no need of having a locator for each page element.

But the locator text was moved inside of the method.

And the method’s code is pretty complicated.

 

Option 3 is using a locator pattern with a parameter:

String pageLocatorTemplate = "//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='%d']";

@Test
public void TestPaging3() throws Exception {

  openSite();
  searchBy("ipad");

  WebElement page = wait.until(
                     elementToBeClickable
                      (pageLocatorFromTemplate(2)));
  page.click();

  assertTrue(isPageDisplayed(2));

  page = wait.until(
          elementToBeClickable
           (pageLocatorFromTemplate(3)));

  page.click();

  assertTrue(isPageDisplayed(3));

}

private By pageLocatorFromTemplate(int page) {
  return By.xpath(
          String.format
           (pageLocatorTemplate, page));
}

We use now a locator pattern with a parameter (%d):

String pageLocatorTemplate = "//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='%d']";

 

A method is still needed for creating the locator.

The method has a pageNumber parameter which is substituted to the parameter (%d) of the locator pattern by the String.format() method:

private By pageLocatorFromTemplate(int page) {
  return By.xpath(
          String.format
           (pageLocatorTemplate, page));
}

The 3rd solution is the best of the 3 presented.

 

Do not store element locators in property files

You have a small test automation project with about 50 test scripts and 10 page object classes.

Each page object class uses approximately 10 web elements.

The project has around 100 web element locators.

Where should the elements’ locators be stored?

Should they be stored in the page object classes, in enumerations, classes with static members or property files?

The last option, property files, seems the best since every time more locators are needed, you just change a text file and not the project’s code.

So for each page object class, you create a property text file and save all element locators in it.

The page classes become smaller this way.

What can be wrong in this case?

The project grew from the initial size to

  • 1000 test scripts
  • 100 page object classes
  • 3000 element locators

Lets say that we are working on a class such as the next one:

public class HomePage {

  WebDriver driver;
  Properties properties;

  public HomePage(WebDriver driver) {
    this.driver = driver;
    this.properties = new Properties();
    InputStream input = new FileInputStream("home.properties");
    this.properties.load(input);
  }

  public void search(String keyword) {

   By searchBoxXpath = By.xpath(properties.get("searchTextBox"));
   WebElement searchTextBox =  driver.findElement(searchBoxXpath);

   searchTextBox.clear();
   searchTextBox.sendKeys(keyword);

   String searchButtonXpath = By.xpath(properties.get("searchButton"))

   WebElement searchButton = driver.findElement(searchButtonXpath);

   searchButton.click();

  }
}

The constructor loads the content of the home.properties file in the properties variable.

The search() method uses the name of the element which is then searched in the properties file.

The home.properties file looks as follows:

searchTextBox = //input[@elementid = 'abc']
searchButton = //input[@elementid = 'def']

Now, we need to change the locators for both elements used in the HomePage class.

How do we do this?

  1. Copy the element name (“searchTextBox”) from the HomePage class
  2. Locate the home.properties file in the project
  3. Open the home.properties file
  4. Search for the element name (“searchTextBox”)
  5. Modify the xpath expression
  6. Save and close the property file

6 steps and a few minutes for changing the value of a locator!
Remember that the project has 3000 locators at this moment.

1st problem of using property files for locators:
it is very time consuming to change the value of a locator.

 

Lets look at the locators situation from a different perspective.

Lots of code has been added, changed and removed from the project in the last year.

How do you know which locators are no longer needed?

With the locators in property files, this is impossible to see immediately.

2nd problem of using property files for locators:
it is not possible to see which locators are no longer used by the page classes.

 

Finally, we are writing test automation code using a language such as Java or C#.

The code that implements the user interactions with each page is organized in classes.

A class should group data and methods together.

By having the locators in the property files, this rule is broken.

3rd problem of using property files for locators:
classes are used incorrectly as they have methods that do not work on class members.

What is the correct approach for saving locators?

Save the locators in the page classes together with the methods.

Doing this allows

  • easy change of a locator in a minimum of steps
  • easy identification of locators not used
  • page classes include locators which are used by class methods

How to work with cookies

Browsers use cookies to store user preferences such as

  • user location
  • number of results per page
  • sort order
  • language

Next time the user visits the site, the user preferences saved in the cookies are set automatically for the site.

Each cookie is defined by the following fields:

  • name
  • value
  • domain
  • path
  • expires
  • size
  • http
  • isSecure
  • sameSite

To take an example, for the Vancouver Public Library – Results Page page, the selected language is saved in the language cookie with the following fields:

  • name = language
  • value = en-CA
  • domain = vpl.bibliocommons.com
  • path = /
  • expires = the expiration date set by the site
  • size = 13
  • http =
  • isSecure = true
  • sameSite =

If the language is changed from english to french, the value of the cookie changes to fr-CA.

It is important to be able to work with cookies in Selenium scripts for scenarios such as

  1. user changes the language and verifies that the change is done correctly
  2. user changes the sort order and verifies that the change is done correctly
  3. user changes the results per page and verifies that the change is done correctly

The cookies support is provided by the WebDriver.Options interface that has the following methods:

void addCookie(Cookie cookie)
Add a specific cookie.

void deleteAllCookies()
Delete all the cookies for the current domain.

void deleteCookie(Cookie cookie)
Delete a cookie from the browser’s “cookie jar”.

void deleteCookieNamed(java.lang.String name)
Delete the named cookie from the current domain.

Cookie getCookieNamed(java.lang.String name)
Get a cookie with a given name.

java.util.Set getCookies()
Get all cookies for the current domain.

An example will clarify how to use these methods.

The following code sample does the following

  1. open a web page: https://vpl.bibliocommons.com/search?q=java&t=keyword
  2. verify that the default language is english (en-CA) by checking the value of the language cookie
  3. verify that the page title is correct for english
  4. change the language to french
  5. verify that the language is french (fr-CA) by checking the value of the language cookie
  6. verify that the page title is correct for french

 

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.util.Date;

public class Class1 {

WebDriver driver;
WebDriverWait wait;

@Before
public void setUp() {
  System.setProperty("webdriver.chrome.driver", 
  "c:/selenium/selenium3/chromedriver.exe");
  
  driver = new ChromeDriver();
  
  wait = new WebDriverWait(driver, 30);
}

@After
public void tearDown() {
  driver.quit();
}

@Test
public void testChangeLanguage() throws InterruptedException {
  openSite();
  assertEquals(currentLanguage(), "en-CA");
  assertTrue(isTitleCorrect()));

  selectLanguage("fr-CA");

  goToPage(2);
  assertEquals(currentLanguage(), "fr-CA");
  assertTrue(isTitleCorrect());
}

private void openSite() {
  driver.get("https://vpl.bibliocommons.com/search?q=java&t=keyword");
}

private void goToPage(int pageNumber) {
  By pageLocator = By.xpath("//a[@testid='link_page" + 
                            pageNumber + "']");
  wait.until(ExpectedConditions
    .elementToBeClickable(pageLocator))
    .click();
}

private void selectLanguage(String language) {
  By changeLanguageMenuLocator = By.id("biblio_language_trigger");
  By languageLocator = By.xpath("//a[contains(@href,'" + 
                                language + 
                                "')]");
  wait.until(ExpectedConditions
     .elementToBeClickable(changeLanguageMenuLocator))
     .click();
  
  wait.until(ExpectedConditions
     .visibilityOfElementLocated(languageLocator));
  
  wait.until(ExpectedConditions
     .elementToBeClickable(languageLocator))
     .click();
}

private boolean isTitleCorrect() {
  if (currentLanguage().equalsIgnoreCase("en-CA") )
   return wait.until(ExpectedConditions
     .titleIs("Search | Vancouver Public Library | BiblioCommons"));
  else
   return wait.until(ExpectedConditions
     .titleIs("Rechercher | Vancouver Public Library | BiblioCommons"));
}

private String currentLanguage() {
  return getCookieValue("language");
}

private String getCookieValue(String cookieName) {
  return driver.manage()
               .getCookieNamed(cookieName)
               .getValue();
}

}

The code should be very easy to understand.

It interacts with cookies in 1 place only to get the value of a cookie (getCookieValue() method.

Is this all we can do with cookies?

In addition to getting the value of a cookie, we can also delete a cookie or even create a new one.

The previous script is very slow as it has to change the language manually.

We could change the language value by modifying the cookie value instead of clicking the language element.

The benefit of doing this is that the script will be faster than before as it has to go through less pages.

See below the changed code.

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.util.Date;

public class Class1 {

WebDriver driver;
WebDriverWait wait;

@Before
public void setUp() {
  System.setProperty("webdriver.chrome.driver", 
                     "c:/selenium/selenium3/chromedriver.exe");
  
  driver = new ChromeDriver();
  
  wait = new WebDriverWait(driver, 30);
}

@After
public void tearDown() {
  driver.quit();
}

@Test
public void testChangeLanguageByAddingCookie() throws InterruptedException {
  openSite();
  assertEquals(getCookieValue("language"), "en-CA");
  assertTrue(isTitleCorrect());

  addCookie("language", "fr-CA");

  goToPage(2);
  assertEquals(getCookieValue("language"), "fr-CA");
  assertTrue(isTitleCorrect());
}

private void openSite() {
  driver.get("https://vpl.bibliocommons.com/search?q=java&t=keyword");
}

private void goToPage(int pageNumber) {
  By pageLocator = By.xpath("//a[@testid='link_page" + 
                            pageNumber + "']");
  
  wait.until(ExpectedConditions
    .elementToBeClickable(pageLocator))
    .click();
}

private void selectLanguage(String language) {
  By changeLanguageMenuLocator = By.id("biblio_language_trigger");
  By languageLocator = By.xpath("//a[contains(@href,'" + 
                                language + 
                                "')]");
  
  wait.until(ExpectedConditions
      .elementToBeClickable(changeLanguageMenuLocator))
      .click();
  wait.until(ExpectedConditions
      .visibilityOfElementLocated(languageLocator));
  
  wait.until(ExpectedConditions
     .elementToBeClickable(languageLocator))
     .click();
}

private void deleteCookie(String cookieName) {
  driver.manage()
        .deleteCookieNamed(cookieName);
}

private String currentLanguage() {
  return getCookieValue("language");
}

private boolean isTitleCorrect() {
  if (currentLanguage().equalsIgnoreCase("en-CA") )
   return wait.until(ExpectedConditions
    .titleIs("Search | Vancouver Public Library | BiblioCommons"));
  else
   return wait.until(ExpectedConditions
    .titleIs("Rechercher | Vancouver Public Library | BiblioCommons"));
}

private String getCookieValue(String cookieName) {
  return driver.manage()
               .getCookieNamed(cookieName)
               .getValue();
}

private void addCookie(String name, String value) {
  deleteCookie(name);

  Cookie cookie = new Cookie.Builder(name, value)
   .domain(".bibliocommons.com")
   .expiresOn(new Date(2022, 03, 13))
   .isHttpOnly(false)
   .isSecure(true)
   .path("/")
   .build();

  driver.manage().addCookie(cookie);
}

}

The script is very similar with the previous one.

The only change is that instead of using the site to change the language, the language cookie is first deleted and then recreated for the new language.

Where can I find a full test automation project with Selenium WebDriver?

You have been learning test automation with Selenium.

You went through lots of concepts and would like to see how they all work together.

Where can you find a project that uses everything you learned and more?

Here 🙂

What follows is a small project that I built a while ago for a job interview.

It uses many test automation concepts such as:

  • page factory
  • base classes
  • html classes
  • test listeners
  • test ng assertions and fixtures
  • annotations
  • custom locators (javascript and jquery)
  • screenshots
  • saving errors in text files

The exercise consisted in automating the following test case with Java and Selenium WebDriver:

  • Launch bestbuy url (www.bestbuy.ca)
  • Search a product and add it to cart
  • Go all the way through checkout process and place the order with invalid credit card
  • Capture the error message due to invalid credit card

Before downloading the project and checking the source code, a few details about the project.

Project details

Maven project
– all dependencies are managed through the pom.xml file

 Test NG

– unit testing library

Java JDK 8

– used for lambda expressions and streams

Page Factory

– pattern for creating page object and page fragment classes

– the elements of page object/fragment classes have names and locators

– names and locators are implemented using annotations

– available locator types are id, xpath, css, name and javascript

  
   @Name("SEARCH_HEADER")
   @FindBy(className = "main-navigation-container") 
   public class SearchHeader extends HtmlElement{ 
 
   @Name("SEARCH_FIELD")
   @FindBy(id = "ctl00_MasterHeader_ctl00_uchead_GlobalSearchUC_TxtSearchKeyword") 
   private TextInput searchKeywordTxt;
 
   @Name("SEARCH_BUTTON")
   @FindBy(id = "ctl00_MasterHeader_ctl00_uchead_GlobalSearchUC_BtnSubmitSearch")
   private Button searchBtn; 
     
   public void search(String keyword) {
     searchKeywordTxt.click();  
     searchKeywordTxt.clear();
     searchKeywordTxt.sendKeys(keyword);
     searchBtn.click();  
   }
}

 

The project has the automation framework classes in the main folder and all test items in the test folder.

Main folder (framework classes)

annotations classes
– FindBy
– FindByJQUERY
– FindByJS
– Name
– Timeout
 
 package com.bestbuy.demo.annotations;

 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;

 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.TYPE, ElementType.FIELD})
 public @interface Name {
   String value();
 }

 

html element classes
 
    package com.bestbuy.demo.element;

    import org.openqa.selenium.By;
    import org.openqa.selenium.NoSuchElementException;
    import org.openqa.selenium.WebElement;

    public class CheckBox extends TypifiedElement {
   
      public CheckBox(WebElement wrappedElement) {
        super(wrappedElement);
      }

      public WebElement getLabel() {
        try {
            return getWrappedElement().findElement(By.xpath("following-sibling::label"));
        } catch (NoSuchElementException e) {
            return null;
        }
      }

      public String getLabelText() {
        WebElement label = getLabel();
        return label == null ? null : label.getText();
      }

      public String getText() {
        return getLabelText();
      }

      public void select() {
        if (!isSelected()) 
            getWrappedElement().click();        
      }

      public void deselect() {
        if (isSelected()) 
            getWrappedElement().click();
      }

      public void set(boolean value) {
        if (value) 
           select();
        else 
           deselect();
      }
   }

exceptions classes

 decorator and proxy classes used by the page factory

page class

– used as base class for the page object classes

 page factory classes

miscellaneous classes such as

– custom driver class, screenshot class
– enumerations (used to avoid hardcoding data in the page objects)
– simple logger class
– property class
– TextFile class

 

Test folder (test items)

    • base test class (used as base by the test classes)
    • page objects classes (all page object and page fragment classes)
    • test listeners (class for taking a screenshot and logging exceptions in case of failures)
 
 package com.bestbuy.demotests.testlisteners;

 import java.lang.reflect.Field;
 import org.testng.ITestContext;
 import org.testng.ITestListener;
 import org.testng.ITestResult;
 import com.bestbuy.demo.exceptions.HtmlElementsException;
 import com.bestbuy.demo.utils.Driver.BrowserDriver;
 import com.bestbuy.demo.utils.Driver.Screenshot;
 import org.openqa.selenium.WebDriver;
 import static com.bestbuy.demotests.BaseTest.BaseTestClass.*;

 public class TestListener implements ITestListener {   
   
    @Override
    public void onTestFailure(ITestResult result) {       
       try {
         Screenshot screenshot = 
         new Screenshot(getDriverFromBaseTest(result));
   
         screenshot.capture(result.getName());
       } 
       catch (Exception ex) {
         throw new HtmlElementsException(ex.getMessage());
       }                 
   }  

   @SuppressWarnings("unchecked")
   private WebDriver getDriverFromBaseTest(ITestResult result) 
       throws IllegalAccessException {
   
      WebDriver driver = null;
   
      try { 
         Class< ? extends ITestResult> testClass = 
         (Class< ? extends ITestResult>) result.getInstance().getClass();
  
         Class< ?extends ITestResult> baseTestClass = 
         (Class< ? extends ITestResult>) testClass.getSuperclass();
  
         Field driverField = baseTestClass.getDeclaredField("driver");
    
         driver = (BrowserDriver)driverField.get(result.getInstance()); 
    
         return driver;
     } 
     catch (SecurityException | NoSuchFieldException | IllegalArgumentException ex) {     
         throw new HtmlElementsException("error getting the driver from base test");    
     }         
   
   }
    
   @Override
   public void onTestSuccess(ITestResult result) 
   {}  
  
   @Override
   public void onTestSkipped(ITestResult result) 
   {} 
  
   @Override
   public void onTestFailedButWithinSuccessPercentage(ITestResult result) 
   {} 
  
   @Override
   public void onStart(ITestContext context)     
   {} 
  
   @Override 
   public void onFinish(ITestContext context)    
   {}
  
   @Override
   public void onTestStart(ITestResult arg0)
   {}
    
 }
  • test class

Most interactions with web elements are done through the page factory classes.

Occasionally, Javascript code is used when buttons cannot be clicked through the page factory.

Some of the pages have random popups that are closed if displayed.

The test script does the following:

  1. Open the home page
  2. Execute a keyword search
  3. Select the Online Only filter
  4. Select a product with online availability
  5. Add the product to the cart
  6. Continue checkout on the basket page
  7. Select New Member
  8. Fill in the Ship To info
  9. Select credit card as payment method
  10. Fill in the payment info
  11. Submits transaction
  12. Verify that there is at least an error message
  13. Saves the error messages and the search keyword to a text file

The test script uses a keyword parameter which can take multiple values.
Download source code

Interested in learning how to build a project like this one?

Get started here

How to create custom expected conditions in Selenium

You need custom expected conditions when the built-in Selenium WebDriver expected conditions are not sufficient for creating complex conditions.

Short review of expected conditions

Explicit waits and expected conditions are a great help for Selenium scripts.

Selenium WebDriver comes with a lot of expected conditions such as

  1. ExpectedCondition < WebElement > elementToBeClickable(By locator)
  2. ExpectedCondition < Boolean > elementToBeSelected(By locator)
  3. ExpectedCondition < WebElement > presenceOfElementLocated(By locator)
  4. ExpectedCondition < Boolean > titleContains(String title)
  5. ExpectedCondition < Boolean > titleIs(String title)
  6. ExpectedCondition < Boolean > urlContains(String fraction)
  7. ExpectedCondition < Boolean > urlToBe(String url)
  8. ExpectedCondition < WebElement > visibilityOfElementLocated(By locator)

There are 2 main expected condition types that can be used with explicit waits:

ExpectedCondition < WebElement >

This type of condition has a web element locator as parameter.

The wait applies the condition which tries finding the web element, depending on its status.

If the condition can find the element, it returns the element as result.

If it cannot find the element, the wait tries the condition again after a short delay.

ExpectedCondition < Boolean >

This type of condition has a string parameter.

The wait applies the condition to the parameter.

If the result of applying the condition is true, true is returned as result.

If the result is false, the wait tries the condition again after a short delay.

While the explicit wait applies the expected condition, the condition code may generate various exceptions.

All these exceptions are ignored by the wait.

Enough theory, lets see some code.

The following short example uses explicit waits and expected conditions to

  • verify if a page title is correct
  • verify if a page url is correct
  • find web elements
public class TestClass {

  WebDriver driver;
  WebDriverWait wait;

  By searchFieldXpath = By.id("globalQuery");
  By searchButtonXpath = By.className("search_button");
  By resultLinkLocator = By.xpath("(//a[@testid='bib_link'])[1]");
  
  String homeUrl = "http://www.vpl.ca";
  String homeTitle = "Vancouver Public Library - Home";

  String resultsTitle = "Search | Vancouver Public Library | BiblioCommons";
  String resultsUrl = "https://vpl.bibliocommons.com/search"; 

  @Before
  public void setUp() }
    driver = new FirefoxDriver();
    wait = new WebDriverWait(driver, 10);
  }

  @After
  public void tearDown() {
    driver.quit();
  }

  @Test
  public void test1() {
    driver.get(homeUrl);

    if (!wait.until(titleContains(homeTitle)) || 
        !wait.until(urlContains(homeUrl)))
          throw new RuntimeException("home page is not displayed");

    WebElement searchField = wait.until(
                         elementToBeClickable(searchFieldXpath));
    searchField.click();
    searchField.sendKeys(keyword);

    WebElement searchButton = wait.until(
                          elementToBeClickable(searchButtonXpath));
    searchButton.click();

    if (!wait.until(titleContains(resultsTitle)) || 
        !wait.until(urlContains(resultsUrl)))
          throw new RuntimeException("results page is not displayed");
  }
}

We use 2 expected conditions for verifying if a page is displayed.

Selenium does not include by default an expected condition that checks that both the page title and url are correct.

This is where we can create custom expected conditions.

How to create a custom expected condition

A custom expected condition is a class that

  • implements the ExpectedCondition interface
  • has a constructor with the parameters of the expected condition
  • overrides the apply method

Lets rewrite the previous exercise with a custom expected condition that verifies if a page is displayed:

public class TestClass {

  WebDriver driver;	
  WebDriverWait wait;

  By searchFieldXpath = By.id("globalQuery");
  By searchButtonXpath = By.className("search_button");

  By resultLinkLocator = By.xpath("(//a[@testid='bib_link'])[1]");	
	
  String homeUrl = "http://www.vpl.ca"; 
  String homeTitle = "Vancouver Public Library - Home";

  String resultsTitle = "Search | Vancouver Public Library | BiblioCommons";
  String resultsUrl = "https://vpl.bibliocommons.com/search";

  @Before
  public void setUp() }
    driver = new FirefoxDriver();
    wait = new WebDriverWait(driver, 10);
  }
	
  @After
  public void tearDown() {
    driver.quit();
  }
	
  @Test
  public void test1() {
    driver.get(siteUrl);
				
    if (!wait.until(new PageLoaded(homeTitle, homeUrl)))
	throw new RuntimeException("home page is not displayed");
				
    WebElement searchField = wait.until(
               elementToBeClickable(searchFieldXpath));
    searchField.click();           
    searchField.sendKeys(keyword);
			
    WebElement searchButton = wait.until(
               elementToBeClickable(searchButtonXpath));
    searchButton.click();	
			
    if (!wait.until(new PageLoaded(resultsTitle, resultsUrl)))
	throw new RuntimeException("results page is not displayed");
  }
}

public class PageLoaded implements ExpectedCondition {		
  String expectedTitle;
  String expectedUrl;
	
  public PageLoaded(String expectedTitle, String expectedUrl) {
    this.expectedTitle = expectedTitle;	
    this.expectedUrl = expectedUrl;
  }
	
  @Override
  public Boolean apply(WebDriver driver) {		
    Boolean isTitleCorrect = driver.getTitle()
                                   .contains(expectedTitle);
    Boolean isUrlCorrect = driver.getCurrentUrl()
                                 .contains(expectedUrl);				
    return isTitleCorrect && isUrlCorrect;
  } 
}

The PageLoaded custom expected condition is used for verifying if

  • HomePage is displayed
  • ResultsPage is displayed

For ResultsPage, we would like to verify not only that the page title and url are correct but also that

  • there are 25 results loaded in the page
  • total result count is > 0

The following custom expected condition does it all:

public class ResultsPageLoaded implements ExpectedCondition {		
  By resultLocator = By.xpath("//div[contains(@data-analytics, 'SubFeature')]");	
  By resultCountLocator = By.xpath("//span[@class='items_showing_count']");
	
  String expectedTitle;
  String expectedUrl;
	
  public ResultsPageLoaded(String expectedTitle, String expectedUrl) {		
    this.expectedTitle = expectedTitle;	
    this.expectedUrl = expectedUrl;		
  }
	
  @Override
  public Boolean apply(WebDriver driver) {					
    Boolean isTitleCorrect = driver.getTitle()
                                   .contains(expectedTitle);
    Boolean isUrlCorrect = driver.getCurrentUrl()
                                 .contains(expectedUrl);
		
   int resultPerPageCount = driver.findElements(resultLocator)
                                  .size();
		
   WebElement resultCountElement = driver.findElement(resultCountLocator);		
		
   return 
    isTitleCorrect && 
    isUrlCorrect && 
    resultPerPageCount == 25 &&
    count(resultCountElement) > 0;			   
 } 
	
  private int count(WebElement element) {		
    String resultCountText = element.getText();
    int index1 = resultCountText.indexOf("of") + 3;
    int index2 = resultCountText.indexOf("items") - 1;
    resultCountText = resultCountText.substring(index1, index2);
    return Integer.parseInt(resultCountText);		
  }

}

Custom expected conditions can return not only a boolean value but also a WebElement.

Lets define a custom condition that

  • finds an element from its parent element
  • returns the element if it is displayed
public class ResultField implements ExpectedCondition {		
  By resultLocator;	
  By parentLocator;
	
  public ResultField(By resultLocator, By parentLocator) {
    this.resultLocator = resultLocator;
    this.parentLocator = parentLocator;			
  }
	
  @Override
  public WebElement apply(WebDriver driver) {		
    WebElement parent = driver.findElement(parentLocator);				
    WebElement result = parent.findElement(resultLocator);
		
    return (result.isDisplayed() ? result : null);			
  } 
}	

How do you use it?

WebElement titleLink = wait.until(new ResultField(resultLocator, titleLocator)); 
titleLink.click();

How to handle random modal windows in Selenium

Many sites use modal windows for sales events, announcements and user surveys.

Especially in the last 2 months of the year when there are so many holidays (Christmas, New Year’s Eve, Boxing Day).

1

A modal is an HTML element that is displayed above the current page.

It includes

  • title
  • description
  • action button (take a survey, visit on sale section, etc)
  • close button

The user cannot interact with the main page until the modal is closed.

Modals can be a problem for test automation scripts.

If the modal is displayed consistently on a page, it is easy for the automation script to interact with it (take the action suggested by the modal or close it).

But if the modal is displayed randomly, the test automation script needs to be able to deal with the modal when displayed and not displayed.

How do we interact with random modal windows?

Let’s see how to do it for a simple web page.

The web page displays initially a button:

2

When clicking the button, the modal is not displayed some times.

In these cases, the page displays the “popup not displayed” and “done” messages:

3

Other times, when clicking the button, the modal is displayed:

4

After closing the modal, the page displays “done”.

5

The sample code consists in 3 classes:

  • test class – includes setUp() and tearDown() methods and the test script
  • main class – implements the interactions with the main page
  • popup class – implements the interaction with the popup

TEST CLASS

import static org.testng.AssertJUnit.assertTrue;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class TestClass {

WebDriver driver;

@BeforeMethod
public void setUp()
{
driver = new ChromeDriver();
System.setProperty("webdriver.chrome.driver",
"C:\\Selenium\\chromedriver.exe");
}

@AfterMethod
public void tearDown() {
driver.quit();
}

@Test(invocationCount = 10)
public void testRandomPopup() {
MainPage mainPage = new MainPage(driver);
mainPage.open();

Popup popup = mainPage.displayPopup();
popup.close();

assertTrue(mainPage.done().equalsIgnoreCase("done"));
}

}

The test script does the following

  • opens the main page
  • clicks the button that may display the popup
  • closes the popup; if the popup is displayed, then it is closed; if the popup is not displayed, nothing happens
  • asserts that the work is done

MAIN PAGE CLASS

 
import org.openqa.selenium.By;
import org.openqa.selenium.NotFoundException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
 
public class MainPage {
 
  private final WebDriver driver;
  private final By doneStatusId = By.id("done");
  private final By displayPopupId = By.id("displayPopup");
 
  public MainPage(WebDriver driver) {
    this.driver = driver;
  }
 
  public void open() throws InterruptedException {
    driver.get("file:///C:/Users/home/Desktop/modal.html");
 
    if (driver.getTitle().contains("Test Random Popup") == false)
        throw new NotFoundException("main page not displayed");
  }
 
  public String done() {
    WebElement doneLabel = driver.findElement(doneStatusId);
    return doneLabel.getText();
  }
 
  public Popup displayPopup() {
    WebElement displayButton = driver.findElement(displayPopupId);
    displayButton.click();
 
    return new Popup(driver);
  }
 
}

The MainPage class has the following methods:

  • open() – opens the page and checks that the opened page is the correct one
  • displayPopup() – clicks the button that may display the popup
  • done() – returns the message displayed after the popup is handled

POPUP CLASS

 
import org.openqa.selenium.By;
import org.openqa.selenium.InvalidElementStateException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;
import org.openqa.selenium.support.ui.WebDriverWait;
 
public class Popup {
 
  private final WebDriverWait wait;
  private final By closeElementLocator = By.id("closePopup");
  private final By popupIdLocator = By.id("popupId");
 
  public Popup(WebDriver driver) {
    this.wait = new WebDriverWait(driver, 5);
  }
 
  public void close() throws InterruptedException {
    if (isDisplayed()) {
        WebElement closeElement = wait.until(
               visibilityOfElementLocated(closeElementLocator));
        closeElement.click();
    }
  }
 
  private Boolean isDisplayed() throws InterruptedException {
     try {
        WebElement popup = wait.until(
                visibilityOfElementLocated(popupIdLocator));
        return popup.isDisplayed();
     }
     catch (Exception ex) {
       return false;
     }
 }
}

The popup class has 2 methods:

1. isDisplayed()

This is the most important method for managing the random modal.

It tries finding the modal element.

If the modal element is found, isDisplayed() returns true if modal is displayed and false otherwise.

If the popup is not displayed, an exception is thrown.

The exception is caught and isDisplayed() returns false;

2. close()

It closes the popup if it is displayed.

Thanks for reading.

Want to get started with Selenium WebDriver and Java in 10 short lessons?