By vs. WebElement

Доброй ночи,

У меня есть экспериментально-образовательный проект на котором провожу различного рода “опыты”, так сказать b-side проект для самого себя. Во имя поддержки паттерна PageObject взялся переделать локаторы типа By на WebElement с аннотацией @FindBy (PageFactory паттерн). И в процессе столкнулся с первой проблемой, что WebElement, например, не поддерживается методом ExpectedConditions.invisibilityOfElementLocated(WebElement a), который иногда очень полезен. Другая проблема это то, что в некоторых местах я использовал удобные конструкции вроде следующей, получая тем самым “динамичный” локатор:

private String someXpathLocator = "//div[@class='some_class']/div[%d]";

 public void doSmth(int amount) {
            driver.findElement(By.xpath(String.format(amount, someXpathLocator))).click();
        }

Конечно эти все “проблемы” можно обойти, например, используя оба: By и @FindBy. Но в итоге я задался вопросом — чем же таки выгодно использование @FindBy кроме удобной читаемости, @CacheLookup и lazy evaluation? (хотя эти аргументы несомненно весомые)

Сложилось впечталение, что By является более гибким решением. Интересно кто что использует на практике.

Спасибо.

И то и другое приводит к одному и тому же результату - webelemen’у.
Pageobject сделан для удобства создания и поддержки. По сути в pagefactory - сама библиотека за вас делает просто findElement.

Она много чего ещё делает. Например, ожидание элемента перед кликом по нему.
Но эффект от использования автоматической инициализации элементов только через FindBy достигается далеко не всегда.

Гораздо чаще работает механизм совместного использования автоматически инициализированных элементов и вызова findElement по требованию. А кто-то и вовсе не использует этот механизм

ожидание будет так же если вы сделаете findElement(By…).click(). Итоговой разницы нету.
Разница во времени инициализации. В одном случае - при обращении к полю, в другом - разумеется сразу.

Про совсместное использование - да. Я , например, иногда использую findBy хотя основную часть делаю через pagefactory

Поле класса страницы инициализируется в момент создания экземпляра класса, в методе initElements(), который навешивает на поля прокси. И одной из задач этих прокси является ожидание появления элемента в DOM-е страницы.

Поэтому разница есть. И она в том, что findElement(By...).click() не ждет появления элемента в DOM-е. Если он его не находит, то сразу выдает исключение. В отличие от элемента, инициализированного через @FindBy - он будет ожидать появления элемента заданное время и только потом упадет

Там есть свои нюансы, которые я не описал, но в общих чертах разница такая

Очень интересное замечание. По поводу ожиданий.

Посмотрим на следующий пример:

public class TestPage{

@FindBy(css ="#id")
private WebElement element;

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

public void click2(){
    webDriver.findElement(By.cssSelector("#id")).click();
}

}

Вы утверждаете, что click - он подождет элемент, а click2 - нет?

Да.

Точнее так: click2 точно не будет ждать, а click подождет в случае, если использовался AjaxElementLocator при декорировании полей класса во время инициализации. Т.к.DefaultElementLocator тоже никого не ждет

и имплицидные ожидания тут совсем не при чем?
В обычном примере - только что попробовал - время будет одним и тем же

Если включить, тогда по идее findElement() должен будет подождать. Но по умолчанию они отключены, так что я их не учитывал :slight_smile:

P.S. Я как-то раз включил их и у меня тесты стали падать не дожидаясь элементов. Хотя по идее implicitWait казалось бы ничего не может сломать. Поэтому я их тут же выключил :smile:

Просто всё ожидание оно вроде как завязывается на имплицидные ожидания. Не знаю, как насчет AjaxElementLocator(не копался), но в дефолтном виде initElements - будет ожидать если выставлены ожидания. и в том и в том случае.

Имплисит вейт работает на уровне драйвера браузера.
AjaxElementLocator тыкается с уровня селениума каждые 250мс, и посылает команду findElement драйверу, который будет ожидать установленный ранее имплисит вейт.

1 лайк

implicit waits (или ожидания драйвера) позволяют полить DOM, пока элемент не появится. Срабатывают они на уровне findElement. Живут по времени жизни драйвера. В случае AjaxElementLocatorFactory, в частности AjaxElementLocator будет в цикле делать то, что описали выше.

explicit waits (или клиентские ожидания) придуманы для ожидания конкретного элемента по заданному условию. Далеко не всегда нам нужно ждать именно появления элемента. Иногда нужно проверять, кликабелен он или нет, или вовсе - появление какого-то текста. В данном случае WebDriverWait в комбинации с ExpectedConditions предоставляют более гибкие решения. А ввиду того, что их механизм в большинстве основан на By локаторах, которые не привязаны ко времени загрузки страницы (ввиду поиска по запросу), то для многих ситуаций эта техника будет являться решением всех проблем с ожиданиями.

Лично для меня связка By + WebDriverWait + ExpectedConditions является более приемлемым вариантом. К тому же проблема красоты решается очень просто путем добавления кастомной аннотации и обертки над классом By.

2 лайка

Очень интересная дискуссия получилась!

Склоняюсь к этим двум вариантам. Хотя сам, до экспериментов с @FindBy, использовал второй способ, описанный @ArtOfLife. Похоже, что этот вариант является наиболее гибким.

Всем спасибо.

Обращаться к элементу страницы, в каждом отдельном методе задавая способ его поиска и ожидания, конечно, более гибко, но и накладывает дополнительные требования:

  1. локатор элемента должен быть единым, независимо от того в скольких методах происходит findElement() по локатору. Иначе, при изменении верстки выловить баги будет сложнее
  2. ожидание элемента так же необходимо делать при каждом findElement()

Получается, что лучше в классе страницы использовать единый метод получения рабочего элемента страницы, типа getOKButton(), перед взаимодействием с ним. Этот get-метод как раз должен выполнять поиск и ожидание необходимого элемента и возвращать рабочий элемент

В добавок отмечу, что гораздо эффективней в данной ситуации осуществлять поиск с ожиданием не в конкретной странице, а в базовой пейдже, сокрыв сам findElement внутри всех методов-оберток над WebDriver API. К примеру:

public void setText(final By locator, final CharSequence text, final Integer timeout) {
    findElement(locator, timeout).sendKeys(text);
}

public void setText(final By locator, final CharSequence text) {
    setText(locator, text, null);
}

При этом findElement может выглядеть следующим образом (без exceptions handling):

public WebElement findElement(final By locator, final Integer timeout) {
    if (timeout != null) {
        wait.withTimeout(timeout >= 0 ? timeout : 0, TimeUnit.SECONDS);
    }

    final WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    wait.withTimeout(DEFAULT_VISIBILITY_TIMEOUT, TimeUnit.SECONDS);

    return element;
}

где wait - объект WebDriverWait, а DEFAULT_VISIBILITY_TIMEOUT - некая константа.

По аналогии, любая другая обертка (click, getText, etc.) будет точно так же дергать кастомный findElement, что позволит нам использовать универсальный механизм ожиданий для элементов любой страницы.

При этом, ExpectedConditions можно сделать условным, в зависимости от, к примеру, дополнительного enum параметра. Либо вообще организовать гибкий механизм переключения между стандартным findElement и wait.until. Тут уже нет предела творчеству.

2 лайка

Така ж думка і в мене.

Кстати, в Java 8 при помощи функциональных интерфейсов можно очень легко соорудить универсальный waiter:

    public WebElement waitForElement(final By locator, final Function<By, ExpectedCondition<WebElement>> condition, final Integer timeout) {

        final WebElement element = wait.withTimeout(
                Optional.ofNullable(timeout)
                        .filter(value -> value >= 0)
                        .orElse(DEFAULT_TIMEOUT), TimeUnit.SECONDS)
                .until(condition.apply(locator));

        wait.withTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);

        return element;
    }

Дергать такой метод можно следующим образом:

waitForElement(By.id(""), ExpectedConditions::presenceOfElementLocated, 0);
waitForElement(By.xpath(""), ExpectedConditions::visibilityOfElementLocated, null);
waitForElement(By.cssSelector(""), ExpectedConditions::elementToBeClickable, 15);
1 лайк