Extend Selenium WebDriver WebElement?
Asked Answered
A

10

12

I'm following the Page Object pattern suggested by Selenium, but how would I create a more specialized WebElement for a page. Specifically, we have tables on our pages and I have written some helper functions to get specific rows of a table, return the contents of a table, etc.

Currently, here is a snippet of a page object I created that has a table:

public class PermissionsPage  {

    @FindBy(id = "studyPermissionsTable")
    private WebElement permissionTable;

    @FindBy(id = "studyPermissionAddPermission")
    private WebElement addPermissionButton;

    ... 

}

So, what I'd like to do is have that permissionsTable to be a more customized WebElement that has some of those methods I mentioned earlier.

For example:

public class TableWebElement extends WebElement {
    WebElement table;
    // a WebDriver needs to come into play here too I think

    public List<Map<String, String>> getTableData() {
        // code to do this
    }

    public int getTableSize() {
        // code to do this
    }

    public WebElement getElementFromTable(String id) {
        // code to do this
    }
}

I hope that this makes sense what I'm trying to explain. I guess what I'm looking for is a way to have this custom WebElement to do some additional stuff that's table-specific. Add this custom element to a Page and take advantage of the way Selenium wires the webelements to the page based on the annotations.

Is it possible? And if so, does anyone know how this can be done?

Athalia answered 25/7, 2012 at 3:39 Comment(0)
S
30

I created an interface that combines all of the WebDriver interfaces:

public interface Element extends WebElement, WrapsElement, Locatable {}

It's just there to wrap up all of the things WebElements can do when wrapping an element.

Then an implementation:

public class ElementImpl implements Element {

    private final WebElement element;

    public ElementImpl(final WebElement element) {
        this.element = element;
    }

    @Override
    public void click() {
        element.click();
    }

    @Override
    public void sendKeys(CharSequence... keysToSend) {
        element.sendKeys(keysToSend);
    }

    // And so on, delegates all the way down...

}

Then, for example a check box:

public class CheckBox extends ElementImpl {

    public CheckBox(WebElement element) {
        super(element);
    }

    public void toggle() {
        getWrappedElement().click();
    }

    public void check() {
        if (!isChecked()) {
            toggle();
        }
    }

    public void uncheck() {
        if (isChecked()) {
            toggle();
        }
    }

    public boolean isChecked() {
        return getWrappedElement().isSelected();
    }
}

When using it in my script:

CheckBox cb = new CheckBox(element);
cb.uncheck();

I've also come up with a way of wrapping the Element classes. You have to create a few factories to replace the built-in PageFactory, but it is doable, and it lends a lot of flexibility.

I've documented this process over on my site:

I've also got a project called selophane that was inspired by this and other questions: selophane

Spate answered 7/12, 2012 at 19:38 Comment(1)
this is how I did it too, but a bit different.. I created an abstract class which implements a custom interface extending WebElement interface. loaded all methods in the abstract and added methods on the final class. And in my InvocationHandler, I intercepted the method and redirected it to the webelement object's methods if they are declared in webelement's interface; else, I wrapped the element with the custom webelement wrapper; and invoked the method. PS. +1 for the blog :) thanks!Exciting
M
4

You can create custom WebElements by using the WebDriver Extensions framework that provides a WebComponent class which implements the WebElement interface

Create your custom WebElement

public class Table extends WebComponent {
    @FindBy(tagName = "tr")
    List<Row> rows;

    public Row getRow(int row) {
        return rows.get(row - 1);
    }

    public int getTableSize() {
        return rows.size();
    }

    public static class Row extends WebComponent {
        @FindBy(tagName = "td")
        List<WebElement> columns;

        public WebElement getCell(int column) {
            return columns.get(column - 1);
        }
    }
}

...and then add it to your PageObject with an @FindBy annotation and use the WebDriverExtensionFieldDecorator when calling the PageFactory.initElements method

public class PermissionPage {
    public PermissionPage(WebDriver driver) {
        PageFactory.initElements(new WebDriverExtensionFieldDecorator(driver), this);
    }

    @FindBy(id = "studyPermissionsTable")
    public Table permissionTable;

    @FindBy(id = "studyPermissionAddPermission")
    public WebElement addPermissionButton;
}

...and then use it in your test

public class PermissionPageTest {
    @Test
    public void exampleTest() {
        WebDriver driver = new FirefoxDriver();
        PermissionPage permissionPage = new PermissionPage(driver);

        driver.get("http://www.url-to-permission-page.com");
        assertEquals(25, permissionPage.permissionTable.getTableSize());
        assertEquals("READ", permissionPage.permissionTable.getRow(2).getCell(1).getText());
        assertEquals("WRITE", permissionPage.permissionTable.getRow(2).getCell(2).getText());
        assertEquals("EXECUTE", permissionPage.permissionTable.getRow(2).getCell(3).getText());
    }
}




Or even better use the WebDriver Extensions PageObject implementation

public class PermissionPage extends WebPage {
    @FindBy(id = "studyPermissionsTable")
    public Table permissionTable;

    @FindBy(id = "studyPermissionAddPermission")
    public WebElement addPermissionButton;

    @Override
    public void open(Object... arguments) {
        open("http://www.url-to-permission-page.com");
        assertIsOpen();
    }

    @Override
    public void assertIsOpen(Object... arguments) throws AssertionError {
        assertIsDisabled(permissionTable);
        assertIsDisabled(addPermissionButton);
    }
}

and the JUnitRunner with the static asserts methods for WebElements

import static com.github.webdriverextensions.Bot.*;

@RunWith(WebDriverRunner.class)
public class PermissionPageTest {

    PermissionPage permissionPage;

    @Test
    @Firefox
    public void exampleTest() {
        open(permissionPage);
        assertSizeEquals(25, permissionPage.permissionTable.rows);
        assertTextEquals("READ", permissionPage.permissionTable.getRow(2).getCell(1));
        assertTextEquals("WRITE", permissionPage.permissionTable.getRow(2).getCell(2));
        assertTextEquals("EXECUTE", permissionPage.permissionTable.getRow(2).getCell(3));
    }
}
Missive answered 4/3, 2015 at 18:35 Comment(1)
I posted this question over 2 years ago and I am finding myself re-visiting this same scenario. I forgot I even asked this question. I was looking at the various answers and your webdriverextensions look awesome! Filling in the holes that Selenium leaves opened. I am going to read your documentation and give it a go.Athalia
F
1

Side note: WebElement is not class, its interface which means your class would look more like this:

public class TableWebElement implements WebElement {

But in that case you must implement all the methods which are in WebDriver. And its kinda overkill.

Here is way how I do this - I got rid completely of proposed PageObjects as proposed by selenium and "reinvented the wheel" by having my own classes. I have one class for the whole application:

public class WebUI{
  private WebDriver driver;    
  private WebElement permissionTable;   

   public WebUI(){
      driver = new firefoxDriver();
   }

  public WebDriver getDriver(){
     return driver;
  }

  public WebElement getPermissionTable(){
     return permissionTable;
  }

  public TableWebElement getTable(){
     permissionTable = driver.findElement(By.id("studyPermissionsTable"));
     return new TableWebElement(this);
  }
}

And then I have my helper classes

public class TableWebElement{
  private WebUI webUI;

 public TableWebElement(WebUI wUI){
    this.webUI = wUI;
 }

 public int getTableSize() {
    // because I dont know exactly what are you trying to achieve just few hints
    // this is how you get to the WebDriver:
    WebElement element = webUI.getDriver().findElement(By.id("foo"));

    //this is how you get to already found table:
    WebElement myTable = webUI.getPermissionTable();

 }

}

Sample of test:

 @Test
 public void testTableSize(){
    WebUI web = new WebUI();
    TableWebElement myTable = web.getTable();
    Assert.assertEquals(myTable.getSize(), 25);
 }
Filter answered 25/7, 2012 at 9:26 Comment(3)
I like this idea, I"ll try it out and let you know how it goes.Athalia
btw, I don' know who noted that this answer is not useful. I'm curious why someone voted that way.Athalia
At least you like the idea :)Filter
A
1

As a follow up to this, this is what I ended up doing (in case anyone else has this same issue). Below is a snippet of the class I created as a wrapper to a WebElement:

public class CustomTable {

private WebDriver driver;
private WebElement tableWebElement;

public CustomTable(WebElement table, WebDriver driver) {
    this.driver = driver;
    tableWebElement = table;
}

public WebElement getTableWebElement() {
    return tableWebElement;
}

public List<WebElement> getTableRows() {
    String id = tableWebElement.getAttribute("id");
    return driver.findElements(By.xpath("//*[@id='" + id + "']/tbody/tr"));
}

public List<WebElement> getTableHeader() {
    String id = tableWebElement.getAttribute("id");
    return tableWebElement.findElements(By.xpath("//*[@id='" + id + "']/thead/tr/th"));
}   
.... more utility functions here
}

And then I use this in any Pages that create by referencing it like so:

public class TestPage {

@FindBy(id = "testTable")
private WebElement myTestTable;

/**
 * @return the myTestTable
 */
public CustomTable getBrowserTable() {
    return new CustomTable(myTestTable, getDriver());
}

The only thing that I don't like is that when the page wants to get the table, it creates a "new" table. I did this to avoid StaleReferenceExeptions which happens when the data in the table is updated (i.e. cell updated, row added, row removed). If anyone has any suggestions on how I can avoid creating a new instance every time it's requested, but rather returns the updated WebElement that would be great!

Athalia answered 13/8, 2012 at 14:3 Comment(0)
B
1

I also created custom WebElement implementation by extending DefaultFieldDecorator and it works great with PageFactory pattern. However I have concerns about using PageFactory itself. It seems to work great only with 'static' applications (where user interaction does not change component layout of the app).

But whenever you need to work with something little more complex, like if you have a page with table, that contains list of Users and delete button next to each user, using PageFactory becomes problematic.

Here's an example to illustrate what I am talking about:

public class UserList
 {
private UserListPage page;

UserList(WebDriver driver)
{
    page = new UserListPage(driver);
}

public void deleteFirstTwoUsers()
{
    if (page.userList.size() <2) throw new RuntimeException("Terrible bug!");  

    page.deleteUserButtons.get(0).click();
    page.deleteUserButtons.get(0).click();
}

class UserListPage {

@FindBy(xpath = "//span[@class='All_Users']")
List<BaseElement> userList;

@FindBy(xpath = "//span[@class='All_Users_Delete_Buttons']")
List<BaseElement> deleteUserButtons;

UserListPage(WebDriver driver)
 {
    PageFactory.initElements(new ExtendedFieldDecorator(driver), this);
 }
}

So, in the above scenario, when one calls deleteFirstTwoUsers() method, it will fail when trying to delete second User with "StaleElementReferenceException: stale element reference: element is not attached to the page document". This happens because 'page' was instantiated only once in the Constructor and PageFactory has 'no way to know' :) that amount of Users has decreased.

The workarounds that I found so far, are ether:

1) Do not use Page Factory altogether for methods like this, and simply use WebDriver directly with some sort of while loop: driver.findElementsBy()....

2) or do something like this:

public void deleteFirstTwoUsers()
{
    if (page.userList.size() <2) throw new RuntimeException("Terrible bug!");  

    new UserListPage(driver).deleteUserButtons.get(0).click();
    new UserListPage(driver).deleteUserButtons.get(1).click();
}

3) or this:

public class UserList {
  private WebDriver driver;

UserList(WebDriver driver)
{
    this.driver = driver;
}

UserListPage getPage { return new UserListPage(driver);})
public void deleteFirstTwoUsers()
{
    if (getPage.userList.size() <2) throw new RuntimeException("Terrible bug!");  

    getPage.deleteUserButtons.get(0).click();
    getPage.deleteUserButtons.get(0).click();
}

But in the first examples, you can't benefit from using your wrapped BaseElement. And in second and third one - you end up creating new instances of Page Factory for every single action you do on the page. So I guess it all comes down to two questions:

1) Do you think it really worth to use PageFactory for anything, really? It would be much 'cheaper' to create each element on the fly like so:

class UserListPage
{
private WebDriver driver;

UserListPage(WebDriver driver)
{
    this.driver = driver;
}

List<BaseElement> getUserList() {
    return driver.findElements(By.xpath("//span[@class='All_Users']"));
}

2) Is it possible to @Override driver.findElements method so it'll return an instance of my wrapped BaseElement object and not WebElement? If not, are there any alternatives?

Benita answered 12/10, 2018 at 21:28 Comment(2)
Is this a new answer to a 6 year old question, or is this a new question?Josephus
I guess it is both - I've added my current implementation, but curious to see if someone knows a better way to do it.Benita
K
0

Why bother extending WebElement? Are you trying to actually develop the Selenium package? Then if you are it wouldn't make sense to extend that method unless your additions can be applicable to every single web element that you use, not just the ones that are specific to your table

Why not just do something like:

public class PermissionsPage extends TableWebElement{

...permissions stuff...

}

import WebElement

public class TableWebElement{

...custom web elements pertaining to my page...

}

I don't know if that answers your question, but it seemed to me it was more a class architecture question more than anything else.

Knew answered 25/7, 2012 at 4:15 Comment(0)
O
0

I would create what I call a "SubPageObject" which represents a PageObject on a PageObject...

The advantage is that you can create custom made methods for it and you can initialize only WebElements you really need within this SubPageObject.

Oyer answered 25/7, 2012 at 7:33 Comment(0)
T
0

Why not make a web element wrapper? like this?

class WEBElment
{

public IWebElement Element;

public WEBElement(/*send whatever you decide, a webelement, a by element, a locator whatever*/)

{

Element = /*make the element from what you sent your self*/

}


public bool IsDisplayed()

{

return Element.Displayed;

}

} // end of class here

but you can make your class more complicated than this. just a concept

Thicket answered 3/8, 2012 at 19:38 Comment(0)
A
0

Take a look at htmlelements framework, it looks like exactly what you need. It has pre-implemented common elements, such as checkbox, radiobox, table, form etc, you can easily create your own and insert one elements into others creating clear tree structure.

Apices answered 9/9, 2015 at 6:14 Comment(0)
L
0

Step 1) Create a base class called Element that extends the IWrapsElement interface

 public class Element : IWrapsElement
{
    public IWebElement WrappedElement { get; set; }
    public Element(IWebElement element)
    {
        this.WrappedElement = element;
    }
}

Step 2) Create a class for each custom element and inherit from the Element class

 public class Checkbox : Element
{
    public Checkbox(IWebElement element) : base(element) { }

    public void Check(bool expected)
    {
        if (this.IsChecked()!= expected)
        {
            this.WrappedElement.Click();
        }
    }

    public bool IsChecked()
    {
        return this.WrappedElement.GetAttribute("checked") == "true";
    }
}

Usage:

a)

Checkbox x = new Checkbox(driver.FindElement(By.Id("xyz")));
x.Check(true)

b)

 private IWebElement _chkMale{ get; set; }
 [FindsBy(How = How.Name, Using = "chkMale")]

 private Checkbox ChkMale
    {
        get { return new Checkbox (_chkMale); }
    }
ChkMale.Check(true);
Larcher answered 20/7, 2017 at 9:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.