Интерфейс
FieldDecorator
используется фабрикой для декорирования объектов в классе страницы. Он предоставляет метод decorate
, который вызывается для каждого поля в классе. По умолчанию фабрика использует класс DefaultFieldDecorator
. Он определяет все WebElement и List<WebElement> объекты в классе и декорирует их с помощью прокси.
Зачем может понадобиться кастомный декоратор?
Интерфейс WebElement
не всегда удобно использовать. Его невозможно расширить и добавить свои методы для работы с элементом. Использовать свои методы можно только создавая классы-обвертки(CheckBox, Button и т.п.), которые просто делегируют вызов методов настоящему WebElement
. Но эти классы приходится вызывать в коде явно, передавая им объект WebElement
.
Использование кастомного декоратора позволяет инициализировать такие классы неявно. Такой подход значительно уменьшает количество кода и повышает его читаемость. Однако есть и своеобразный минус — для каждого найденного веб-элемента будет создаваться лишний объект в памяти, представляющий Вашу обвертку.
Класс-обвертка должен выглядеть примерно следующим образом, обязательным условием является наличие конструктора принимающего WebElement
:
import org.openqa.selenium.WebElement; public class Element { protected WebElement webElement; public Element(WebElement webElement) { this.webElement = webElement; } // универсальные методы для всех элементов }
Наследуясь от этого класса можно создавать уже более узконаправленные классы, отличающиеся по функциональности (кнопки, текстовые поля, списки):
import org.openqa.selenium.WebElement; public class CheckBox extends Element { public CheckBox(WebElement webElement) { super(webElement); } public void setChecked(boolean value) { if (value != isChecked()) { webElement.click(); } } public boolean isChecked() { return webElement.isSelected(); } }
Кастомный декоратор можно полностью написать самим, реализовав интерфейс FieldDecorator
. А можно унаследоваться от класса DefaultFieldDecorator
и использовать найденные им веб-элементы для создания своих классов. Обратите внимание, что приведенный ниже декоратор работает только с классами-обвертками и больше не обрабатывает WebElement
.
import java.lang.reflect.Field; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.pagefactory. DefaultElementLocatorFactory; import org.openqa.selenium.support.pagefactory.DefaultFieldDecorator; import org.openqa.selenium.support.pagefactory.ElementLocator; public class CustomFieldDecorator extends DefaultFieldDecorator { public CustomFieldDecorator(SearchContext searchContext) { super(new DefaultElementLocatorFactory(searchContext)); } /** * Метод вызывается фабрикой для каждого поля в классе */ @Override public Object decorate(ClassLoader loader, Field field) { Class<?> decoratableClass = decoratableClass(field); // если класс поля декорируемый if (decoratableClass != null) { ElementLocator locator = factory.createLocator(field); if (locator == null) { return null; } // элемент return createElement(loader, locator, decoratableClass); } return null; } /** * Возвращает декорируемый класс поля, * либо null если класс не подходит для декоратора */ private Class<?> decoratableClass(Field field) { Class<?> clazz = field.getType(); // у элемента должен быть конструктор, принимающий WebElement try { clazz.getConstructor(WebElement.class); } catch (Exception e) { return null; } return clazz; } /** * Создание элемента. * Находит WebElement и передает его в кастомный класс */ protected <T> T createElement(ClassLoader loader, ElementLocator locator, Class<T> clazz) { WebElement proxy = proxyForLocator(loader, locator); return createInstance(clazz, proxy); } /** * Создает экземпляр класса, * вызывая конструктор с аргументом WebElement */ private <T> T createInstance(Class<T> clazz, WebElement element) { try { return (T) clazz.getConstructor(WebElement.class) .newInstance(element); } catch (Exception e) { throw new AssertionError( "WebElement can't be represented as " + clazz ); } } }
Если нужно использовать одновременно и свои классы и WebElement
, замените в методе decorate
последнюю строку на:
return super.decorate(loader, field);
В приведенном примере декоратор работает только с отдельными элементами. О том как реализовать обработку списка кастомных элементов речь пойдет в следующей статье Selenium PageFactory и FieldDecorator (часть 2).
Справедливости ради стоит заметить, что вместе с этим декоратором можно использовать готовый класс org.openqa.selenium.support.ui.Select
, предоставляемый библиотекой Selenium. Но его методы ограничены лишь работой со списком, поэтому лучше все же написать свой.
Page Object класс страницы с использованием декоратора (здесь используется org.openqa.selenium.support.ui.Select
):
import org.openqa.selenium.WebDriver; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; import org.openqa.selenium.support.ui.Select; public class SelectExamplePage { private WebDriver driver; @FindBy(tagName = "select") private Select select; public SelectExamplePage(WebDriver driver) { PageFactory.initElements(new CustomFieldDecorator(driver), this); this.driver = driver; } public void printInfoAboutSelectElements() { System.out.println("Select option: " + select.getFirstSelectedOption().getText()); System.out.println("Is multiply: " + select.isMultiple() + "\n"); } }
Как быть с конструктором если целевая страница с переопределяемыми веб-элементами у нас наследуется от базовой?
Если вызов фабрики выполняется в конструкторе базового класса, то:
Но порядок другой:
public BasePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(new CustomFieldDecorator(driver), this);
}
Иначе волшебство не срабатывало.
В любом случае большое спасибо, за блог!
Очень вдохновляет.
Видимо потому, что в new CustomFieldDecorator(driver) у Вас передавался this.driver, а не параметр метода. Тут путаница получилась из-за имен, если бы у параметра было другое имя, то такая ситуация бы не возникла)))
а паттерн Декоратор не пробовали использовать? Почти как обертка, но при этом реализует интерфейс WebElement, что позволяет использовать кастомные элементы со стандартным DefaultFieldDecorator?
По задумке 🙂 Т.к. у меня пока не заработало, но не в слое Selenium (там все нормально определяется по интерфейсу WebElement), а глубже, на уровне Field.set() вылетает эксепшн sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException.
Интересно узнать пробовали ли вы и получилось ли?
Подскажите пожалуйста, а как решить проблему с обёртками в фукнциях ожидания? Они же используют читый WebElement
public WebElement waitElemByXpath(int sec, final String SELECTOR) {
LOGGER.info("Trying to find element by xpath: " + SELECTOR);
WebElement tempElement = (WebElement) (new WebDriverWait(this.driver, sec)).until(new ExpectedCondition() {
@Override
public WebElement apply(WebDriver driver) {
return driver.findElement(By.xpath(SELECTOR));
}
});
return tempElement;
Используйте конструктор обвертки, который принимает как раз WebElement