Htmlelements: как лучше организовать ожидания

А вы не пробовали намекнуть разработчикам, что ожидать элементов по 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 совсем?..

Если я правильно понимаю разницу явных и неявных ожиданий, то я не использую имплицитных ожиданий вовсе. По крайней мере я совершенно точно нигде не использую конструкцию

driver.manage().timeouts().implicitlyWait(N, TimeUnit.SECONDS);

В моем случае (когда я наследуюсь от 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);
  }
}

я переопределяю метод IsElementUsable

 @Override
 protected boolean isElementUsable(final WebElement element) {
    return element.isDisplayed();
}

В моем понимании это и есть явное ожидание (ожидание до тех пор, пока элемент не станет displayed). Или я не права?

Этот негативный эффект очень сильно снижает профит от вашего автомейшена. Если будет много поломанных тестов, то total execution time возрастет на N * 2 min. Пока вы дождетесь результатов, мануальщики смогут все ручками перепроверить.

А вот и плохо, что не помните. Один локальный пример не должен влиять на весь подход. Перегрузка метода с кастомным таймаутом - гораздо более эффективное решение, нежели забитая в лоб константа, взятая с потолка, и о которой уже никто ничего не помнит.

Я уже ответил, почему. Вы передаете элемент, который был “кем-то” до этого найден и не меняет своего состояния. Т.е. вы просто в течении двух минут опрашиваете состояние неизменного элемента. Откуда драйвер узнает, что его состояние изменилось, если вы не пытаетесь его вновь найти? В том то и отличие: в случае с By осуществляется циклический поиск для рефреша состояния.

Достаточно давно живу без стандартной фабрики и ничуть не жалею. Даже написал кастомную обертку с аннотацией, чтобы было похоже на @FindBy + WebElement. Было бы желание.

1 лайк

Вы писали об этом в этой теме, верно? Можно ли почитать где-то подробнее о кастомной обертке с аннотацией?

Когда-то заливал в местный GitHub сырой пример. Насколько я помню, там еще нет Java 8 фишек, каких-либо оптимизаций и т.п. Но для понимания сути в целом будет достаточно.

1 лайк

Спасибо, пример очень полезный. Насколько я понимаю, все элементы имеют один тип 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 и будет вам счастье.

2 лайка

Большое спасибо, многое теперь прояснилось.

Кстати, я нашла ответ на первый свой вопрос, возможно это будет полезно тем, кто захочет использовать HTMLElements:

"Ability to use your own ElementLocator

Issue #4

Мы также добавили возможность использовать свою реализацию ElementLocator вместо нашей при инициализации элементов и page-объектов:

HtmlElementLoader.populateHtmlElement(HtmlElement htmlElement, CustomElementLocatorFactory locatorFactory)

HtmlElementLoader.populatePageObject(Object page, CustomElementLocatorFactory locatorFactory)"

А если использовать матчеры - задекорированные ожиданиями?

С одной стороны - это решает проблему. С другой стороны - использовать ассерты вне тестов - плохая практика…

А где там динамические изменения локаторов?
Там просто поиск внутри элементов

Не совсем понял, что есть - “поиск внутри элементов”? Под динамическим локатором подразумевалась возможность изменения его частей в рантайме.

@HTML(searchBy = XPATH, value = "//tbody/tr[?]/td[?]/a")
private HTMLElement gridCells;
// ...
click(updateElement(gridCells, "M", "N"));

Сорри, это я не про то написал, невнимательно прочитал сообщение твое. Я думал ты про htmlElements от яндекса

Вы передаете элемент, который был “кем-то” до этого найден и не меняет своего состояния. Т.е. вы просто в течении двух минут опрашиваете состояние неизменного элемента. Откуда драйвер узнает, что его состояние изменилось, если вы не пытаетесь его вновь найти? В том то и отличие: в случае с By осуществляется циклический поиск для рефреша состояния.

Сергей,такой вопрос,что бы до конца понять:
“кем-то” до этого найден - это,как я понимаю вебдрайвером и опрашивает тоже он,но почему тогда нельзя и на элемент с @FindBy повесить

                withTimeout(timeout, TimeUnit.SECONDS).
                ignoring(NoSuchElementException.class).
                ignoring(StaleElementReferenceException.class).
                pollingEvery(500, TimeUnit.MILLISECONDS);

и получить тоже самое что и с By ?

Повестить каким образом? Через 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
Я конечно,полезу в код чтобы разобратся,но если можете обьясните тоже детально.

Вот код с Гита на By и на WebElement…а в разницу “не вьежаю”

public static ExpectedCondition<WebElement> elementToBeClickable(
            final By locator) {
              return new ExpectedCondition<WebElement>() {

  public ExpectedCondition<WebElement> visibilityOfElementLocated =
    ExpectedConditions.visibilityOfElementLocated(locator);

  @Override
  public WebElement apply(WebDriver driver) {
    WebElement element = visibilityOfElementLocated.apply(driver);
    try {
      if (element != null && element.isEnabled()) {
        return element;
      } else {
        return null;
      }
    } catch (StaleElementReferenceException e) {
      return null;
    }
  }

  @Override
  public String toString() {
    return "element to be clickable: " + locator;
     }
   };
 }

 public static ExpectedCondition<WebElement> elementToBeClickable(
             final WebElement element) {
             return new ExpectedCondition<WebElement>() {

  public ExpectedCondition<WebElement> visibilityOfElement =
    ExpectedConditions.visibilityOf(element);

  @Override
  public WebElement apply(WebDriver driver) {
    WebElement element = visibilityOfElement.apply(driver);
    try {
      if (element != null && element.isEnabled()) {
        return element;
      } else {
        return null;
      }
    } catch (StaleElementReferenceException e) {
      return null;
    }
  }

  @Override
  public String toString() {
    return "element to be clickable: " + element;
      }
    };
  }

Ну во-первых, зачем вам FluentWait, если WebDriverWait уже итак его наследует?

Во-вторых, зачем усложнять, осуществляя поиск по GitHub, если можно прямо в IDEA через Ctrl заглянуть в любой класс драйвера, и в адекватном редакторе посмотреть все взаимосвязи?

В третьих, смотреть нужно глубже, т.к. чуть ли не первой инструкцией в обоих методах идет вызов совершенно разных хэлперов: visibilityOfElementLocated vs visibilityOf.

Когда узрите разницу, перечитайте еще раз, по какой причине возникает StaleElementReferenceException, и попробуйте связать все это воедино.

1 лайк

Подитожу что я понял:
разница хэлперов: visibilityOfElementLocated vs visibilityOf в том,что первый ищет елемент в DOMе и,соответственно ловит StaleElementReferenceException

try {
     return elementIfVisible(findElement(locator, driver));
    } catch (StaleElementReferenceException e) {
      return null;
    }

второй же просто возвращает обертку над 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 с динамической фильтрацией элементов, и поиграйтесь с ним.