Отслеживание изменения списка потомков в DOM

Пытаюсь реализовать кастомное условие на ожидание изменения списка потомков в DOM. Проблема в том, что размер этого списка может остаться прежним, при этом элементы могут обновиться. У меня есть окно выбора элементов, элементы которого я фильтрую при помощи строки поиска. После ввода строки в input, я хочу быть уверен, что список элементов был изменен.
image
image

Я не смог найти готовое решение среди Conditions в selenide, поэтому решил прибегнуть к исполнению js в рамках теста. Я попытался подключить MutationObserver, который бы отслеживал изменения в родительской ноде для этих элементов, но я не уверен в таком случае, что объект наблюдателя создаться до заполнения строки фильтра из за асинхронности.

Каким образом можно отслеживать изменения в DOM после заполнения строки фильтра?

Код:
Класс с кастомным условием

    public IsElementChanged(){
        super("IsElementChanged");
    }

    @CheckReturnValue
    public @NotNull CheckResult check(Driver driver, WebElement element) {

        String script =
                "var callback = arguments[arguments.length - 1];" +
                "function elementChanged(arguments[0]) {\n" +
                "    return new Promise((resolve) => { \n" +
                "            new MutationObserver((mutationsList, observer) => {\n" +
                "                if (mutationsList.length > 0) {\n" +
                "                    resolve();\n" +
                "                    observer.disconnect();\n" +
                "                }\n" +
                "\n" +
                "            })\n" +
                "        .observe(targetNode, {subtree: true, childList: true});\n" +
                "    });\n" +
                "};\n" +
                "elementChanged(targetNode).then(() => { return callback(true); })";


        Object isElementChanged = null;
        try {
             isElementChanged = executeAsyncJavaScript(script, element);
        }
        catch (Exception e){
            e.printStackTrace();
        }

        boolean result = isElementChanged != null && (boolean) isElementChanged;

        String actualValue = String.format("isElementChanged:%s", result);
        return new CheckResult(result ? CheckResult.Verdict.ACCEPT : CheckResult.Verdict.REJECT, actualValue);
    }

    @CheckReturnValue
    @Nonnull
    public Condition negate() {
        return new Not(this, true);
    }
}

Вызов этого условия:

    /**
     * Метод осуществляет фильтрацию строк по тексту в столбце
     * @param searchStr строка поиска для фильтра
     * @param columnName название столбца
     */
    private void filterForColumn(String searchStr, String columnName) {
        // Xpath для фильтра конкретного столбца в таблице
        final String columnFilterInputXpathString = headerFilterRowXpathString +
                "//td[contains(@aria-label,'" +
                columnName +
                "')]" +
                "//input";

        final By columnFilterInputLocator = By.xpath(columnFilterInputXpathString);

        $(columnFilterInputLocator)
                .shouldBe(Condition.visible)
                .should(ajaxFinished)
                .clear();

        $(columnFilterInputLocator)
                .shouldBe(Condition.visible)
                .should(ajaxFinished)
                .sendKeys(searchStr);

        $(By.xpath(contentXpathString))
                **.should(isElementChanged)**
                .should(ajaxFinished);
    }

Selenide 6.0.3

  • у каждого элемента списка делайте .getAttribute(“outerHTML”)
  • получите стринговый код хтмл этого элемента
  • некое действие на странице
  • опять дёргаете outerHTML и сравниваете

Но это прям решение в лоб
просто тексты брать в список и сравнивать не вариант?

Странный вопрос.

  1. До ввода текста: findAll, сохраняем в list
  2. Вводим текст.
  3. Ждем обновление списка (завершение асинхронных операций).
  4. FindAll, сохраняем в list.
  5. Сравниваем атрибуты элементов в lists.

Но я бы делал по-другому: исходя из скринов, задача - проверить, что каждый элемент списка после обновления содержит введенное слово, а не то, что что-то там меняется.
Итого:

  1. Вводим текст
  2. Ждем обновление списка (завершение асинхронных операций).
  3. Проверяем через regexp, что каждый элемент списка имеет в составе введенное слово.
    Профит!

А каким образом можно проверить завершение асинхронных операций? В этом проблема и заключается. Ожидание завершения ajax не гарантирует того, что список обновился

Тут есть 2 варианта имплементации:

  1. Если апдейт списка сделан через кол на бек. Тогда завершение ajax таки гарантирует апдейт.
  2. Апдейт выполняется на фронте. В этом случае ajax кол по очевидным причинам не выполняется.
    Проверить, какой из способов применён, можно элементарно: открыть дев тулз и проверить, выполяется ли кол во время апдейта или нет. При этом не мешало бы убедиться, что выполняются граничные условия: например, 3 буквы минимум для старта поиска.

Для случая 2 можно сделать в лоб: написать свой ExpectedCondition, который возьмет изначальный список (findAll) и будет ждать, пока текст не изменится (для красоты напишите свой Comparator) ну или я бы специально под этот тест сделал бы ExpectedCondition, который заканчивает ожидание в случае, когда все записи ( findAll) содержат введенный текст.

1 лайк

В общем-то так и сделал, написал свой CollectionCondition:
`public class DifferentFrom extends CollectionCondition {

private final List<WebElement> initialWebElements;

public DifferentFrom(ElementsCollection elementsCollection){
    this.initialWebElements = elementsCollection
            .stream()
            .map(SelenideElement::toWebElement)
            .collect(Collectors.toList());
}

@Override
public boolean test(List<WebElement> webElements){
    return !listEqualsIgnoreOrder(webElements, initialWebElements);
}

@Override
public void fail(CollectionSource collectionSource, @Nullable List<WebElement> elements, @Nullable Exception e, long l ){
    List<String> initialTexts = ElementsCollection.texts(initialWebElements);
    List<String> actualTexts = ElementsCollection.texts(elements);
    throw new NoDifferenceWebElementsError(initialTexts, actualTexts);
}

@Override
public boolean missingElementSatisfiesCondition(){
    return false;
}

Ну и собственно сам метод для сравнения:
public static <T> boolean listEqualsIgnoreOrder(List<T> list1, List<T> list2) { return new HashSet<>(list1).equals(new HashSet<>(list2)); }
Equals для RemoteWebElement решил не переопределять

1 лайк