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

Здравствуйте. У меня есть тестовый фреймворк на Page Object и большая каша в голове по поводу явных ожиданий. Хотелось бы прояснить для себя спорные вопросы и послушать советы опытных коллег.

  1. На данный момент я инициализирую элементы на страницах с помощью кастомного декоратора, который использует кастомный же локатор, унаследованный от AjaxElementLocator. Сделано все по образу и подобию http://internetka.in.ua/selenium-fielddecorator/ и http://internetka.in.ua/ajaxelementlocatorfactory/ для того, чтобы перед инициализацией элементов происходило ожидание до 120 секунд, пока элемент не станет isDisplayed.

Есть большое желание перейти на библиотеку HTMLElements, однако она использует AjaxElementLocator, то есть дожидается лишь появления элемента в DOMе, но при этом элемент может быть не isDisplayed, Да и ожидание, насколько я поняла, происходит до 5 секунд, что для нашего приложения маловато.

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

  1. Существует следующая проблема для элементов, инициализируемых PageFactory, Есть такой метод:

    public void waitUntilDisappear(final WebElement element) {
    (new WebDriverWait(driver, 120)).until(new ExpectedCondition() {
    @Override
    public Boolean apply(final WebDriver driver) {
    try {
    return !(element.isDisplayed());
    } catch (NoSuchElementException e) {
    return true;
    } catch (StaleElementReferenceException e) {
    return true;
    } catch (NullPointerException e) {
    log.error(“Element to disappear doesn’t exist at all”, e);
    throw e;
    }
    }

     });
    

    }

Он используется в ситуации, когда нужно отследить, что при нажатии на кнопку ОК исчезает попап и можно продолжать тест. Ожидалось, что работать этот метод будет достаточно быстро, то есть как только попап исчезнет, сработает либо условие !(element.isDisplayed()), либо выпадет одно из исключений. Но нет, из-за ожиданий, описанных в пункте 1 элемент сначала ожидается в течение 120 секунд, и только потом выбрасывается исключение.

Как лучше осуществить проверку, что элемент исчез с учетом того, что такая проверка используется в шаге теста, то есть assert в данном случае не уместен?

  1. Есть UI-ный элемент, унаследованный от абстрактного элемента, реализующего интерфейс WebElement (как Button и прочее в библиотеке HtmlELements), По сути это простой Input, при вводе теста в который посылается запрос на сервер, происходит поиск, сервер возвращает результат и затем выпадает список с подсказками. Соответственно список выпадает через некоторое время после ввода текста. Как в данном случае организовать ожидание выпадающего списка в классе этого элемента?

А вы не пробовали намекнуть разработчикам, что ожидать элементов по 2 минуты - нынче не слишком “современно” с точки зрения UX?

Предположу, что ваш метод так работает по причине того, что состояние элемента не обновляется в течении заявленных 2 минут ожидания. Вы посмотрите, как реализован invisibilityOfElementLocated в ExpectedConditions. Там происходит постоянный поиск элемента в заданном интервале, т.е. его состояние обновляется до тех пор, пока элемент не исчезнет, либо не будет достигнут порог.

В этом, мне кажется, во многом и заключается основное преимущество By над FindBy + WebElement. Описанные проблемы решаются на раз-два при помощи By + WebDriverWait + ExpectedConditions. Более того, перегрузив findElement via WebDriverWait + custom timeout, вы сможете достаточно гибко управлять поиском. А добавь вы еще капельку functional style от восьмерки, так и expected conditions можно будет легко передавать в качестве параметра. Тут ведь вся соль в том, что у вас есть множество элементов, которые надо ждать по разным условиям. От этого и нужно плясать в плане кастомизации.

Но, как говорится, - it’s up to you.

Хочу добавить что если implicityWait будет стоять 120 секунд то меньший таймаут на явных ожиданиях особой роли уже играть не будет.
Вот ссылка на похожий случай.
Могу предположить что стоит implicityWait поставить поменьше, а в особо тяжких случаях уже играться с явными ожиданиями и необходимыми тайм аутами.

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