А вы не пробовали намекнуть разработчикам, что ожидать элементов по 2 минуты - нынче не слишком “современно” с точки зрения UX?
Да, действительно было бы странно, если бы для отображения контента требовалось 2 минуты. Разумеется, это не так. Откуда взялся этот таймаут - уже не помню, из каких-то примеров и так сложилось. Но даже если бы он был 10 минут, какое это имеет значение, если ожидание происходит ровно до того момента, пока элемент не покажется, а это может быть меньше секунды? Я вижу только один негативный эффект от большого таймаута, в случае, если тест сломается и элемент не найдется, прохождение теста займет длительное время. Впрочем, согласна, 2 минуты - перебор даже для подстраховки.
Вы посмотрите, как реализован invisibilityOfElementLocated в ExpectedConditions.
Именно с invisibilityOfElementLocated мой метод и был скопирован, вот он:
public static ExpectedCondition<Boolean> invisibilityOfElementLocated(
final By locator) {
return new ExpectedCondition<Boolean>() {
@Override
public Boolean apply(WebDriver driver) {
try {
return !(findElement(locator, driver).isDisplayed());
} catch (NoSuchElementException e) {
// Returns true because the element is not present in DOM. The
// try block checks if the element is present but is invisible.
return true;
} catch (StaleElementReferenceException e) {
// Returns true because stale element reference implies that element
// is no longer visible.
return true;
}
}
Я так и не разобралась, почему с By этот метод работает как и ожидается, а в моем случае (с WebElement) нет.
В этом, мне кажется, во многом и заключается основное преимущество By над FindBy + WebElement.
Всецело согласна. Однако при использовании PageFactory использовать By становится невозможным. Как быть в этом случае: не использовать PageFactory совсем?..
Если я правильно понимаю разницу явных и неявных ожиданий, то я не использую имплицитных ожиданий вовсе. По крайней мере я совершенно точно нигде не использую конструкцию
В моем случае (когда я наследуюсь от AjaxElementLocatorFactory) ожидание происходит следующим образом:
В AjaxElementLocatorFactory переопределен метод findElement
@Override
public WebElement findElement() {
SlowLoadingElement loadingElement = new SlowLoadingElement(clock, timeOutInSeconds);
try {
return loadingElement.get().getElement();
} catch (NoSuchElementError e) {
throw new NoSuchElementException(
String.format("Timed out after %d seconds. %s", timeOutInSeconds, e.getMessage()),
e.getCause());
}
}
SlowLoadingElement.get():
@Override
@SuppressWarnings("unchecked")
public T get() {
try {
isLoaded();
return (T) this;
} catch (Error e) {
load();
}
long end = clock.laterBy(SECONDS.toMillis(timeOutInSeconds));
while (clock.isNowBefore(end)) {
try {
isLoaded();
return (T) this;
} catch (Error e) {
// Not a problem, we could still be loading
}
isError();
waitFor();
}
isLoaded();
return (T) this;
}
isLoaded():
@Override
protected void isLoaded() throws Error {
try {
element = AjaxElementLocator.super.findElement();
if (!isElementUsable(element)) {
throw new NoSuchElementException("Element is not usable");
}
} catch (NoSuchElementException e) {
lastException = e;
// Should use JUnit's AssertionError, but it may not be present
throw new NoSuchElementError("Unable to locate the element", e);
}
}
Этот негативный эффект очень сильно снижает профит от вашего автомейшена. Если будет много поломанных тестов, то total execution time возрастет на N * 2 min. Пока вы дождетесь результатов, мануальщики смогут все ручками перепроверить.
А вот и плохо, что не помните. Один локальный пример не должен влиять на весь подход. Перегрузка метода с кастомным таймаутом - гораздо более эффективное решение, нежели забитая в лоб константа, взятая с потолка, и о которой уже никто ничего не помнит.
Я уже ответил, почему. Вы передаете элемент, который был “кем-то” до этого найден и не меняет своего состояния. Т.е. вы просто в течении двух минут опрашиваете состояние неизменного элемента. Откуда драйвер узнает, что его состояние изменилось, если вы не пытаетесь его вновь найти? В том то и отличие: в случае с By осуществляется циклический поиск для рефреша состояния.
Достаточно давно живу без стандартной фабрики и ничуть не жалею. Даже написал кастомную обертку с аннотацией, чтобы было похоже на @FindBy + WebElement. Было бы желание.
Когда-то заливал в местный GitHub сырой пример. Насколько я помню, там еще нет Java 8 фишек, каких-либо оптимизаций и т.п. Но для понимания сути в целом будет достаточно.
Спасибо, пример очень полезный. Насколько я понимаю, все элементы имеют один тип HTMLElement, то есть Вы не используете типизацию элементов (Button, Input и прочее). Не было необходимости, не считаете нужным в принципе или просто не было реализовано в конкретном примере?
Правильно ли я поняла, что ожидания в этом примере только имплицитные?
Да, HTMLElement - обертка над классом By с поддержкой изменения локатора на лету (формирование динамического пути).
В спуске до уровня PageElement не видел необходимости. Такой подход обоснован, когда у вас много кастомных элементов. В случае со стандартными, мне кажется это лишним оверхэдом. У меня были проекты различного уровня сложности, но изобретать свои собственные элементы пришлось лишь пару раз - для Select2 и кастомного грида. В остальном, всегда обходился чистым PageObject.
В приведенном примере нет кастомного findElement с WebDriverWait, но как вы правильно заметили, он есть в данной теме. Т.е. можете смело применять его на практике.
public WebElement findElement(final HTMLElement element,
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(element.getLocator()));
wait.withTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
return element;
}
public WebElement findElement(final HTMLElement element,
final Function<By, ExpectedCondition<WebElement>> condition) {
return findElement(element, condition, null);
}
public WebElement findElement(final HTMLElement element) {
return findElement(element, ExpectedConditions::visibilityOfElementLocated);
}
public void click(final HTMLElement element) {
findElement(element).click();
}
Если при этом нужно что-то упростить / кастомизировать, просто перегружайте findElement и будет вам счастье.
Вы передаете элемент, который был “кем-то” до этого найден и не меняет своего состояния. Т.е. вы просто в течении двух минут опрашиваете состояние неизменного элемента. Откуда драйвер узнает, что его состояние изменилось, если вы не пытаетесь его вновь найти? В том то и отличие: в случае с By осуществляется циклический поиск для рефреша состояния.
Сергей,такой вопрос,что бы до конца понять:
“кем-то” до этого найден - это,как я понимаю вебдрайвером и опрашивает тоже он,но почему тогда нельзя и на элемент с @FindBy повесить
Повестить каким образом? Через WDWait? Просто загляните в класс ExpectedConditions, и посмотрите, каким образом осуществляется опрос элементов в случае с By и WebElement. Помимо этого, попытайтесь понять, что в случае с @FindBy вы не контролируете процесс поиска, этим занимается фабрика. В случае с By вы вольны сами выбирать момент, когда и что искать, и какого события ожидать.
Вот мой универсальний вейтер,который используется или сам по себе или его оборачивают click(),input() и тд.
public <V> V $(Function<? super WebDriver, V> condition, int timeout) {
try {
Wait<WebDriver> wait = new FluentWait<WebDriver>(driver).
withTimeout(timeout, TimeUnit.SECONDS).
ignoring(NoSuchElementException.class).
ignoring(StaleElementReferenceException.class).
pollingEvery(500, TimeUnit.MILLISECONDS);
return wait.until(condition);
} catch (TimeoutException e) {
LOG.error("Element hasn't been found:TIMEOUT EXCEPTION");
return null;
}
}
Я действительно не могу понять разницу поиска если я в condition передам елемент @FindBy…
Я конечно,полезу в код чтобы разобратся,но если можете обьясните тоже детально.
Ну во-первых, зачем вам FluentWait, если WebDriverWait уже итак его наследует?
Во-вторых, зачем усложнять, осуществляя поиск по GitHub, если можно прямо в IDEA через Ctrl заглянуть в любой класс драйвера, и в адекватном редакторе посмотреть все взаимосвязи?
В третьих, смотреть нужно глубже, т.к. чуть ли не первой инструкцией в обоих методах идет вызов совершенно разных хэлперов: visibilityOfElementLocated vs visibilityOf.
Когда узрите разницу, перечитайте еще раз, по какой причине возникает StaleElementReferenceException, и попробуйте связать все это воедино.
Подитожу что я понял:
разница хэлперов: visibilityOfElementLocated vs visibilityOf в том,что первый ищет елемент в DOMе и,соответственно ловит StaleElementReferenceException
второй же просто возвращает обертку над element.isDisplayed() ? element : null;
return elementIfVisible(element);
Когда же этот StaleElementReferenceException вылетает :
A StaleElementException is thrown when the element you were interacting is destroyed and then recreated. Most complex web pages these days will move things about on the fly as the user interacts with it and this requires elements in the DOM to be destroyed and recreated.
When this happens the reference to the element in the DOM that you previously had becomes stale and you are no longer able to use this reference to interact with the element in the DOM. When this happens you will need to refresh your reference, or in real world terms find the element again.
То бишь,поля (поллинг) DOM мы получаем всегда свежий результат для взаимодействия с елементом при использовании By и возможность получить “черствую булочку” в виде елемента который только опрашивается на isDisplayed().
Это касательно visibility,но,думаю,в других кондышенах суть та же.
Если ошибся или есть дополнения буду благодарен.
P.S. Наверное я путаюсь так как не могу технически понять разницу между механизмами
“переискивать” и “опрашивать”.
Как понимаю : при первом действии,как писал выше “получаем всегда свежий результат для взаимодействия”,при втором обращаемся к состоянию которое было на момент нахождения елемента.Но это состояние может изменится,ведь для етого мы используем вейтер.То есть,для профилактики StaleElementException мне очевидно преимущество,а для других нет.
И @FindBy ищет елемент только при обращении ,а не когда-то нашол и все время использует.
Только практика поможет вам разобраться в таком случае. Возьмите, к примеру, какой-то кастомный Select2 с динамической фильтрацией элементов, и поиграйтесь с ним.