Проблемы с организацией объекта для работы с элементами.

Доброго времени суток. Пишу свой маленький framework. Придерживаюсь паттерна PageObject. Пытаюсь вынести все действия c WebElement (click, findElement, sendKeys) в отдельный класс BaseElement, для избегания дублирования и макаронного кода. Возникла проблема, с тем, что класс получается слишком большой (тут еще много чего добавлять можно для работы).Помогите с тем, как можно вынести методы в отдельные классы Click, Wait … без сильного ущерба для гибкости (подразумевается работа с одним элементом, но разными способами)

Сам класс

import framework.ConfigReader;
import framework.Log;
import framework.webdriver.Browser;
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.*;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


public class BaseElement {
    private WebDriver driver;
    private WebDriverWait wait;
    private WebElement element=null;
    private List<BaseElement> baseElementList;
    private List<WebElement> elementsList;

    private By locator=null;
    private String propertyElement ="";

    public BaseElement(By locator){
        driver= Browser.getInstance().getDriver();
        wait=new WebDriverWait(driver,ConfigReader.getTimeOuts(),1500);
        this.locator=locator;
    }

    public BaseElement(By locator,String propertyElement){
        this(locator);
        this.propertyElement = propertyElement;
    }

    public BaseElement(WebElement element){
        this(null,"");
        this.element=element;
    }

    public void click(){
       click(propertyElement);
    }

    public void click(String message){
        if(isClickble()){
            Log.info("Select: "+message);
            element.click();
        }else {
            Log.info("Don't select: " + message);
        }
    }

    public boolean isClickble(){
        try {
            if (element==null)
            element = waitElement(ExpectedConditions.elementToBeClickable(locator));
            return element != null && element.isDisplayed();
        } catch ( StaleElementReferenceException | IndexOutOfBoundsException e) {
            return false;
        }
    }

    public boolean isEnabled(){
        try {
            if (element==null)
                element = waitElement(ExpectedConditions.visibilityOfElementLocated(locator));
            return element != null && element.isEnabled();
        } catch ( StaleElementReferenceException | IndexOutOfBoundsException | TimeoutException e) {
            return false;
        }
    }

    private WebElement initWebElement(ExpectedCondition<WebElement> expectedConditions){
        element=wait.until(expectedConditions);
      return element;
    }

    private WebElement waitElement(final ExpectedCondition<WebElement> expectedConditions){
        new FluentWait<WebElement>(initWebElement(expectedConditions))
                .withTimeout(ConfigReader.getTimeOuts(),TimeUnit.SECONDS)
                .pollingEvery(1,TimeUnit.SECONDS)
                .until(new com.google.common.base.Function<WebElement, Boolean>() {
                    @Override
                    public Boolean apply(WebElement input) {
                       try{
                          initWebElement(expectedConditions);
                       }catch (StaleElementReferenceException | NoSuchElementException e){
                           return false;
                       }
                       return true;
                    }
                });
        return initWebElement(expectedConditions);
    }

    public void jsClick(){
        JavascriptExecutor executor = (JavascriptExecutor) Browser.getInstance().getDriver();
        executor.executeScript("arguments[0].click();", waitElement(ExpectedConditions.elementToBeClickable(locator)));
    }

    private void findElements(){
       waitElement(ExpectedConditions.visibilityOfElementLocated(locator));
            elementsList=driver.findElements(locator);
    }

    public List<BaseElement> getBaseElementList(){
        findElements();
        baseElementList=new ArrayList<BaseElement>(elementsList.size());
        for (WebElement anElementsList : elementsList) {
            baseElementList.add(new BaseElement(anElementsList));
        }
        return baseElementList;
    }

    public String getText(){
        if(isEnabled()) {
            return element.getText();
        }
        return "Don't search element";
    }

    public void sendKeys(String key){
        if(isEnabled()){
            Log.info("Sendkeys: "+key);
            element.sendKeys(key);
        }else{
            Log.info("Don't send "+key);
        }
    }

    public WebElement getWebElement(){
        if(isEnabled()){
            return element;
        }
        return null;
    }
}

Применение данного класса

BaseElement button=new BaseElement(By.xpath("//*[@class=\"cui-button\"]"));

public void selectButton(){
    button.click();
}

Благодарю за внимание и помощь.

Все фреймворки которые видел в паблике, на это забивают и фигачят все в базовый класс. Мне такое не нравиться, мне больше нравится, когда все четко разделено. Когда я кликаю на метод и попадаю в нужное место, где не намешано все подряд…

Мне так и не удалось найти изящное решение чтобы сделать много разных таких классов, и в каждом своя логика. Поэтому я себе сделал так:

используя Java8, можно делать интерфейсы с дефолтной реализацией. У такого подхода нет состояния, есть только реализация метода. Но это наверное даже больше плюс чем минус :slight_smile:

  • берешь класс BasePage и имплиментируешь ему интерфейс TestCase:
public abstract class BaseTest implements TestCase { 

}
  • В интерфейсе TestCase наследуешься от интерфейса Validator:
public interface TestCase extends Validator {
}

Мой Validator выглядит как-то так, пока он еще сырой, только недавно начал юзать такой подход:

public interface Validator {

    Logger logger = LogManager.getLogger(BaseTest.class);

    @Step("Verify characters count in string")
    default void verifyStringCount(String s, int expectedLength) {
        int actualLength = s.length();
        if (actualLength <= expectedLength) {
            logger.info("Current characters: " +
                    actualLength + ", and expected length: " + expectedLength);
        } else {
            throw new ExceptionInInitializerError("Current characters: " +
                    actualLength + ", and expected length: " + expectedLength);
        }
    }

    @Step("Verify browser console log entry")
    default void verifyBrowserConsoleLogEntry(List<String> listBrowserConsoleLog) {
        boolean condition = listBrowserConsoleLog.isEmpty();
        if (!condition) {
            htmlToAllureReport("Browser console log entry", String.join("<br><br>", listBrowserConsoleLog));
        }
        assertTrue(condition);
    }

    @Step("Verify that response code \"{1}\" = \"{0}\"")
    default void verifyResponseCodeEquals(final String actual, final String expected) {
        textToAllureAsStep("Actual", actual);
        textToAllureAsStep("Expected", expected);
        assertEquals(actual, expected);
    }

    @Step("Verify that \"{1}\" = \"{0}\"")
    default void verifyTextEquals(final String actual, final String expected, final String message) {
        textToAllureAsStep("Actual", actual);
        textToAllureAsStep("Expected", expected);
        textToAllureAsStep("Message", message);
        assertEquals(actual, expected, message);
    }

    @Step("Verify that \"{1}\" = \"{0}\"")
    default void verifyTextEquals(final String actual, final String expected) {
        textToAllureAsStep("Actual", actual);
        textToAllureAsStep("Expected", expected);
        assertEquals(actual, expected);
    }

    @Step("Verify that \"{0}\" contains \"{1}\"")
    default void verifyTextContains(final String actual, final String containsString) {
        htmlToAllureReport("Actual: "+actual+"<br><br>Contains string: "+containsString);
        assertTrue(actual.contains(containsString));
    }

    @Step("Verify that \"{0}\" not contains \"{1}\"")
    default void verifyTextNotContains(final String actual, final String containsString) {
        assertTrue(!actual.contains(containsString));
    }

    @Step("Verify that url \"{0}\" contains \"{1}\"")
    default void verifyUrlContains(final String actual, final String containsString) {
        textToAllureAsStep("Actual", actual);
        textToAllureAsStep("Expected", containsString);
        assertTrue(actual.contains(containsString));
    }

    @Step("Verify that code \"{0}\" is present on page")
    default void verifyThatCodeIsPresent(String code) {

    }

}

В тесте, который естественно наследуется от BaseTest, выглядит это так:

    @Title("Sign In with correct data")
    @Test(groups = "login_positive", priority = 10)
    public void signInWithCorrectData() {
        // --------------------- Test Case ----------------------//
        String pageTitle = controlPanelPage().openPage().loginWith("name", "pass").getPageTitle();
        verifyTextEquals(pageTitle, "Control Panel Page");
    }

Таким не хитрым образом ты в отдельном месте соберешь все методы по валидации, прикрутишь туда логирование + если надо какие-то вещи для репортинга. Зачем я сделал прослойку TestCase - для того, чтобы если надо то заэкстендиться более чем от одного. К примеру у меня там не только Validator, есть еще и другие. Таким образом все тесты будут получать методы для валидации тестов

Идем дальше… Теперь тоже самое проделываем для всех пейджов ПейджОбжекта:

public abstract class BasePage implements интерфейс_с_твоими_кликами, Cookies, Pages и т.д... {
}

к примеру Cookies у меня выглядит так:

public interface Cookies {

    default void clearCookies() {
            getDriver().manage().deleteAllCookies();
            getDriver().manage().deleteAllCookies();
            sleep(300);
    }

    default void clearCookies(String urlIgnoring) {
        if (!(getDriver().getCurrentUrl().equalsIgnoreCase(urlIgnoring))) {
            getDriver().manage().deleteAllCookies();
            getDriver().manage().deleteAllCookies();
            sleep(300);
        }
    }

    @Step("Add cookies for page with name = \"{0}\", value = \"{1}\"")
    default void addCookies(String cookiesName, String cookiesValue) {
        Cookie ck = new Cookie(cookiesName, cookiesValue);
        getDriver().manage().addCookie(ck);
        logger.info(String.format("add cookies with name = \"%s\", value = \"%s\"", cookiesName, cookiesValue));
    }

    /**
     * Deleting the specific cookie with cookie name
     * @param cookieName Cookie name "--utmb"
     */
    @Step("Deleting the specific cookie with cookie name \"{0}\"")
    default void deleteCookieNamed(final String cookieName) {
        getDriver().manage().deleteCookieNamed(cookieName);
    }
    
}

Pages:

public interface Pages {

    Logger logger = LogManager.getLogger(Pages.class);

    /**
     * @return Returns the current url of the web page
     */
    default String getPageUrl() {
        logger.info("Current url: "+url()+" of the web page");
        return url();
    }

    /**
     * @return Returns the title of the web page
     */
    default String getPageTitle() {
        return title();
    }

    /**
     * @return Returns the source code of the current page
     */
    default String getPageSource() {
        return source();
    }

}

Далее уже можешь эксперементировать, докурчивать, по сути у тебя для пейджов будет интерфейс Action или что-то подобное, и там ты будешь реализовывать дефолтные методы по работе с кликами…

Но если ты к этому решению прикрутишь Selenide, тогда решится твоя проблема с вейтами и кликами и пейджОбжект твой может выглядить так:

public class ControlAdminOrdersPage extends BasePage<ControlAdminOrdersPage> {

    @Override
    public String getUrl() {
        return CONTROL_ADMIN_BASE_URL.getValue()+"/orders";
    }

    private SelenideElement fieldOrderSearch   = $(By.xpath("//*[@id='orders_filter']/div/input"));
    private SelenideElement buttonOrderSearch  = $(By.xpath("//*[@id='orders_filter']/div/button"));
    private SelenideElement textOrderIdValue   = $(By.xpath("//*[@id='orders']/tbody/tr/td[5]/div"));
    private SelenideElement emailInOrdersTable = $("tr.row-parent").$$("td").get(9);

    @Step("Type OrderID in search orders field")
    public ControlAdminOrdersPage searchByOrderID(String orderId) {
        fieldOrderSearch.setValue(orderId);
        buttonOrderSearch.click();
        textOrderIdValue.shouldBe(visible).shouldHave(text(orderId));
        return this;
    }

    public String getOrderIdValue() {
        return textOrderIdValue.text();
    }

    public String getOrderEmailValue() {
        return emailInOrdersTable.text();
    }

    public ControlAdminCustomerInfoPage clickEmailFromOrdersTable() {
        emailInOrdersTable.click();
        return controlAdminCustomerInfoPage();
    }

}

У меня к примеру, с таким подходом + Selenide, все тесты получаются очень читаемыми, есть цепочки и очень быстро пишется пейджОбжект…

1 лайк

Крайне интересные подход и реализация. Обязательно возьму на внедрение.

Тоже не нравится нагромождение все в одном объекте, и так же находил большие кучи кода, но покопался в коде selenied. Там все действия вынесены в отдельные объекты и связываются они как раз через интерфейс+Generic() (interface Command). Их реализация пока сложна для моего уровня программирования.

Спасибо за столь емкий и развернутый ответ.
P.S. Думал вынести все действия в отдельные классы и сделать новый класс, где эти классы будут объявлены

class Wait{
}
class Click{
}
....
class BaseElement{
   Click clickElement=new Click();
   Wait waitElement=new Wait();

   void selectItem(){
      clickElement.click(WebElement);
   }
   void doubleClick(){
      clickElement.doubleClick(WebElement);
   }
   ......
}

Но встала проблема с связывание между собой классов, хранения самого WebElement и избегания дублирования и использования не нужного кода (в классе Click не должен искаться элемент). Право на жизнь тоже имеет, но с интерфейсами получается более изящное решение

ну дефолтние методи в интерфейсе кагби не для того что б их напихивать по 100500 - они кагби для сюпорта легаси кода били в 8 джаве добавлени, может лучше все таки в реальних имплементциях их оверрайдить

выше написал, что не самое элегантное решение. Но в целом, можно реализовать наследование, без ущерба проектированию… :slight_smile:

Были придуманы для легаси кода, но и в контексте этом тоже решают проблему и пока довольно хорошо… Я буду очень признателен, если мы вместе все подумаем и найдем причину так неделать и как сделать лучше :slight_smile:

Наследование, по мне, тяжелое решение. Означает жесткую привязку. Интерфейсы от части упрощают замену, как такового. В книге “философия Java” сказано, что не взяли от С++ множественное наследование, так как оно доставляет много головной боли. И на замену этому наследованию приходит от части интерфейсы

множественное наследование - зло… :slight_smile:

фишка в том, что у таких интерфейсов, как описал выше - нет состояния и есть общая реализация. Я этот способ подглядел где-то и прикрутил его для себя для разделения всех этих методов, которые копипастят в базовом классе. Даже если глянуть на Selenide, там что-то похожее, но там конечно интерфейсы без дефолтной реализации, там они как в целом и должно быть. Можно оттуда посмотреть как это делается и тоже сделать так же…

Еще раз спасибо за помощь. Буду прикручивать помаленьку…