Помогите разобраться с проблемой цикла в Selenide

Доброго времени суток.

В работе столкнулся с проблемой, попробую рассказать идею и суть проблемы.

На странице имеются динамические элементы, нахожу и храню с помощью аннотации @FIndBy и храню в коллекции элементов так:

@FindBy(xpath = ".//*[@id='inspectors']//i[contains(@class, 'icon--cancel--tn')]")
private ElementsCollection buttonRemoveChooseInspectors;

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

Пытался сделать через do-while

void test(){
    do{
            buttonRemoveChooseInspectors.get(0).click();
     }while(buttonRemoveChooseInspectors.size() != 0);
}

Но после того как удаляются все элементы, он все ровно пытается найти этот элемент,

Element not found {By.xpath: .//*[@id=‘inspectors’]//i[contains(@class, ‘icon–cancel–tn’)]}
Expected: visible

Помогите разобраться в чем тут может быть проблема.

.get() не изменяет .size() элемента. Я не слишком уверен в селенидовской ElementsCollection, возможно стоит попробовать .pop()

Привет!
Селенидовский ElementsCollection действительно не очень удачно сделан. Он был рассчитан на то, что элементы на странице могут подгружаться медленно, и ждёт, пока они подгрузятся. А вот на то, что элементы могут пропадать, мы как-то не рассчитывали. :frowning:

К счастью, есть простое решение.
Выкиньте нафиг @FindBy - как я недавно писал, это бесполезная хрень.

By xpath = By.xpath(".//*[@id='inspectors']//i[contains(@class, 'icon--cancel--tn')]");
for (SelenideElement element = $(xpath); element.exists();  element = $(xpath)) {
  element.click();
}

А вообще это какой-то стрёмный тест.
Какая от него польза? Что он вообще проверяет?

2 лайка

Тут скорее не проверка идет, а действия по удалению динамически созданных элементов.

Пример:

Допустим есть некий проект, за этим проектом можно закреплять пользователей разных ролей, добавлять можно любое количество пользователей. Ну и собственно в чем заключается тест, открывает некий проект, определяет какое количество пользователей закреплено за этим проектом и удаляет их (нажатием на кастомный элемент). Вот как то так.

Всё равно непонятно. Что именно он тестирует?
Чтобы протестировать удаление, достаточно кликнуть один элемент.

2 лайка

Может он тестирует поп-ап, который появляется после удаленыя всех пользователей :slight_smile:

А так, да, мне тоже кажется что удаление и добавление можно протестировать одним добавлением и удалением и потом сверкой списка пользователей.

Это известная особенность селениде:

Которая благодаря этой теме теперь может быть изменена :slight_smile:

Может конечно это не совсем твой случай, но очень похож имхо…

Пока что воркераунд в том, что бы не использовать обьекты ElementsCollection как переменные, а использовать их как “методы”.

То есть что бы заработал твой код, тебе нужно вместо

@FindBy(xpath = ".//*[@id='inspectors']//i[contains(@class, 'icon--cancel--tn')]")
private ElementsCollection buttonRemoveChooseInspectors;

использовать

private ElementsCollection buttonRemoveChooseInspectors() {
    return $$(By.xpath(".//*[@id='inspectors']//i[contains(@class, 'icon--cancel--tn')]"));
}

P.S.
Похдход с FindBy плохой в принципе, потому что способствует “избыточности кода”. Если тебе нужна сущность только в одном месте - то незачем ее выносить в переменную (если только эта сущность не нуждается в читабельном имени, без которого прийдется использовать комментарий что бы ее объяснить). А подход с FindBy - не оставляет выбора - и учитывая то что обычно нужно “динамические элементы” (для тих же вейтов) принуждает тебя всегда создавать переменные, не важно нужны они или нет. Конечно это замечание касается чистого селениума, в селениде элементы - ленивые динамические прокси сами по себе, поэтому ты можешь и пользоваться просто:

private ElementsCollection buttonRemoveChooseInspectors = $$(By.xpath(".//*[@id='inspectors']//i[contains(@class, 'icon--cancel--tn')]"));

и да, икспасы ЗЛО, ведь разве следующий код не более читабельный?:

private ElementsCollection buttonRemoveChooseInspectors = $$("#inspectors .icon--cancel--tn)]"));

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

Более правильное решение вот такое:

private ElementsCollection inspectors = $$("#inspectors");

public void removeInspector(int index) {
    inspectors.get(0).find(".icon--cancel--tn").click();
}

или если учитывать твои нужды и то что селениде сейчас не апдейтит корректно состояние коллекции при использовании get и size, то:

private ElementsCollection inspectors() { return $$("#inspectors"); }

public void removeInspector(int index) {
    inspectors().get(0).find(".icon--cancel--tn").click();
}

public void removeAllInspectors(int index) {
    while (!inspectors().isEmpty()) {
        removeInspector(0);
    }
}

или если ты хочешь все в стиле ООП, то:

public class Inspectors {
    private ElementsCollection inspectors() { return $$("#inspectors"); }
    
    public void remove(int index) {
        inspectors().get(0).find(".icon--cancel--tn").click();
    }
    
    public void removeAll(int index) {
        while (!inspectors().isEmpty()) {
            remove(0);
        }
    }
}

ну тут уже можно извращаться как только душе угодно, при этом конечно непонятно зачем, наверное из-за сильной любви к модным шаблонам типа ElementObject :slight_smile:

public class Inspector {
    SelenideElement self;
    
    public Inspector(SelenideElement container) {
        this.self = container
    }

    public void remove(int index) {
        self.find(".icon--cancel--tn").click();
    }

    // ...
}

public class Inspectors {
    private ElementsCollection inspectors() { return $$("#inspectors"); }
    
    public Inspector inspector(int index){
        return new Inspector(inspectors().get(index));
    }

    public void remove(int index) {
        inspector(0).remove();
    }
    
    public void removeAll(int index) {
        while (!inspectors().isEmpty()) {
            remove(0);
        }
    }
}

P.S. 2 Тем кто сейчас набежит защищать свои любимые икспасы - ЗЛОМ я их обозвал не потому что они никогда не нужны, а потому что они не нужны в 99 процентов случаев :wink: Ну ок… в 99 процентов случаев ЕСЛИ вы используете selenide, в котором все для чего обычно нужны икспасы реализовуется намного более удобно и читабельно через методы
ElementsCollection#findBy(Condition)
ElementsCollection#filterBy(Condition)
ElementsCollection#exclude(Condition)
Selectors.byText(String)
Selectors.withText(String)
SelenideElement#parent()

Но даже если у вас нет такого замечательного инструмента как селениде - создать набор таких методов в виде хелперов - ничего не стоит. Это повысит ваше КПД при написании тестов и их поддержке на порядок по сравнению с использованием этих ужасных громоздких и абсолютно нечитабельных икспасов.

Единственный 1% где таки стоит использовать икспас напрямую - это там где есть ну очень большой список айтемов на странице, в несколько десятков - и у вас ну очень много кода где вы делаете кучу махинаций с этими айтемами - тогда тот бенефит в скорости который дадут икспасы - действительно может быть существенным.

А так то - все поиски по тексту, поиски среди соседей/друзей/родителей/детей/братьев реализовываются намного более читабельно (а потому экономят время на поддержке) с методами упомянутыми выше.

просто пример:

$$("#todo-list li").findBy(exactText("4")).find(".destroy").click()

против

$(By.xpath("//*[@id='todo-list']//li[.//text()='4']//*[@class='destroy']").click()

:wink:

12 лайков

Спасибо, за подробное изложение и желание помочь разобраться, лайк!)

1 лайк

Привет! Можно ссылку на “Выкиньте нафиг @FindBy - как я недавно писал, это бесполезная хрень.”?

Ну вы вспомнили, конечно, спустя 5 лет… :slight_smile:

Возможно, я имел в виду эту статью: Настоящие пэдж-объекты · Andrei Solntsev

2 лайка

Спасибо большое! Просто перевожу проект довольно немаленький в Selenide и много читаю. Вот и попадаю на некоторые вопросы-ответы)

Удачи тогда!
Спрашивайте, если что.

1 лайк

Огромное спасибо!