it-roy-ru.com

Расширить Selenium WebDriver WebElement?

Я следую шаблону Page Object, предложенному Selenium, но как бы я создал более специализированный WebElement для страницы. В частности, у нас есть таблицы на наших страницах, и я написал несколько вспомогательных функций для получения определенных строк таблицы, возврата содержимого таблицы и т.д.

В настоящее время приведен фрагмент созданного мной объекта страницы с таблицей:

public class PermissionsPage  {

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

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

    ... 

}

Итак, я хотел бы сделать так, чтобы permissionsTable был более настраиваемым WebElement, который имеет некоторые из тех методов, которые я упоминал ранее.

Например:

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
    }
}

Я надеюсь, что это имеет смысл, что я пытаюсь объяснить. Я думаю, что я ищу способ иметь этот пользовательский WebElement, чтобы делать некоторые дополнительные вещи, которые являются специфичными для таблицы. Добавьте этот пользовательский элемент на страницу и воспользуйтесь тем, как Selenium связывает элементы со страницей на основе аннотаций. 

Является ли это возможным? И если так, кто-нибудь знает, как это можно сделать?

13
pipplupp

Я создал интерфейс, который объединяет все интерфейсы WebDriver:

public interface Element extends WebElement, WrapsElement, Locatable {}

Это просто для того, чтобы обернуть все, что могут сделать WebElements при переносе элемента.

Тогда реализация:

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...

}

Тогда, например, флажок:

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();
    }
}

При использовании этого в моем сценарии:

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

Я также придумал способ обернуть классы Element. Вы должны создать несколько фабрик, чтобы заменить встроенную переменную PageFactory, но это выполнимо и дает большую гибкость.

Я задокументировал этот процесс на моем сайте: 

У меня также есть проект под названием selophane, который был вдохновлен этим и другими вопросами: selophane

26
Elijah Sarver

Вы можете создавать собственные WebElements с помощью WebDriver Extensions framework, который предоставляет класс WebComponent, который реализует интерфейс 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);
        }
    }
}

... а затем добавьте его в свой PageObject с аннотацией @FindBy и используйте WebDriverExtensionFieldDecorator при вызове метода PageFactory.initElements

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;
}

... а затем использовать его в своем тесте

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());
    }
}




Или даже лучше использовать Расширения WebDriver 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);
    }
}

и 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));
    }
}
3
AndiDev

Я также создал пользовательскую реализацию WebElement, расширив DefaultFieldDecorator, и она прекрасно работает с шаблоном PageFactory. Однако у меня есть опасения по поводу использования самой PageFactory. Кажется, он прекрасно работает только со «статическими» приложениями (где взаимодействие с пользователем не меняет компоновку компонентов приложения). 

Но всякий раз, когда вам нужно работать с чем-то немного более сложным, например, если у вас есть страница с таблицей, которая содержит список пользователей и кнопку удаления рядом с каждым пользователем, использование PageFactory становится проблематичным.

Вот пример, чтобы проиллюстрировать, о чем я говорю:

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);
 }
}

Таким образом, в приведенном выше сценарии, когда кто-то вызывает метод deleteFirstTwoUsers (), он потерпит неудачу при попытке удалить второго пользователя с помощью «StaleElementReferenceException: устаревшая ссылка на элемент: элемент не прикреплен к документу страницы». Это происходит потому, что экземпляр «page» был создан только один раз в конструкторе, а PageFactory «не может знать» :), что количество пользователей уменьшилось.

Обходные пути, которые я нашел до сих пор, являются эфиром: 

1) Не используйте Page Factory вообще для подобных методов, а просто используйте WebDriver напрямую с циклом while: driver.findElementsBy()...

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) или это:

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();
}

Но в первых примерах вы не можете извлечь выгоду из использования вашего упакованного BaseElement. А во втором и третьем - вы создаете новые экземпляры Page Factory для каждого отдельного действия, которое вы выполняете на странице. Итак, я думаю, все сводится к двум вопросам: 

1) Как вы думаете, действительно ли стоит использовать PageFactory для чего-нибудь, правда? Было бы намного «дешевле» создавать каждый элемент на лету, вот так:

class UserListPage
{
private WebDriver driver;

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

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

2) Можно ли использовать метод @Override driver.findElements, чтобы он возвращал экземпляр моего обернутого объекта BaseElement, а не WebElement? Если нет, есть ли альтернативы?

1
Vlad

В качестве продолжения этого я и закончил (на тот случай, если у кого-то еще возникла такая же проблема). Ниже приведен фрагмент класса, который я создал как оболочку для 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
}

И затем я использую это на любых страницах, которые создаются путем ссылки на него, например, так:

public class TestPage {

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

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

Единственное, что мне не нравится, это то, что когда страница хочет получить таблицу, она создает «новую» таблицу. Я сделал это, чтобы избежать StaleReferenceExeptions, который происходит, когда данные в таблице обновляются (то есть ячейка обновляется, строка добавляется, строка удаляется). Если у кого-то есть какие-либо предложения о том, как мне избежать создания нового экземпляра каждый раз, когда он запрашивается, а лучше вернуть обновленный WebElement, это было бы здорово!

1
pipplupp

Примечание: WebElement не является классом, его интерфейс , что означает, что ваш класс будет выглядеть примерно так:

public class TableWebElement implements WebElement {

Но в этом случае вы должны реализовать все методы, которые есть в WebDriver. И это своего рода излишество.

Вот как я это делаю - я полностью избавился от предложенных PageObjects, предложенных Selenium, и «заново изобрел колесо», имея свои собственные классы. У меня есть один класс для всего приложения:

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);
  }
}

И тогда у меня есть свои вспомогательные классы

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();

 }

}

Образец теста:

 @Test
 public void testTableSize(){
    WebUI web = new WebUI();
    TableWebElement myTable = web.getTable();
    Assert.assertEquals(myTable.getSize(), 25);
 }
1
Pavel Janicek

Посмотрите на htmlelements framework, это выглядит именно то, что вам нужно. Он имеет предварительно реализованные общие элементы, такие как флажки, радиобоксы, таблицы, формы и т.д., Вы можете легко создавать свои собственные и вставлять одни элементы в другие, создавая четкую древовидную структуру.

0
artkoshelev

Почему бы не сделать обертку веб-элемента? как это?

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

но вы можете сделать свой класс более сложным, чем этот. просто концепция

0
will

Я хотел бы создать то, что я называю «SubPageObject», который представляет PageObject на PageObject ...

Преимущество заключается в том, что вы можете создавать собственные методы для него и инициализировать только те веб-элементы, которые вам действительно нужны в этом SubPageObject.

0
Franz Ebner

Шаг 1) Создайте базовый класс с именем Element, который расширяет интерфейс IWrapsElement

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

Шаг 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";
    }
}

Использование:

а)

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

б) 

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

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

Зачем беспокоиться о расширении WebElement? Вы пытаетесь на самом деле разработать пакет Selenium? Тогда, если это так, не имеет смысла расширять этот метод, если только ваши дополнения не будут применимы к каждому отдельному веб-элементу, который вы используете, а не только к тем, которые относятся к вашей таблице.

Почему бы просто не сделать что-то вроде:

public class PermissionsPage extends TableWebElement{

...permissions stuff...

}

import WebElement

public class TableWebElement{

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

}

Я не знаю, отвечает ли это на ваш вопрос, но мне показалось, что это скорее вопрос классовой архитектуры, чем что-либо еще.

0
Greg