简体   繁体   English

扩展Selenium WebDriver WebElement?

[英]Extend Selenium WebDriver WebElement?

I'm following the Page Object pattern suggested by Selenium, but how would I create a more specialized WebElement for a page. 我正在遵循Selenium建议的页面对象模式,但是如何为页面创建更专业的WebElement。 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. 所以,我想要做的是将权限表作为一个更加自定义的WebElement,其中包含我之前提到的一些方法。

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. 我想我正在寻找的是让这个自定义WebElement做一些特定于表的其他东西的方法。 Add this custom element to a Page and take advantage of the way Selenium wires the webelements to the page based on the annotations. 将此自定义元素添加到页面,并利用Selenium基于注释将webelements连接到页面的方式。

Is it possible? 可能吗? And if so, does anyone know how this can be done? 如果是这样,有谁知道如何做到这一点?

I created an interface that combines all of the WebDriver interfaces: 我创建了一个结合了所有WebDriver接口的接口:

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. 它就是包装WebElements在包装元素时可以做的所有事情。

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. 我还提出了一种包装Element类的方法。 You have to create a few factories to replace the built-in PageFactory , but it is doable, and it lends a lot of flexibility. 您必须创建一些工厂来替换内置的PageFactory ,但它是可行的,并且它提供了很大的灵活性。

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 我还有一个名为selophane的项目受到了这个和其他问题的启发: selophane

You can create custom WebElements by using the WebDriver Extensions framework that provides a WebComponent class which implements the WebElement interface 您可以使用WebDriver Extensions框架创建自定义WebElements,该框架提供实现WebElement接口的WebComponent类

Create your custom WebElement 创建自定义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 ...然后使用@FindBy注释将其添加到PageObject中,并在调用PageFactory.initElements方法时使用WebDriverExtensionFieldDecorator

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 或者甚至更好地使用WebDriver Extensions PageObject实现

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 和JUnitRunner以及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));
    }
}

Side note: WebElement is not class, its interface which means your class would look more like this: 旁注: WebElement不是类,它的接口意味着你的类看起来更像是这样:

public class TableWebElement implements WebElement {

But in that case you must implement all the methods which are in WebDriver. 但在这种情况下,您必须实现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. 这是我如何做到这一点 - 我完全摆脱了selenium提出的建议的PageObjects和通过拥有我自己的类“重新发明轮子”。 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);
 }

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: 下面是我作为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 (ie cell updated, row added, row removed). 我这样做是为了避免更新表中的数据时发生的StaleReferenceExeptions(即更新单元格,添加行,删除行)。 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! 如果有人对我每次请求时如何避免创建新实例有任何建议,而是返回更新的WebElement,这将是很棒的!

I also created custom WebElement implementation by extending DefaultFieldDecorator and it works great with PageFactory pattern. 我还通过扩展DefaultFieldDecorator创建了自定义WebElement实现,并且它在PageFactory模式下运行良好。 However I have concerns about using PageFactory itself. 但是我担心使用PageFactory本身。 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. 但是每当你需要使用一些更复杂的东西时,比如你有一个包含表格的页面,其中包含每个用户旁边的用户列表和删除按钮,使用PageFactory就会出现问题。

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". 因此,在上面的场景中,当一个人调用deleteFirstTwoUsers()方法时,尝试使用“StaleElementReferenceException:stale元素引用:元素未附加到页面文档”时尝试删除第二个User时将失败。 This happens because 'page' was instantiated only once in the Constructor and PageFactory has 'no way to know' :) that amount of Users has decreased. 发生这种情况是因为'page'在构造函数中只实例化了一次,并且PageFactory“无法知道”:)用户数量减少了。

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()... . 1)不要像这样的方法一起使用Page Factory,只需使用某种while循环直接使用WebDriver: driver.findElementsBy()...

2) or do something like this: 2)或做这样的事情:

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: 3)或者这个:

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. 但在第一个示例中,您无法使用包装的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. 在第二个和第三个 - 您最终为页面上执行的每个操作创建了Page Factory的新实例。 So I guess it all comes down to two questions: 所以我想这一切都归结为两个问题:

1) Do you think it really worth to use PageFactory for anything, really? 1)你觉得使用PageFactory真的值得吗? 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? 2)是否有可能@Override driver.findElements方法,所以它将返回我包装的BaseElement对象的实例,而不是WebElement? If not, are there any alternatives? 如果没有,有没有其他选择?

Why bother extending WebElement? 为什么要麻烦扩展WebElement? Are you trying to actually develop the Selenium package? 您是否正在尝试开发Selenium包? 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 然后,如果你是扩展该方法是没有意义的,除非你的添加可以适用于你使用的每个web元素,而不仅仅是那些特定于你的表的元素

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. 我不知道这是否能回答你的问题,但在我看来,它更像是一个阶级架构问题。

I would create what I call a "SubPageObject" which represents a PageObject on a PageObject... 我会创建我称之为“SubPageObject”的东西,它代表PageObject上的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. 优点是您可以为它创建自定义方法,并且只能在此SubPageObject中初始化您真正需要的WebElements。

Why not make a web element wrapper? 为什么不制作一个Web元素包装器? 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 只是一个概念

Take a look at htmlelements framework, it looks like exactly what you need. 看一下htmlelements框架,看起来就像你需要的那样。 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. 它具有预先实现的常用元素,如复选框,radiobox,table,form等,您可以轻松创建自己的元素,并将一个元素插入到其他元素中,从而创建清晰的树结构。

Step 1) Create a base class called Element that extends the IWrapsElement interface 步骤1)创建一个名为Element的基类,扩展IWrapsElement接口

 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 步骤2)为每个自定义元素创建一个类,并从Element类继承

 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) b)

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

 private Checkbox ChkMale
    {
        get { return new Checkbox (_chkMale); }
    }
ChkMale.Check(true);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM