Чого ви не знали про Selenide

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Привіт. Я Сергій — автоматизатор тестування з багаторічним досвідом.

Сьогодні поговоримо про Selenide. Але я не буду його порівнювати з іншими бібліотеками чи фреймворками. Це невелика замітка, як полегшити собі життя, використовуючи цей інструмент.

Отже. Ви знаєте, що для того, щоб вибирати опцію з випадаючого списка, використовується щось на кшталт:

SelenideElement element = $("#selectElement");
element.selectOptionByValue("value");

Там ще багато методів для подібних дій. Але ці методи працюють тільки зі списками з тегом select.

А що ж тоді робити, коли у вас списки кастомні. Наприклад, написані на Angular, React або ж на чомусь ще. Тоді цей метод не підійде. Треба писати щось своє і використовувати цей код усюди де Ви взаємодієте з такими списками. Приклади таких списків можна подивитись тут.

Давайте спробуємо написати один із варіантів обробки такого списка.

Важливо

В цій реалізації я не намагався показати гарний код. Це просто демонстрація можливості використання.

Отже, якщо ми тестуємо щось написане на React, треба розуміти, що всі списки однакові за своєю структурою. Тобто подібні списки будуть мати однаковий функціонал.

Спосіб перший

Розглянемо один цікавий клас у бібліотеці Selenide. Клас Commands. Саме цей клас відповідає за зв’язування команди Selenide з її реалізацією. Тобто ви можете написати свою команду і динамічно зв’язати її, щоб використовувати як нативну.

Тепер напишемо дві команди для демострації.

Клас ReactGetSelectedOptionText, яка буде нашою реалізацією для отримання тексту вибраної опції. Моя реалізацїя така:

import com.codeborne.selenide.Command;
import com.codeborne.selenide.Driver;
import com.codeborne.selenide.SelenideElement;
import com.codeborne.selenide.impl.WebElementSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class ReactGetSelectedOptionText implements Command<String> {
    @Nonnull
    @Override
    public String execute(SelenideElement proxy, WebElementSource selectElement, @Nullable Object[] args) {
        return execute(selectElement.driver(), selectElement.getWebElement());
    }
    @Nonnull
    public String execute(Driver driver, WebElement webElement) {
        if (webElement.findElement(By.cssSelector(".select__single-value")).isEnabled()){
            return webElement.findElement(By.cssSelector(".select__single-value")).getText();
        } else {
            return webElement.findElements(By.cssSelector(".select__multi-value")).get(0).getText();
        }
    }
}

Та клас ReactSelectOptionByTextOrIndex для вибору опції зі списку:

import com.codeborne.selenide.Command;
import com.codeborne.selenide.SelenideElement;
import com.codeborne.selenide.impl.Arguments;
import com.codeborne.selenide.impl.WebElementSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
import static com.codeborne.selenide.commands.Util.merge;
public class ReactSelectOptionByTextOrIndex implements Command<Void> {
    @Nullable
    @Override
    public Void execute(SelenideElement proxy, WebElementSource selectField, @Nullable Object[] args) {
        Arguments arguments = new Arguments(args);
        if (args == null || args.length == 0) {
            throw new IllegalArgumentException("Missing arguments");
        } else if (args[0] instanceof String firstOptionText) {
            List<String> texts = merge(firstOptionText, arguments.nth(1));
            selectOptionsByTexts(selectField, texts);
            return null;
        } else if (args[0] instanceof Integer firstOptionIndex) {
            int[] otherIndexes = arguments.nth(1);
            selectOptionsByIndexes(selectField, merge(firstOptionIndex, otherIndexes));
            return null;
        } else {
            throw new IllegalArgumentException("Unsupported argument (expected String or Integer): " + Arrays.toString(args));
        }
    }
    private void selectOptionsByTexts(WebElementSource selectField, List<String> texts) {
        WebDriver webDriver = selectField.driver().getWebDriver();
        selectField.getWebElement().click();
        WebElement options = webDriver.findElement(By.cssSelector(".select__menu"));
        List<WebElement> elements = options.findElements(By.cssSelector(".select__menu [id^=react-select][id*=-option-]"));
        texts.forEach(t -> elements.stream().filter(e -> e.getText().equals(t)).findFirst().get().click());
    }
    private void selectOptionsByIndexes(WebElementSource selectField, List<Integer> indexes) {
        WebDriver webDriver = selectField.driver().getWebDriver();
        selectField.getWebElement().click();
        WebElement options = webDriver.findElement(By.cssSelector(".select__menu"));
        List<WebElement> elements = options.findElements(By.cssSelector(".select__menu [id^=react-select][id*=-option-]"));
        indexes.forEach(i -> elements.get(i + 1).click());
    }
}

За суттю це переписані методи SelectOptionByTextOrIndex та GetSelectedOptionText самого Selenide. Так що тут ніякої магії.

Тепер давайте це все спробуємо зв’язати.

import com.codeborne.selenide.Configuration;
import com.codeborne.selenide.commands.Commands;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static com.codeborne.selenide.Selenide.*;
public class ExperimentsTests {
    @BeforeClass
    public void beforeClass() {
        Commands.getInstance().add("getSelectedOptionText", new ReactGetSelectedOptionText());
        Commands.getInstance().add("selectOption", new ReactSelectOptionByTextOrIndex());
    }
    @Test
    public void testExperiments() {
        Configuration.browser = "firefox";
        open("https://react-select.com/home");
        $$(".select__control").get(0).selectOption("Red");
        String selectedOptionText = $$(".select__control").get(0).getSelectedOptionText();
        Assert.assertEquals(selectedOptionText, "Red");
    }
}

Зверніть увагу на BeforeClass-метод. Саме в ньому ми зв’язуємо нашу реалізацію зі стандартними методами Selenide. Як ви далі можете бачити, в самому тесті ми вже використовуємо звичайні методи самого Selenide.

Це був простий і зрозумілий спосіб без магії.

Спосіб другий

А тепер додамо трохи дивини.

Є така штука у Java — ServiceLoader. Не буду вдаватися до подробиць, скажу лише одне. Цей механізм дає змогу при старті підміняти деякі класи або інтерфейси автоматично на свої реалізації.

Отже. У нас вже є реалізаціі двох методів для роботи з дропдауном. Тепер треба вказати Java, що ми хочемо їх використовувати з Selenide-тестами. Тут Андрій Сонцев(батько Selenide) пояснює, як це зробити. У репозиторії Selenide перелічено всі такі сервіси, які можна перегрузити. Зверніть увагу на те, що для нашого випадку таких сервісів нема. Але є сервіс com.codeborne.selenide.commands.Commands, що загалом нам теж підходить.

Тепер підготуємо наш проєкт для того, щоб це все спрацювало. По-перше, створимо свою реалізацію для com.codeborne.selenide.commands.Commands. Ми вже використовували цей клас явно. А тепер дамо це право Java. В мене вийшла така реалізація:

import com.codeborne.selenide.commands.Commands;
public class MyCommands extends Commands {
    public MyCommands() {
      super();
      add("getSelectedOptionText", new ReactGetSelectedOptionText());
      add("selectOption", new ReactSelectOptionByTextOrIndex());
   }
}

Тобто я викликаю в конструкторі конструктор батьківського класу і доповнюю його своїми реалізаціями.

Далі вам потрібно створити у вашому проєкті папку :

<project.root.dir>/src/main/resources/META-INF/services 

Після цього створити там файл з назвою:

com.codeborne.selenide.commands.Commands  

Безпосередньо у файлі треба прописати повну назву вашої реалізації. В моєму випадку це:

org.bryt.MyCommands

Остаточний варінт проєкту:

Тепер, коли ви запустите свої тести, то Java «сходить» у META-INF/services, подивиться, що треба підмінити, завантажить потрібні реалізації.

Після цього все буде працювати, як і в попередньому випадку, тільки без явного переоприділення потрібних методів.

В моєму випадку це якось так:

import com.codeborne.selenide.Configuration;
import org.testng.Assert;
import org.testng.annotations.Test;
import static com.codeborne.selenide.Selenide.*;
public class ExperimentsTests {
    
     @Test
     public void testExperiments() {
          Configuration.browser = "firefox";
          open("https://react-select.com/home");
          $$(".select__control").get(0).selectOption("Red");
          String selectedOptionText = $$(".select__control").get(0).getSelectedOptionText();
          Assert.assertEquals(selectedOptionText, "Red");
    }
}

Ось і все. Доволі просто.
Гарного вам тестування!

P.S. Заглядайте у мій Github-проект Playwrightium. Це спроба об’єднати Selenium java та Playwright Java.

👍ПодобаєтьсяСподобалось10
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі