Що це. Selenide — це Java-бібліотека для UI-тестів, побудована над Selenium WebDriver. Вона не замінює WebDriver і не має власного протоколу: під капотом ті самі chromedriver, geckodriver чи Grid. Selenide лише ховає рутину, яку в чистому Selenium кожна команда пише сама.
What it is. Selenide is a Java library for UI tests built on top of Selenium WebDriver. It does not replace WebDriver and has no protocol of its own: under the hood there is the same chromedriver, geckodriver or Grid. Selenide simply hides the boilerplate that every team otherwise writes by hand in plain Selenium.
Що саме додає. По-перше, розумні очікування: кожна дія і кожна перевірка на кшталт shouldBe(visible) повторюється, доки умова не виконається або не мине Configuration.timeout. По-друге, ліниві елементи: $("#login") шукає вузол у момент дії, тому StaleElementReferenceException практично зникає. По-третє, керування браузером: драйвер створюється при першому open(), живе один на потік і закривається сам. По-четверте, діагностика: при падінні автоматично зберігаються скріншот і HTML сторінки, а повідомлення показує локатор, очікувану умову й фактичне значення. Плюс готові речі для файлів, фреймів, алертів, колекцій і Allure.
What exactly it adds. First, smart waiting: every action and every check such as shouldBe(visible) is retried until the condition holds or Configuration.timeout runs out. Second, lazy elements: $("#login") looks the node up at the moment of the action, so StaleElementReferenceException practically disappears. Third, browser management: the driver is created on the first open(), lives one per thread and closes itself. Fourth, diagnostics: on failure a screenshot and the page HTML are saved automatically, and the message shows the locator, the expected condition and the actual value. On top of that come ready helpers for files, frames, alerts, collections and Allure.
Коли обирати. Коли команда пише на Java, тестує звичайний веб-застосунок і хоче короткі, читабельні тести без власного фреймворку-обгортки. Для нестандартних речей завжди можна дістати сирий WebDriver.
When to choose it. When the team writes Java, tests a regular web application and wants short, readable tests without building its own wrapper framework. For anything unusual you can always get the raw WebDriver.
Підводні камені. Selenide — не тестовий раннер: запуск, життєвий цикл і паралельність дає JUnit 5 або TestNG. І він не робить повільний застосунок швидким: якщо елемент з’являється за 10 секунд, тест чекатиме 10 секунд або впаде з таймаутом.
Pitfalls. Selenide is not a test runner: running, lifecycle and parallelism come from JUnit 5 or TestNG. And it does not make a slow application fast: if an element appears after 10 seconds, the test either waits 10 seconds or fails with a timeout.
// Selenium: очікування, пошук і перевірка вручну
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(4));
driver.get("https://shop.example.com/login");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("email"))).sendKeys("anna@example.com");
driver.findElement(By.id("password")).sendKeys("secret");
driver.findElement(By.cssSelector("button[type=submit]")).click();
WebElement greeting = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".greeting")));
assertEquals("Hello, Anna", greeting.getText());
// Selenide: те саме, очікування вбудовані в кожен рядок
open("https://shop.example.com/login");
$("#email").setValue("anna@example.com");
$("#password").setValue("secret");
$("button[type=submit]").click();
$(".greeting").shouldHave(exactText("Hello, Anna"));
// Selenium: waits, lookups and assertions by hand
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(4));
driver.get("https://shop.example.com/login");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("email"))).sendKeys("anna@example.com");
driver.findElement(By.id("password")).sendKeys("secret");
driver.findElement(By.cssSelector("button[type=submit]")).click();
WebElement greeting = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".greeting")));
assertEquals("Hello, Anna", greeting.getText());
// Selenide: the same, with waiting built into every line
open("https://shop.example.com/login");
$("#email").setValue("anna@example.com");
$("#password").setValue("secret");
$("button[type=submit]").click();
$(".greeting").shouldHave(exactText("Hello, Anna"));
WebDriverWait і ExpectedConditions треба писати там, де ти здогадався, що елемент може запізнитися. У Selenide $ повертає лінивий SelenideElement, а setValue, click і shouldHave самі чекають до Configuration.timeout. exactText — умова з класу Condition, яку зазвичай імпортують статично.WebDriverWait and ExpectedConditions wherever you guessed the element might be late. In Selenide, $ returns a lazy SelenideElement, and setValue, click and shouldHave wait on their own up to Configuration.timeout. exactText is a condition from the Condition class, usually imported statically.Що потрібно. Основний артефакт — com.codeborne:selenide з Maven Central. Він тягне Selenium транзитивно, тому окремо selenium-java додавати не треба, а драйвер браузера завантажує вбудований у Selenium 4 Selenium Manager. Гілка 7.x вимагає Java 17 або новішої. Інтеграції з JUnit 4 і TestNG винесені в окремі артефакти selenide-junit4 і selenide-testng, а для JUnit 5 усе потрібне вже є в основному.
What you need. The main artifact is com.codeborne:selenide from Maven Central. It brings Selenium transitively, so you do not add selenium-java separately, and the browser driver is downloaded by Selenium Manager, which is built into Selenium 4. The 7.x line requires Java 17 or newer. JUnit 4 and TestNG integrations live in separate artifacts, selenide-junit4 and selenide-testng, while everything JUnit 5 needs is already in the main one.
Приклад. Тест — звичайний клас JUnit 5 зі статичними імпортами Selenide.* і Condition.*. open(url) відкриває сторінку і при першому виклику сам запускає браузер, за замовчуванням Chrome. Жодного new ChromeDriver(), @BeforeEach зі створенням драйвера чи quit() у @AfterEach писати не треба.
Example. A test is an ordinary JUnit 5 class with static imports of Selenide.* and Condition.*. open(url) opens the page and, on the first call, starts the browser itself, Chrome by default. There is no new ChromeDriver(), no driver creation in @BeforeEach and no quit() in @AfterEach.
Підводні камені. Не змішуй мажорні версії: якщо в pom є ще й старий selenium-java 3.x або інша 4.x, Maven може підтягнути несумісний набір класів, і тест впаде з дивним NoSuchMethodError. Краще взагалі не оголошувати Selenium окремо. Друга пастка — забути статичні імпорти: тоді IDE пропонує $ із зовсім іншої бібліотеки.
Pitfalls. Do not mix major versions: if the pom also has an old selenium-java 3.x or a different 4.x, Maven can resolve an incompatible set of classes and the test dies with a strange NoSuchMethodError. It is better not to declare Selenium separately at all. The second trap is forgetting the static imports: the IDE then offers a $ from a completely different library.
// pom.xml
// <dependency>
// <groupId>com.codeborne</groupId>
// <artifactId>selenide</artifactId>
// <version>7.18.2</version>
// <scope>test</scope>
// </dependency>
import org.junit.jupiter.api.Test;
import static com.codeborne.selenide.Condition.*;
import static com.codeborne.selenide.Selenide.*;
class SearchTest {
@Test
void searchShowsResults() {
open("https://duckduckgo.com");
$("[name=q]").setValue("selenide").pressEnter();
$$("[data-testid=result]").first().shouldHave(text("selenide"));
}
}
groupId com.codeborne і artifactId selenide; для Gradle той самий рядок пишуть як testImplementation("com.codeborne:selenide:7.18.2"). import static робить доступними open, $, $$ і умови на кшталт text без префікса класу. setValue(...).pressEnter() — ланцюжок: кожна дія повертає той самий елемент.groupId com.codeborne and artifactId selenide; in Gradle the same line is written as testImplementation("com.codeborne:selenide:7.18.2"). import static makes open, $, $$ and conditions like text available without a class prefix. setValue(...).pressEnter() is a chain: each action returns the same element.Що це. Навігацію дають статичні методи класу Selenide. open(url) відкриває адресу; якщо вона починається зі слеша, Selenide доклеює її до Configuration.baseUrl (за замовчуванням http://localhost:8080). refresh() перезавантажує поточну сторінку, back() і forward() рухаються історією браузера, title() повертає заголовок, а WebDriverRunner.url() — поточну адресу.
What it is. Navigation comes from static methods of the Selenide class. open(url) opens an address; if it starts with a slash, Selenide appends it to Configuration.baseUrl (by default http://localhost:8080). refresh() reloads the current page, back() and forward() move through browser history, title() returns the page title and WebDriverRunner.url() returns the current address.
Коли що. baseUrl задають один раз у конфігурації або через -Dselenide.baseUrl=..., а в тестах пишуть лише шляхи на кшталт open("/cart"). Тоді той самий набір без змін запускається на dev, stage чи локальному стенді. Є і варіант open("/profile", ProfilePage.class), який одразу повертає Page Object.
When to use what. baseUrl is set once in configuration or via -Dselenide.baseUrl=..., and tests only contain paths such as open("/cart"). The same suite then runs unchanged against dev, stage or a local stand. There is also open("/profile", ProfilePage.class), which returns a Page Object straight away.
Приклад. Тест кошика: відкрити каталог, додати товар, перезавантажити сторінку і переконатися, що лічильник не скинувся, потім повернутися назад. Саме refresh() часто ловить баги зі станом, який зберігся лише в пам’яті фронтенду.
Example. A cart test: open the catalogue, add an item, reload the page and make sure the counter did not reset, then go back. It is exactly refresh() that often catches bugs where state lived only in front-end memory.
Підводні камені. Після back() чи refresh() не треба нічого чекати вручну: наступна перевірка сама дочекається елемента. Але старі посилання на сирі WebElement після перезавантаження точно стануть недійсними, тому в Selenide-коді їх не зберігають. І не плутай open() без аргументів: він лише запускає браузер з порожньою сторінкою, нічого не відкриваючи.
Pitfalls. After back() or refresh() you do not wait for anything by hand: the next assertion waits for its element anyway. But references to raw WebElement objects are certainly invalid after a reload, which is why Selenide code does not keep them. And do not confuse the no-argument open(): it only starts the browser with a blank page and opens nothing.
Configuration.baseUrl = "https://stage.shop.example.com";
open("/catalog");
$(".product").$("button.add").click();
$("#cart-count").shouldHave(exactText("1"));
refresh();
$("#cart-count").shouldHave(exactText("1"));
open("/cart");
back();
webdriver().shouldHave(urlContaining("/catalog"));
Configuration.baseUrl — статичне поле, яке зазвичай задають у @BeforeAll або в selenide.properties. open("/catalog") перетворюється на https://stage.shop.example.com/catalog. webdriver().shouldHave(urlContaining(...)) — перевірка адреси з очікуванням; умова urlContaining імпортується з WebDriverConditions.Configuration.baseUrl is a static field usually set in @BeforeAll or in selenide.properties. open("/catalog") becomes https://stage.shop.example.com/catalog. webdriver().shouldHave(urlContaining(...)) is a URL check with waiting; the urlContaining condition is imported from WebDriverConditions.Що це. SelenideElement — інтерфейс, який розширює звичайний WebElement і додає методи Selenide: should*, setValue, download, scrollTo тощо. Об’єкт, який повертає $(".price"), — це проксі, що зберігає лише спосіб пошуку, а не знайдений DOM-вузол.
What it is. SelenideElement is an interface that extends a regular WebElement and adds Selenide methods: should*, setValue, download, scrollTo and so on. The object returned by $(".price") is a proxy that stores only how to find the element, not a found DOM node.
Як працює лінивість. Сам виклик $ нічого не робить у браузері. Пошук відбувається тоді, коли ти викликаєш дію або перевірку, і повторюється при кожному такому виклику. Тому елемент можна оголосити полем Page Object ще до відкриття сторінки, і тому він не «застаріває», коли фронтенд перемальовує DOM.
How laziness works. Calling $ does nothing in the browser. The lookup happens when you call an action or an assertion, and it is repeated on every such call. That is why an element can be declared as a Page Object field before the page is even open, and why it does not go stale when the front end re-renders the DOM.
Приклад. Поле SelenideElement total = $(".cart-total") створюється разом із класом сторінки. Після кожного додавання товару фреймворк заново знаходить блок суми і читає свіже значення, хоча React міг замінити вузол повністю.
Example. The field SelenideElement total = $(".cart-total") is created together with the page class. After every item is added, the framework finds the total block again and reads the fresh value, even though React may have replaced the node entirely.
Підводні камені. Лінивість означає, що помилка в селекторі проявиться не в рядку з $, а в першій дії, і лише після таймауту. Метод cached() вимикає повторний пошук заради швидкості, але повертає ризик застарілого елемента, тож його варто брати лише для статичних частин сторінки. А toWebElement() віддає сирий елемент без очікування і без захисту.
Pitfalls. Laziness means a typo in a selector shows up not on the line with $ but on the first action, and only after the timeout. The cached() method turns off re-lookup for speed but brings back the risk of a stale element, so use it only for static parts of the page. And toWebElement() hands out a raw element with no waiting and no protection.
public class CartPage {
// тут браузер ще нічого не шукає
private final SelenideElement total = $(".cart-total");
private final SelenideElement addButton = $("[data-test=add-item]");
public void addItemAndCheckTotal(String expected) {
addButton.click(); // пошук + очікування + клік
total.shouldHave(exactText(expected)); // новий пошук, навіть якщо DOM перемальовано
}
}
public class CartPage {
// the browser has not searched for anything yet
private final SelenideElement total = $(".cart-total");
private final SelenideElement addButton = $("[data-test=add-item]");
public void addItemAndCheckTotal(String expected) {
addButton.click(); // lookup + wait + click
total.shouldHave(exactText(expected)); // a fresh lookup, even if the DOM was re-rendered
}
}
SelenideElement ініціалізуються викликом $ прямо в оголошенні, бо це дешево: створюється лише проксі з CSS-селектором. click() спершу чекає, доки елемент стане видимим і доступним, а shouldHave(exactText(...)) знову шукає вузол і порівнює текст, повторюючи спробу до таймауту.SelenideElement are initialised with $ right in the declaration, because it is cheap: only a proxy holding the CSS selector is created. click() first waits for the element to become visible and usable, and shouldHave(exactText(...)) finds the node again and compares the text, retrying until the timeout.Що це. StaleElementReferenceException Selenium кидає, коли тест тримає посилання на DOM-вузол, якого вже немає: сторінку перезавантажили, React перемалював список, модальне вікно відкрили заново. У чистому Selenium це одна з головних причин нестабільних тестів.
What it is. Selenium throws StaleElementReferenceException when the test holds a reference to a DOM node that no longer exists: the page was reloaded, React re-rendered a list, a modal was reopened. In plain Selenium it is one of the main sources of flaky tests.
Як працює захист. SelenideElement зберігає не вузол, а критерій пошуку. Коли дія чи перевірка отримує stale-помилку, Selenide не падає одразу: він заново знаходить елемент за тим самим селектором і повторює операцію, поки не мине Configuration.timeout. Те саме стосується вкладених елементів $(".row").$(".price"): ланцюжок перебудовується від кореня.
How the protection works. A SelenideElement stores the search criteria, not the node. When an action or an assertion gets a stale error, Selenide does not fail immediately: it finds the element again with the same selector and repeats the operation until Configuration.timeout runs out. The same goes for nested elements such as $(".row").$(".price"): the chain is rebuilt from the root.
Приклад. Таблиця замовлень оновлюється кожні дві секунди через polling. Тест пише $("#orders tr", 0).$(".status").shouldHave(text("Paid")), і навіть якщо рядок замінять між пошуком і читанням тексту, перевірка просто повториться на свіжому вузлі.
Example. An orders table refreshes every two seconds via polling. The test writes $("#orders tr", 0).$(".status").shouldHave(text("Paid")), and even if the row is replaced between the lookup and reading its text, the check simply repeats against the fresh node.
Коли захист не діє. Якщо ти сам дістав сирий WebElement через toWebElement(), getWrappedElement() чи з executeJavaScript і зберіг у змінну, повторного пошуку не буде. Те саме з cached() і з об’єктами, отриманими зі стріму колекції, якщо ними користуються вже після зміни DOM. Ще один випадок — селектор, що після перемальовування знаходить інший елемент: stale не буде, але тест перевірить не те.
When it does not help. If you grabbed a raw WebElement through toWebElement(), getWrappedElement() or executeJavaScript and kept it in a variable, there is no re-lookup. The same applies to cached() and to objects taken from a collection stream if they are used after the DOM changed. Another case is a selector that finds a different element after the re-render: there is no stale error, but the test checks the wrong thing.
Уточнення. Правильний висновок для співбесіди: stale-помилка в Selenide-проєкті майже завжди вказує на код, який обійшов лінивий пошук.
Follow-up. The right conclusion for an interview: a stale error in a Selenide project almost always points to code that bypassed lazy lookup.
// безпечно: кожна перевірка шукає рядок і статус заново
$$("#orders tr").findBy(text("#1042")).$(".status").shouldHave(text("Paid"));
// небезпечно: сирий WebElement не шукається повторно
WebElement status = $$("#orders tr").findBy(text("#1042")).$(".status").toWebElement();
$("#refresh").click();
status.getText(); // тут може вилетіти StaleElementReferenceException
// safe: every check looks up the row and the status again
$$("#orders tr").findBy(text("#1042")).$(".status").shouldHave(text("Paid"));
// unsafe: a raw WebElement is never looked up again
WebElement status = $$("#orders tr").findBy(text("#1042")).$(".status").toWebElement();
$("#refresh").click();
status.getText(); // StaleElementReferenceException can be thrown here
findBy(text(...)) повертає перший рядок колекції з потрібним текстом, а .$(".status") шукає всередині нього. Увесь ланцюжок лишається лінивим, поки ти не викличеш toWebElement(): цей метод одразу знаходить вузол і віддає звичайний Selenium-об’єкт без очікувань і повторів.findBy(text(...)) returns the first row of the collection with the given text, and .$(".status") searches inside it. The whole chain stays lazy until you call toWebElement(): that method finds the node right away and hands out an ordinary Selenium object with no waiting and no retries.Як це влаштовано. Браузер стартує ліниво, при першому open() у потоці, і далі перевикористовується всіма тестами цього потоку. Selenide тримає окремий драйвер на кожен потік і закриває всі браузери сам, через shutdown hook JVM наприкінці прогону. Якщо браузер завис або його закрили, при reopenBrowserOnFail=true (значення за замовчуванням) наступний open() підніме новий.
How it works. The browser starts lazily on the first open() in a thread and is then reused by every test in that thread. Selenide keeps a separate driver per thread and closes all browsers itself through a JVM shutdown hook at the end of the run. If the browser hung or was closed, with reopenBrowserOnFail=true (the default) the next open() starts a new one.
closeWebDriver(). Робить quit() драйвера поточного потоку, закриваючи всі його вікна. Його кличуть, коли потрібна чиста сесія: у @AfterEach, щоб кожен тест мав новий браузер, або в @AfterAll, щоб звільнити слот на Grid одразу після класу, а не наприкінці всього прогону. Для першого варіанта є готове розширення BrowserPerTestStrategyExtension.
closeWebDriver(). It calls quit() on the current thread's driver, closing all its windows. You call it when you need a clean session: in @AfterEach so that every test gets a new browser, or in @AfterAll to free a Grid slot right after the class rather than at the end of the whole run. For the first option there is a ready extension, BrowserPerTestStrategyExtension.
Коли що обрати. Спільний браузер на клас або прогін швидший, але тести ділять cookies, localStorage і відкриті вкладки. Тоді між тестами чистять стан, наприклад через Selenide.cookies().clear() у свіжих 7.x. Браузер на кожен тест повільніший, зате ізоляція повна.
What to choose. A browser shared per class or per run is faster, but tests share cookies, localStorage and open tabs. In that case you clean state between tests, for example with Selenide.cookies().clear() in recent 7.x. A browser per test is slower but gives full isolation.
holdBrowserOpen. Прапорець лишав браузер відкритим після тестів для налагодження. З версії 7.3.1 він deprecated: браузер лишався, а процес драйвера працював вічно. Автори радять замість нього тимчасово поставити sleep(600_000) у тест, який розбираєш, або брейкпоінт у дебагері.
holdBrowserOpen. This flag kept the browser open after the tests for debugging. Since 7.3.1 it is deprecated: the browser stayed open and the driver process ran forever. The authors suggest temporarily putting sleep(600_000) into the test you are investigating, or using a debugger breakpoint.
// 1. Спільний браузер на весь прогін: нічого не пишемо, Selenide закриє його сам
// 2. Новий браузер для кожного тесту
@ExtendWith(BrowserPerTestStrategyExtension.class)
class CheckoutTest { /* ... */ }
// 3. Звільнити слот Grid після класу і почистити стан між тестами
class ProfileTest {
@AfterEach
void cleanState() {
Selenide.cookies().clear();
Selenide.localStorage().clear();
}
@AfterAll
static void releaseBrowser() {
closeWebDriver();
}
}
// 1. One browser for the whole run: write nothing, Selenide closes it itself
// 2. A new browser for every test
@ExtendWith(BrowserPerTestStrategyExtension.class)
class CheckoutTest { /* ... */ }
// 3. Free the Grid slot after the class and clean state between tests
class ProfileTest {
@AfterEach
void cleanState() {
Selenide.cookies().clear();
Selenide.localStorage().clear();
}
@AfterAll
static void releaseBrowser() {
closeWebDriver();
}
}
BrowserPerTestStrategyExtension лежить у пакеті com.codeborne.selenide.junit5 і викликає closeWebDriver() після кожного тесту. Метод у @AfterAll має бути static, бо JUnit 5 викликає його на рівні класу. cookies() і localStorage() повертають обгортки над сховищами поточного браузера з методом clear().BrowserPerTestStrategyExtension lives in the com.codeborne.selenide.junit5 package and calls closeWebDriver() after every test. The @AfterAll method must be static because JUnit 5 calls it at class level. cookies() and localStorage() return wrappers over the current browser's storages with a clear() method.Що це. Клас WebDriverRunner тримає драйвер поточного потоку. WebDriverRunner.getWebDriver() повертає звичайний Selenium WebDriver, а Selenide.webdriver().object() робить те саме в стилі умов Selenide. Зворотний напрям — WebDriverRunner.setWebDriver(driver): він змушує Selenide працювати з драйвером, який ти створив сам.
What it is. The WebDriverRunner class holds the current thread's driver. WebDriverRunner.getWebDriver() returns an ordinary Selenium WebDriver, and Selenide.webdriver().object() does the same in the style of Selenide conditions. The opposite direction is WebDriverRunner.setWebDriver(driver): it makes Selenide work with a driver you created yourself.
Коли виправдано. Для речей, яких Selenide не обгортає: керування розміром і позицією вікна, Actions зі складними жестами, CDP чи BiDi через HasDevTools і HasBiDi, сторонні бібліотеки, що приймають WebDriver (наприклад, axe для доступності). Також коли тести мігрують із Selenium поступово і частина старих хелперів ще приймає драйвер параметром.
When it is justified. For things Selenide does not wrap: window size and position, Actions with complex gestures, CDP or BiDi via HasDevTools and HasBiDi, third-party libraries that take a WebDriver (for example axe for accessibility). Also when tests migrate from Selenium gradually and some old helpers still take the driver as a parameter.
setWebDriver. Метод корисний, коли драйвер створює чужий код. Але документація прямо попереджає: такий драйвер Selenide не закриває, quit() — твоя відповідальність. І викликати setWebDriver треба до першого open(), інакше Selenide встигне підняти власний браузер. Для більшості випадків краще WebDriverProvider: тоді Selenide сам керує життєвим циклом.
setWebDriver. It is useful when someone else's code creates the driver. But the documentation warns explicitly: Selenide does not close such a driver, and quit() is your responsibility. And setWebDriver must be called before the first open(), otherwise Selenide will already have started its own browser. For most cases a WebDriverProvider is better, because Selenide then manages the lifecycle.
Підводні камені. Усе, що ти робиш сирим драйвером, втрачає розумні очікування: driver.findElement не чекає і не захищає від stale. Не зберігай драйвер у статичне поле: у паралельному прогоні кожен потік має свій екземпляр, і статична змінна змішає сесії.
Pitfalls. Everything done through the raw driver loses smart waiting: driver.findElement neither waits nor protects against stale elements. Do not store the driver in a static field: in a parallel run every thread has its own instance, and a static variable mixes sessions up.
// дістати драйвер поточного потоку
WebDriver driver = WebDriverRunner.getWebDriver();
driver.manage().window().setSize(new Dimension(1440, 900));
if (driver instanceof HasDevTools cdp) {
DevTools devTools = cdp.getDevTools();
devTools.createSessionIfThereIsNotOne();
// далі команди CDP
}
// підсунути власний драйвер: робити до першого open() і закривати самому
ChromeDriver myDriver = new ChromeDriver(new ChromeOptions().addArguments("--lang=uk"));
WebDriverRunner.setWebDriver(myDriver);
open("/");
// ...
myDriver.quit();
// get the current thread's driver
WebDriver driver = WebDriverRunner.getWebDriver();
driver.manage().window().setSize(new Dimension(1440, 900));
if (driver instanceof HasDevTools cdp) {
DevTools devTools = cdp.getDevTools();
devTools.createSessionIfThereIsNotOne();
// CDP commands go here
}
// hand in your own driver: do it before the first open() and close it yourself
ChromeDriver myDriver = new ChromeDriver(new ChromeOptions().addArguments("--lang=uk"));
WebDriverRunner.setWebDriver(myDriver);
open("/");
// ...
myDriver.quit();
instanceof HasDevTools cdp — pattern matching з Java 17: перевірка типу й оголошення змінної в одному виразі. createSessionIfThereIsNotOne() відкриває CDP-сесію для поточної вкладки. Після setWebDriver усі $ і open працюють із переданим драйвером, але закрити його треба явним quit().instanceof HasDevTools cdp is Java 17 pattern matching: a type check and a variable declaration in one expression. createSessionIfThereIsNotOne() opens a CDP session for the current tab. After setWebDriver, every $ and open uses the given driver, but you must close it with an explicit quit().Що це. Це три статичні методи класу Selenide, з яких починається майже кожен рядок тесту. $(".price") повертає один SelenideElement — перший вузол, що відповідає CSS-селектору. $$(".price") повертає ElementsCollection — усі відповідні вузли. $x("//h1") і $$x(...) роблять те саме, але приймають XPath замість CSS.
What it is. These are three static methods of the Selenide class, and almost every test line starts with one of them. $(".price") returns one SelenideElement, the first node matching the CSS selector. $$(".price") returns an ElementsCollection with all matching nodes. $x("//h1") and $$x(...) do the same but take XPath instead of CSS.
Варіанти виклику. Крім рядка, $ і $$ приймають будь-який Selenium By, наприклад $(By.name("q")) або $(byText("Увійти")). Є і форма з індексом: $(".item", 2) дає третій елемент, бо рахунок іде з нуля. Це коротший запис для $$(".item").get(2).
Call variants. Besides a string, $ and $$ accept any Selenium By, for example $(By.name("q")) or $(byText("Sign in")). There is also a form with an index: $(".item", 2) gives the third element because counting starts at zero. It is a shorter way to write $$(".item").get(2).
Коли що. CSS читається легше і працює швидше, тому він за замовчуванням. XPath беруть, коли треба піднятися до предка чи зачепитися за текст, хоча для цього в Selenide є зручніші інструменти: closest(), byText(), withText().
When to use what. CSS reads more easily and runs faster, so it is the default. XPath is used when you need to go up to an ancestor or hook onto text, although Selenide has handier tools for that: closest(), byText(), withText().
Підводні камені. Якщо селектору відповідає кілька вузлів, $ мовчки візьме перший, і тест може клікнути не ту кнопку. Порожня колекція від $$ — не помилка: вона впаде лише на перевірці або при зверненні до елемента. І жоден із методів не шукає нічого одразу: пошук стається лише на дії чи перевірці.
Pitfalls. If several nodes match, $ silently takes the first one, and the test may click the wrong button. An empty collection from $$ is not an error: it fails only on an assertion or when you access an element. And none of these methods searches right away: the lookup happens only on an action or an assertion.
SelenideElement title = $("h1.product-title");
ElementsCollection prices = $$(".cart-row .price");
SelenideElement thirdRow = $(".cart-row", 2);
SelenideElement heading = $x("//section[@id='reviews']//h2");
title.shouldHave(text("Coffee grinder"));
prices.shouldHave(size(3));
thirdRow.$(".remove").click();
heading.shouldBe(visible);
$ і $x повертають SelenideElement, $$ і $$x — ElementsCollection. Другий аргумент у $(".cart-row", 2) — індекс з нуля серед усіх збігів. Умова size для колекцій імпортується з CollectionCondition, а text і visible — з Condition.$ and $x return a SelenideElement, while $$ and $$x return an ElementsCollection. The second argument in $(".cart-row", 2) is a zero-based index among all matches. The size condition for collections is imported from CollectionCondition, while text and visible come from Condition.Що це. Клас Selectors містить фабрики, що повертають звичайний Selenium By, тому їх передають у $, $$ чи find. Найуживаніші — текстові: byText("Оплатити") шукає елемент із повним збігом тексту, а withText("Оплат") — з підрядком. Обидва ігнорують зайві пробіли й переноси, але чутливі до регістру; для нечутливого пошуку є byTextCaseInsensitive і withTextCaseInsensitive.
What it is. The Selectors class holds factories that return an ordinary Selenium By, so you pass them to $, $$ or find. The most common are text-based: byText("Pay now") finds an element whose whole text matches, while withText("Pay") matches a substring. Both ignore extra spaces and line breaks but are case-sensitive; for case-insensitive search there are byTextCaseInsensitive and withTextCaseInsensitive.
Посилання й атрибути. Для посилань працюють і By.linkText("Доставка"), і By.partialLinkText("Достав"), і їхні аналоги byLinkText та byPartialLinkText. Для атрибутів є byAttribute(name, value), byName, byTitle, byId. З версії 7.12 додалися byTestId (за замовчуванням атрибут data-test-id, назву можна змінити властивістю selenide.test-id.attribute), byLabel для поля за текстом його мітки та byAltText, а в 7.17 — byRole.
Links and attributes. For links you can use By.linkText("Delivery"), By.partialLinkText("Deliv") or their counterparts byLinkText and byPartialLinkText. For attributes there are byAttribute(name, value), byName, byTitle and byId. Version 7.12 added byTestId (by default the data-test-id attribute, renamable via the selenide.test-id.attribute property), byLabel for a field found by its label text, and byAltText; 7.17 added byRole.
Коли що. Найстабільніший варіант — окремий тестовий атрибут, про який домовилися з розробниками. Текстові селектори зручні для кнопок і пунктів меню, але ламаються при зміні копірайтингу чи мови інтерфейсу.
When to use what. The most stable option is a dedicated test attribute agreed with the developers. Text selectors are handy for buttons and menu items but break when the copy or the UI language changes.
Підводні камені. withText("Так") знайде і «Так», і «Також», тож короткий підрядок підступний. А текстовий селектор повертає найглибший вузол із цим текстом, тому іноді це span усередині кнопки, а не сама кнопка.
Pitfalls. withText("Yes") will also match "Yesterday", so a short substring is treacherous. And a text selector returns the deepest node containing that text, so sometimes you get a span inside the button rather than the button itself.
import static com.codeborne.selenide.Selectors.*;
$(byText("Checkout")).click();
$(withText("items in your cart")).shouldBe(visible);
$(By.linkText("Delivery terms")).click();
$(byPartialLinkText("Returns")).shouldBe(visible);
$(byTestId("promo-code")).setValue("AUTUMN10");
$(byLabel("Email")).setValue("anna@example.com");
$(byAttribute("aria-label", "Close dialog")).click();
Selectors.* робить фабрики доступними без префікса. Кожна з них повертає By, тож результат можна передати і в Selenide, і в сирий driver.findElement. byTestId("promo-code") еквівалентний byAttribute("data-test-id", "promo-code"), якщо назву атрибута не перевизначено.Selectors.* makes the factories available without a prefix. Each one returns a By, so the result can go into Selenide or into a raw driver.findElement. byTestId("promo-code") is equivalent to byAttribute("data-test-id", "promo-code") unless the attribute name was overridden.Що це. У SelenideElement є ті самі методи пошуку, що й у класу Selenide, але вони шукають лише серед нащадків. $(".cart").$(".total") і $(".cart").find(".total") — одне й те саме: перший нащадок із класом total. Для всіх нащадків є $$(".row") або синонім findAll(".row"), а для XPath усередині — $x і $$x.
What it is. A SelenideElement has the same lookup methods as the Selenide class, but they search only among descendants. $(".cart").$(".total") and $(".cart").find(".total") are the same thing: the first descendant with the total class. For all descendants there is $$(".row") or its synonym findAll(".row"), and for XPath inside an element there are $x and $$x.
Навіщо ланцюжок. Можна написати один CSS .cart .total, але ланцюжок має переваги. Він відображає структуру сторінки: спершу блок, потім деталь у ньому. У повідомленні про падіння видно, яка саме ланка не знайшлася. А корінь легко винести в поле компонента і перевикористати для кількох дочірніх елементів.
Why chain. You could write a single CSS selector .cart .total, but chaining has advantages. It mirrors the page structure: first the block, then the detail inside it. The failure message shows exactly which link was not found. And the root is easy to move into a component field and reuse for several child elements.
Приклад. На сторінці два однакові блоки адрес — доставки й оплати. $("#shipping").$("[name=city]") однозначно вибирає поле міста в першому, а без кореня $("[name=city]") завжди потрапляв би в той блок, що стоїть вище.
Example. A page has two identical address blocks, shipping and billing. $("#shipping").$("[name=city]") unambiguously picks the city field in the first one, while without a root $("[name=city]") would always land in whichever block comes first.
Підводні камені. XPath усередині ланцюжка має починатися з крапки: $(".cart").$x(".//button"). Без неї вираз //button шукатиме по всьому документу. І пам’ятай, що кожна ланка шукається заново при кожній дії, тож довгі ланцюжки з десятка кроків роблять тест повільнішим і важчим для читання.
Pitfalls. XPath inside a chain must start with a dot: $(".cart").$x(".//button"). Without it, //button searches the whole document. And remember that every link is looked up again on every action, so chains of ten steps make a test slower and harder to read.
SelenideElement shipping = $("#shipping-address");
SelenideElement billing = $("#billing-address");
shipping.$("[name=city]").setValue("Lviv");
billing.find("[name=city]").setValue("Kyiv");
ElementsCollection rows = $("#cart").$$(".row"); // те саме, що findAll(".row")
rows.shouldHave(size(2));
$("#cart").$x(".//button[text()='Checkout']").click(); // крапка: пошук лише всередині #cart
SelenideElement shipping = $("#shipping-address");
SelenideElement billing = $("#billing-address");
shipping.$("[name=city]").setValue("Lviv");
billing.find("[name=city]").setValue("Kyiv");
ElementsCollection rows = $("#cart").$$(".row"); // same as findAll(".row")
rows.shouldHave(size(2));
$("#cart").$x(".//button[text()='Checkout']").click(); // the dot keeps the search inside #cart
shipping.$(...) і billing.find(...) — синоніми, різниця лише в стилі. $("#cart").$$(".row") повертає колекцію нащадків кореня. У XPath .// означає «будь-який нащадок поточного вузла», а // без крапки — «будь-який вузол документа».shipping.$(...) and billing.find(...) are synonyms, the difference is only style. $("#cart").$$(".row") returns a collection of the root's descendants. In XPath .// means "any descendant of the current node", while // without the dot means "any node in the document".Що це. Selenide має методи відносної навігації, які теж лишаються лінивими. parent() повертає безпосереднього батька. closest(".card") і його синонім ancestor(".card") піднімаються вгору до найближчого предка, що відповідає селектору. ancestor(".card", 1) бере другого такого предка вище, бо індекс рахується з нуля. sibling(0) — перший наступний сусід на тому ж рівні, preceding(0) — перший попередній, lastChild() — останній дочірній елемент.
What it is. Selenide has relative navigation methods that also stay lazy. parent() returns the direct parent. closest(".card") and its synonym ancestor(".card") climb up to the nearest ancestor matching the selector. ancestor(".card", 1) takes the second such ancestor higher up, since the index is zero-based. sibling(0) is the first following sibling on the same level, preceding(0) is the first preceding one and lastChild() is the last child element.
Коли потрібно. Типовий випадок — якір є лише в одному місці. У таблиці користувачів унікальний текст має клітинка з email, а кнопка «Видалити» в тому самому рядку однакова для всіх. Знаходимо клітинку за текстом, піднімаємося до tr через closest і вже в ньому шукаємо кнопку. Так само sibling допомагає дістати значення поруч із підписом у списку «назва — значення».
When you need it. The typical case is when the anchor exists in only one place. In a users table the email cell has unique text, while the Delete button in the same row is identical for everyone. You find the cell by text, climb to the tr with closest and look for the button inside it. In the same way sibling helps you reach the value next to a caption in a "name and value" list.
Обмеження селектора. closest і ancestor приймають простий селектор: тег, клас, атрибут або атрибут зі значенням, наприклад form, .active, [data-testid=row]. Складні комбінатори на кшталт div.card > .body туди не передають.
Selector limits. closest and ancestor take a simple selector: a tag, a class, an attribute, or an attribute with a value, such as form, .active or [data-testid=row]. Complex combinators like div.card > .body do not belong there.
Підводні камені. parent().parent().parent() прив’язує тест до точної глибини вкладеності, і будь-яка обгортка від дизайнера його ламає; closest із класом чи атрибутом стійкіший. sibling і preceding рахують лише елементи, а не текстові вузли, і тільки на тому ж рівні. Якщо тобі постійно потрібна відносна навігація на кілька рівнів, це сигнал попросити в розробників тестовий атрибут на потрібному контейнері.
Pitfalls. parent().parent().parent() ties the test to the exact nesting depth, and any wrapper a designer adds breaks it; closest with a class or attribute is sturdier. sibling and preceding count only elements, not text nodes, and only on the same level. If you keep needing relative navigation across several levels, it is a signal to ask developers for a test attribute on the container you need.
// рядок таблиці, де є потрібний email
SelenideElement emailCell = $(byText("anna@example.com"));
emailCell.closest("tr").$("button.delete").click();
// <dt>Статус</dt><dd>Оплачено</dd>
$(byText("Status")).sibling(0).shouldHave(exactText("Paid"));
// підпис перед полем і останній пункт списку
$("#zip").preceding(0).shouldHave(text("Postal code"));
$("ul.breadcrumbs").lastChild().shouldHave(text("Checkout"));
// другий предок з атрибутом data-testid вище клітинки
emailCell.ancestor("[data-testid]", 1).shouldBe(visible);
// the table row that contains the email we need
SelenideElement emailCell = $(byText("anna@example.com"));
emailCell.closest("tr").$("button.delete").click();
// <dt>Status</dt><dd>Paid</dd>
$(byText("Status")).sibling(0).shouldHave(exactText("Paid"));
// the caption before a field and the last item of a list
$("#zip").preceding(0).shouldHave(text("Postal code"));
$("ul.breadcrumbs").lastChild().shouldHave(text("Checkout"));
// the second ancestor with a data-testid attribute above the cell
emailCell.ancestor("[data-testid]", 1).shouldBe(visible);
SelenideElement, тому його можна продовжувати ланцюжком: closest("tr").$("button.delete"). Індекси в sibling, preceding і ancestor починаються з нуля: 0 — найближчий елемент у потрібному напрямку, 1 — наступний за ним.SelenideElement, so you can keep chaining: closest("tr").$("button.delete"). Indexes in sibling, preceding and ancestor start at zero: 0 is the nearest element in that direction, 1 is the one after it.Що це. Shadow DOM — це ізольоване піддерево веб-компонента. Звичайний CSS-селектор із документа крізь його межу не проходить, тому $("my-input input") нічого не знайде, хоча в DevTools поле видно. Selenide дає два селектори в класі Selectors.
What it is. Shadow DOM is the isolated subtree of a web component. An ordinary CSS selector from the document does not cross its boundary, so $("my-input input") finds nothing even though DevTools shows the field. Selenide offers two selectors in the Selectors class.
shadowCss. shadowCss(target, host, innerHosts...) описує шлях явно: спершу CSS цільового елемента, потім ланцюжок хостів від зовнішнього до внутрішнього. Наприклад, shadowCss("input", "#payment-form", "card-number") означає: знайди хост #payment-form, у його shadow root знайди хост card-number, а вже в ньому — input. Такий селектор можна викликати й від іншого елемента через find.
shadowCss. shadowCss(target, host, innerHosts...) spells the path out: first the CSS of the target element, then the chain of hosts from outer to inner. For example, shadowCss("input", "#payment-form", "card-number") means: find the #payment-form host, inside its shadow root find the card-number host, and inside that one find input. You can also call such a selector from another element via find.
shadowDeepCss. shadowDeepCss("input[name=cvc]") проходить усі shadow root сторінки без знання шляху. Він зручніший, коли вкладеність глибока або часто змінюється, але повільніший і може знайти однойменний елемент в іншому компоненті.
shadowDeepCss. shadowDeepCss("input[name=cvc]") walks through every shadow root on the page without knowing the path. It is handier when nesting is deep or changes often, but it is slower and can match a same-named element in another component.
Підводні камені. Обидва способи працюють лише з відкритими shadow root, тобто створеними з mode: open. До закритого кореня немає доступу ні зі скрипта, ні з WebDriver, і тоді лишається домовлятися з розробниками про тестову збірку або тестувати компонент окремо. Ще одна пастка — iframe усередині компонента: спершу потрібне switchTo().frame(...). Для Page Object є анотація @ShadowRoot у інтерфейсі Container, але для кількох полів простіше описати селектори явно.
Pitfalls. Both approaches only work with open shadow roots, created with mode: open. A closed root is inaccessible to scripts and to WebDriver alike, and then you need to agree on a test build with developers or test the component separately. Another trap is an iframe inside a component: you need switchTo().frame(...) first. For Page Objects there is a @ShadowRoot annotation in the Container interface, but for a few fields it is simpler to describe the selectors explicitly.
import static com.codeborne.selenide.Selectors.shadowCss;
import static com.codeborne.selenide.Selectors.shadowDeepCss;
$(shadowCss("input", "#payment-form", "card-number")).setValue("4242424242424242");
$(shadowCss("button.pay", "#payment-form")).shouldBe(enabled).click();
$(shadowDeepCss("input[name=cvc]")).setValue("123");
$("#payment-form")
.find(shadowCss(".error", "card-number"))
.shouldHave(text("Invalid card number"));
shadowCss — CSS шуканого елемента, решта — CSS хостів у порядку вкладеності. shadowDeepCss приймає лише цільовий CSS. Обидва повертають By, тому результат іде в $, $$ або find; в останньому випадку перший хост шукається всередині елемента, від якого викликано find.shadowCss is the CSS of the target element, the rest are host selectors in nesting order. shadowDeepCss takes only the target CSS. Both return a By, so the result goes into $, $$ or find; in the last case the first host is searched inside the element find was called on.Що це. as("Кнопка оплати") задає елементу людську назву — alias. Метод повертає той самий елемент, тож його дописують прямо в оголошення: $("#btn-7f3a").as("Pay button"). Alias можна прочитати через getAlias(), а для колекцій є такий самий $$(".row").as("Cart rows").
What it is. as("Pay button") gives an element a human name, an alias. The method returns the same element, so you add it right in the declaration: $("#btn-7f3a").as("Pay button"). The alias can be read with getAlias(), and collections have the same $$(".row").as("Cart rows").
Що змінюється. У повідомленні про падіння, у текстовому звіті Selenide і в кроках Allure замість сирого селектора з’являється назва. Замість «Element not found {#btn-7f3a}» у звіті видно «Element not found {Pay button}», і менеджер чи розробник розуміє крок без відкриття коду. Це особливо корисно, коли селектори згенеровані фреймворком і нічого не кажуть людині.
What changes. In the failure message, in the Selenide text report and in Allure steps the name appears instead of the raw selector. Instead of "Element not found {#btn-7f3a}" the report says "Element not found {Pay button}", and a manager or developer understands the step without opening the code. It is especially useful when selectors are generated by a framework and mean nothing to a human.
Коли варто. Для ключових елементів Page Object з незрозумілими селекторами: згенеровані id, довгі XPath, nth-child. Якщо селектор і так читається, наприклад [data-test-id=pay], alias мало що додає.
When it is worth it. For key Page Object elements with unclear selectors: generated ids, long XPath, nth-child. If the selector already reads well, such as [data-test-id=pay], an alias adds little.
Підводні камені. Автори Selenide самі застерігають у документації: здебільшого цей метод не потрібен, бо реальний селектор кращий за «гарний» опис, який може застаріти й навіть вводити в оману. Якщо розробник змінив кнопку, а alias лишився старим, звіт показуватиме неправду. Тому я даю назви економно і стежу, щоб вони описували роль елемента, а не його текст на екрані.
Pitfalls. The Selenide authors warn in the documentation that you usually do not need this method, because the real selector beats a "nice" description that can go stale and even mislead. If a developer changed the button but the alias stayed, the report will tell a lie. So I name elements sparingly and make sure the name describes the element's role, not its on-screen text.
public class CheckoutPage {
private final SelenideElement payButton = $("#btn-7f3a").as("Pay button");
private final ElementsCollection rows = $$("div[class*=row_]").as("Cart rows");
public void pay() {
rows.shouldHave(sizeGreaterThan(0));
payButton.shouldBe(enabled).click();
}
}
// Помилка у звіті:
// Element should be enabled {Pay button}
// Element: '<button id="btn-7f3a" disabled>Pay</button>'
// Screenshot: file:/build/reports/tests/1727600000.0.png
public class CheckoutPage {
private final SelenideElement payButton = $("#btn-7f3a").as("Pay button");
private final ElementsCollection rows = $$("div[class*=row_]").as("Cart rows");
public void pay() {
rows.shouldHave(sizeGreaterThan(0));
payButton.shouldBe(enabled).click();
}
}
// The error in the report:
// Element should be enabled {Pay button}
// Element: '<button id="btn-7f3a" disabled>Pay</button>'
// Screenshot: file:/build/reports/tests/1727600000.0.png
as(...) повертає той самий елемент, тож виклик вбудовується в ініціалізацію поля. У фігурних дужках повідомлення про помилку Selenide показує alias, а якщо його немає — селектор. sizeGreaterThan(0) — умова колекції з CollectionCondition. Формат повідомлення в коментарі наведено для ілюстрації, точні рядки залежать від версії.as(...) returns the same element, so the call fits into the field initialiser. In the curly braces of an error message Selenide shows the alias, or the selector if there is no alias. sizeGreaterThan(0) is a collection condition from CollectionCondition. The message format in the comment is illustrative; exact lines depend on the version.Що це. Це методи перевірки на SelenideElement. should, shouldBe і shouldHave роблять одне й те саме: приймають одну чи кілька умов і чекають, доки елемент їм відповідатиме. shouldNot, shouldNotBe і shouldNotHave чекають на протилежне. Різниця між назвами суто граматична: shouldBe(visible) читається як «має бути видимим», shouldHave(text("Ok")) — як «має мати текст». Технічно shouldBe(text(...)) теж спрацює.
What it is. These are the assertion methods of SelenideElement. should, shouldBe and shouldHave do exactly the same thing: they take one or more conditions and wait until the element satisfies them. shouldNot, shouldNotBe and shouldNotHave wait for the opposite. The difference between the names is purely grammatical: shouldBe(visible) reads as "should be visible" and shouldHave(text("Ok")) as "should have text". Technically shouldBe(text(...)) works too.
Як чекають. Перевірка повторюється кожні pollingInterval мілісекунд (200 за замовчуванням), доки умова не справдиться або не мине Configuration.timeout. Якщо час вийшов, летить ElementNotFound (елемента немає в DOM) або ElementShould чи ElementShouldNot (елемент є, але не в тому стані). Усі вони спадкують UIAssertionError, тобто це AssertionError, і JUnit показує тест як failed, а не errored.
How they wait. The check is repeated every pollingInterval milliseconds (200 by default) until the condition holds or Configuration.timeout runs out. When time is up you get ElementNotFound (the element is not in the DOM) or ElementShould or ElementShouldNot (the element exists but is in the wrong state). All of them extend UIAssertionError, which is an AssertionError, so JUnit reports the test as failed rather than errored.
Приклад. $("#submit").shouldBe(visible, enabled) перевіряє дві умови по черзі. Метод повертає той самий елемент, тож можна одразу продовжити .click().
Example. $("#submit").shouldBe(visible, enabled) checks two conditions in turn. The method returns the same element, so you can go straight on with .click().
Підводні камені. Методи is(condition), has(condition) і exists() повертають boolean і не чекають зовсім: це знімок стану в конкретну мілісекунду. Тому assertTrue($(".toast").is(visible)) — класичне джерело flaky-тестів. Для гілок логіки є варіант is(condition, Duration) з таймаутом, але автори Selenide прямо радять не зловживати ним у тестах.
Pitfalls. The methods is(condition), has(condition) and exists() return a boolean and do not wait at all: they are a snapshot of the state at one particular millisecond. That is why assertTrue($(".toast").is(visible)) is a classic source of flaky tests. For branching logic there is is(condition, Duration) with a timeout, but the Selenide authors explicitly advise against leaning on it in tests.
$("#submit").shouldBe(visible, enabled).click();
$(".greeting").shouldHave(text("Welcome"));
$(".greeting").should(text("Welcome")); // те саме, інша граматика
$(".spinner").shouldNotBe(visible);
$(".order-status").shouldNotHave(text("Failed"));
// погано: знімок стану без очікування
assertTrue($(".toast").is(visible));
// добре: очікування до таймауту
$(".toast").shouldBe(visible);
$("#submit").shouldBe(visible, enabled).click();
$(".greeting").shouldHave(text("Welcome"));
$(".greeting").should(text("Welcome")); // the same, different grammar
$(".spinner").shouldNotBe(visible);
$(".order-status").shouldNotHave(text("Failed"));
// bad: a snapshot of the state without waiting
assertTrue($(".toast").is(visible));
// good: waits up to the timeout
$(".toast").shouldBe(visible);
should* приймає varargs умов і повертає SelenideElement, тому після перевірки можна продовжити ланцюжок дією. Умови visible, enabled і text — статичні члени Condition. is(visible) повертає boolean, і assertTrue з JUnit перевіряє його один раз.should* takes varargs conditions and returns a SelenideElement, so after the check you can continue the chain with an action. The conditions visible, enabled and text are static members of Condition. is(visible) returns a boolean, and JUnit's assertTrue checks it exactly once.Що це. exist перевіряє лише те, що елемент є в DOM, навіть якщо він прихований через display: none. visible вимагає, щоб елемент був у DOM і браузер вважав його відображеним, тобто isDisplayed() повертав true. hidden — протилежність visible: він задоволений, коли елемент невидимий або його в DOM немає зовсім. Є ще синоніми для читабельності: appear означає те саме, що visible, а disappear — те саме, що hidden.
What it is. exist only checks that the element is in the DOM, even if it is hidden with display: none. visible requires the element to be in the DOM and the browser to consider it displayed, meaning isDisplayed() returns true. hidden is the opposite of visible: it is satisfied when the element is invisible or not in the DOM at all. There are also synonyms for readability: appear means the same as visible and disappear the same as hidden.
Коли що. visible — вибір за замовчуванням для всього, з чим взаємодіє користувач. exist потрібен для службових вузлів: прихований input з токеном, meta-тег, елемент, що з’явиться пізніше анімацією. hidden беруть, щоб дочекатися, поки зникне спінер чи закриється модальне вікно.
When to use what. visible is the default choice for anything a user interacts with. exist is for service nodes: a hidden input with a token, a meta tag, an element that an animation will reveal later. hidden is used to wait until a spinner goes away or a modal closes.
Приклад. Після сабміту форми тест пише $(".spinner").shouldBe(hidden), а потім $(".success").shouldBe(visible). Перевірка на exist тут нічого б не довела: повідомлення могло б бути в DOM, але схованим.
Example. After a form is submitted, the test writes $(".spinner").shouldBe(hidden) and then $(".success").shouldBe(visible). Checking exist here would prove nothing: the message could be in the DOM but hidden.
Підводні камені. Елемент із opacity: 0 або з нульовим розміром WebDriver зазвичай вважає невидимим, а елемент, перекритий іншим шаром, — видимим, хоча клікнути його не вийде. Тож visible не гарантує, що користувач бачить елемент очима. І hidden не доводить, що елемента немає в DOM: для цього потрібна інша перевірка.
Pitfalls. WebDriver usually treats an element with opacity: 0 or zero size as invisible, while an element covered by another layer counts as visible even though you cannot click it. So visible does not guarantee the user actually sees the element. And hidden does not prove the element is gone from the DOM; that needs a different check.
$("#order-form button[type=submit]").click();
$(".spinner").shouldBe(hidden); // зник або ніколи не з'являвся
$(".success").shouldBe(visible); // є в DOM і відображений
$("input[name=csrf]").should(exist); // прихований, але присутній
$(".toast").should(appear); // синонім visible
$(".toast").should(disappear); // синонім hidden
$("#order-form button[type=submit]").click();
$(".spinner").shouldBe(hidden); // gone, or never appeared
$(".success").shouldBe(visible); // in the DOM and displayed
$("input[name=csrf]").should(exist); // hidden but present
$(".toast").should(appear); // synonym for visible
$(".toast").should(disappear); // synonym for hidden
Condition, а не методи, тому пишуться без дужок. appear і disappear створені через be(visible) і be(hidden), тобто це ті самі перевірки з іншою назвою в звіті.Condition class, not methods, so they are written without parentheses. appear and disappear are built as be(visible) and be(hidden), so they are the same checks under a different name in the report.Два різні питання. «Користувач цього не бачить» і «цього вузла немає в розмітці» — різні твердження, і в Selenide для них різні перевірки. Плутанина між ними дає тести, що проходять, коли мали б падати.
Two different questions. "The user cannot see it" and "this node is not in the markup" are different statements, and Selenide has different checks for them. Mixing them up produces tests that pass when they should fail.
Невидимий або відсутній. Тут є два рівноцінні записи. Перший — shouldBe(hidden). Другий — shouldNotBe(visible), заперечення видимості. Обидва проходять і тоді, коли елемент лишився в DOM зі стилем display: none, і тоді, коли його видалили. Цього досить для спінерів, тултіпів і модальних вікон, де для користувача важливо лише те, що він бачить.
Invisible or missing. There are two equivalent ways to write this. The first is shouldBe(hidden). The second is shouldNotBe(visible), the negation of visibility. Each passes when the element stayed in the DOM with display: none and when it was removed. That is enough for spinners, tooltips and modals, where only what the user sees matters.
Відсутній у DOM. Потрібне заперечення exist: shouldNot(exist) або те саме з іншою граматикою shouldNotBe(exist). Така перевірка впаде, якщо вузол лишився прихованим. Вона потрібна, коли важлива саме відсутність: видалений рядок таблиці, пункт меню, недоступний для ролі, кнопка адміністратора у звичайного користувача, де прихований у DOM елемент уже є вразливістю.
Missing from the DOM. You need the negation of exist: shouldNot(exist) or the same with different grammar, shouldNotBe(exist). This check fails if the node stayed around hidden. You need it when absence itself matters: a deleted table row, a menu item unavailable to a role, an admin button for a regular user, where a hidden element in the DOM is already a vulnerability.
Підводні камені. shouldBe(hidden) не доводить відсутності вузла, тож для перевірок прав доступу він слабкий. Заперечення умов, що не стосуються присутності, поводяться інакше: shouldNotHave(text("Error")) на відсутньому елементі впаде з ElementNotFound, бо спершу треба знайти елемент, щоб прочитати текст. І shouldNot(exist) одразу після кліку може пройти занадто рано, якщо елемент ще не встиг з’явитися; спершу варто дочекатися події, яка мала його прибрати.
Pitfalls. shouldBe(hidden) does not prove the node is gone, so it is weak for access-control checks. Negations of conditions that are not about presence behave differently: shouldNotHave(text("Error")) on a missing element fails with ElementNotFound, because the element must be found before its text can be read. And shouldNot(exist) right after a click can pass too early if the element has not appeared yet; first wait for the event that was supposed to remove it.
// спінер: досить, що користувач його не бачить
$(".spinner").shouldBe(hidden);
$(".spinner").shouldNotBe(visible); // рівноцінний запис
// права доступу: вузла не має бути в розмітці
open("/dashboard");
$(".user-name").shouldHave(text("Regular User")); // спершу дочекалися сторінки
$("[data-test-id=admin-panel]").shouldNot(exist);
$("[data-test-id=admin-panel]").shouldNotBe(exist); // те саме
// видалений рядок
$$("#orders tr").findBy(text("#1042")).$(".delete").click();
$(byText("#1042")).shouldNot(exist);
// spinner: it is enough that the user cannot see it
$(".spinner").shouldBe(hidden);
$(".spinner").shouldNotBe(visible); // an equivalent form
// access rights: the node must not be in the markup
open("/dashboard");
$(".user-name").shouldHave(text("Regular User")); // wait for the page first
$("[data-test-id=admin-panel]").shouldNot(exist);
$("[data-test-id=admin-panel]").shouldNotBe(exist); // the same
// a deleted row
$$("#orders tr").findBy(text("#1042")).$(".delete").click();
$(byText("#1042")).shouldNot(exist);
shouldNot і shouldNotBe обгортають умову в заперечення. Для visible і exist заперечення вважає відсутній елемент успіхом, тому shouldNot(exist) проходить, щойно вузол зник. Перевірка .user-name перед нею — якір, який гарантує, що сторінка вже намальована і відсутність панелі не випадкова.shouldNot and shouldNotBe wrap a condition in a negation. For visible and exist the negation treats a missing element as success, so shouldNot(exist) passes as soon as the node is gone. The .user-name check before it is an anchor that ensures the page has rendered, so the missing panel is not an accident.text. text("Замовлення") перевіряє, що видимий текст елемента містить рядок як підрядок. Регістр не враховується, кілька пробілів і переносів між словами вважаються одним. Порожній рядок передати не можна: будь-який текст містить порожній підрядок, тому Selenide кидає IllegalArgumentException.
text. text("Order") checks that the element's visible text contains the string as a substring. Case is ignored, and several spaces or line breaks between words count as one. You cannot pass an empty string: any text contains an empty substring, so Selenide throws IllegalArgumentException.
exactText. exactText("Замовлення №42") вимагає повного збігу тексту, але теж без урахування регістру. Якщо регістр важливий, беруть exactTextCaseSensitive, а для підрядка з урахуванням регістру — textCaseSensitive. Є і явна назва partialText для підрядка.
exactText. exactText("Order #42") requires the whole text to match, but it also ignores case. If case matters, use exactTextCaseSensitive, and for a case-sensitive substring use textCaseSensitive. There is also an explicitly named partialText for substrings.
ownText і exactOwnText. Дивляться лише на власний текст вузла, без тексту дочірніх елементів. Це потрібно, коли в li лежить назва товару, а поруч вкладений span з ціною чи бейджем.
ownText and exactOwnText. They look only at the node's own text, without the text of child elements. You need this when an li holds a product name and a nested span with a price or a badge sits next to it.
matchText. matchText("Замовлення №\d+") перевіряє регулярний вираз. Зручно для номерів, дат і сум, що змінюються між прогонами.
matchText. matchText("Order #\d+") checks a regular expression. It is handy for numbers, dates and amounts that change between runs.
Підводні камені. text("1") пройде на «12» і на «100», тож для чисел і коротких статусів потрібен exactText. У Selenide 7 поведінку text можна перемкнути на повний збіг через Configuration.textCheck = TextCheck.FULL_TEXT, і автори планують зробити це стандартом у версії 8, тож старі тести з частковими збігами варто переглянути заздалегідь.
Pitfalls. text("1") passes on "12" and on "100", so numbers and short statuses need exactText. In Selenide 7 you can switch text to a full match with Configuration.textCheck = TextCheck.FULL_TEXT, and the authors plan to make that the default in version 8, so old tests relying on partial matches are worth reviewing in advance.
// <li class="item">Coffee grinder <span class="badge">NEW</span></li>
SelenideElement item = $(".item");
item.shouldHave(text("coffee")); // підрядок, регістр неважливий
item.shouldHave(exactText("coffee grinder new")); // повний текст, регістр неважливий
item.shouldHave(exactTextCaseSensitive("Coffee grinder NEW"));
item.shouldHave(exactOwnText("Coffee grinder")); // без тексту span
item.shouldHave(ownText("grinder"));
$("#order-number").shouldHave(matchText("Order #\\d{4,}"));
$("#cart-count").shouldHave(exactText("1")); // text("1") прийняв би й "12"
// <li class="item">Coffee grinder <span class="badge">NEW</span></li>
SelenideElement item = $(".item");
item.shouldHave(text("coffee")); // substring, case ignored
item.shouldHave(exactText("coffee grinder new")); // whole text, case ignored
item.shouldHave(exactTextCaseSensitive("Coffee grinder NEW"));
item.shouldHave(exactOwnText("Coffee grinder")); // without the span text
item.shouldHave(ownText("grinder"));
$("#order-number").shouldHave(matchText("Order #\\d{4,}"));
$("#cart-count").shouldHave(exactText("1")); // text("1") would accept "12" too
Condition, що приймають рядок. У Java-рядку зворотний слеш подвоюють, тому регулярний вираз \d{4,} (чотири й більше цифр) записано як "\\d{4,}". Варіанти з Own ігнорують текст вкладених тегів, варіанти з CaseSensitive враховують регістр.Condition that take a string. In a Java string the backslash is doubled, so the regular expression \d{4,} (four or more digits) is written as "\\d{4,}". The Own variants ignore the text of nested tags, and the CaseSensitive variants respect case.Що є. text() і його синонім getText() повертають видимий текст елемента. innerText() читає текст разом із прихованими частинами, getOwnText() — лише власний текст без дочірніх тегів. Значення поля дають val() або getValue(). Атрибут читають через attr("href") або стандартний getAttribute("href") — це одне й те саме. CSS-властивість повертає getCssValue("color"), успадкований від WebElement; окремого короткого getter для CSS у Selenide немає.
What there is. text() and its synonym getText() return the element's visible text. innerText() reads the text including hidden parts, and getOwnText() reads only the node's own text without child tags. A field's value comes from val() or getValue(). An attribute is read with attr("href") or the standard getAttribute("href"), which are the same thing. A CSS property comes from getCssValue("color"), inherited from WebElement; Selenide has no separate short getter for CSS.
Як вони чекають. Getter чекає лише на те, щоб елемент знайшовся в DOM, а потім повертає значення в ту мілісекунду, коли його прочитав. Якщо текст ще оновлюється після AJAX-відповіді, getter поверне старе значення, і тест упаде або, гірше, пройде на застарілих даних.
How they wait. A getter only waits for the element to be found in the DOM and then returns the value at the millisecond it read it. If the text is still updating after an AJAX response, the getter returns the old value, and the test fails or, worse, passes on stale data.
Чому краще умова. assertEquals("3", $("#count").text()) перевіряє один раз, а $("#count").shouldHave(exactText("3")) повторює перевірку до таймауту і в разі падіння показує фактичне значення, скріншот і HTML. Автори Selenide мають окрему сторінку в wiki з назвою «не використовуйте getter-и в тестах».
Why a condition is better. assertEquals("3", $("#count").text()) checks once, while $("#count").shouldHave(exactText("3")) repeats the check until the timeout and, on failure, shows the actual value, a screenshot and the HTML. The Selenide authors even have a wiki page called "do not use getters in tests".
Коли getter доречний. Коли значення треба запам’ятати й використати далі: номер створеного замовлення, щоб знайти його в списку; ціну до знижки, щоб порахувати очікувану після неї. У цьому разі спершу дочекайся стабільного стану умовою, а потім читай.
When a getter makes sense. When a value has to be remembered and used later: the number of a created order, to find it in a list; a price before a discount, to compute the expected one after it. In that case, wait for a stable state with a condition first and read afterwards.
// погано: одна спроба, без очікування потрібного значення
assertEquals("3", $("#cart-count").text());
// добре: очікування і зрозуміле повідомлення про помилку
$("#cart-count").shouldHave(exactText("3"));
// getter доречний, коли значення потрібне далі
$("#order-number").shouldHave(matchText("\\d+")); // спершу дочекалися
String orderNumber = $("#order-number").text();
String link = $("a.invoice").attr("href");
String email = $("#email").val();
String color = $(".status").getCssValue("color");
open("/orders");
$$("#orders tr").findBy(text(orderNumber)).shouldBe(visible);
// bad: one attempt, no waiting for the expected value
assertEquals("3", $("#cart-count").text());
// good: waiting and a clear failure message
$("#cart-count").shouldHave(exactText("3"));
// a getter makes sense when the value is needed later
$("#order-number").shouldHave(matchText("\\d+")); // wait first
String orderNumber = $("#order-number").text();
String link = $("a.invoice").attr("href");
String email = $("#email").val();
String color = $(".status").getCssValue("color");
open("/orders");
$$("#orders tr").findBy(text(orderNumber)).shouldBe(visible);
text(), attr(), val() і getCssValue() повертають String. getCssValue повертає обчислене значення, тому колір приходить у форматі на кшталт rgba(34, 139, 34, 1), а не як у CSS-файлі. Рядок із matchText перед getter-ом гарантує, що номер уже з’явився, а не порожній.text(), attr(), val() and getCssValue() return a String. getCssValue returns the computed value, so a colour arrives in a format like rgba(34, 139, 34, 1) rather than as written in the CSS file. The matchText line before the getter guarantees the number has appeared and is not empty.Загальний атрибут. attribute("disabled") перевіряє, що атрибут є з будь-яким значенням, attribute("data-state", "open") — що значення точно збігається, attributeMatching("data-id", "\d+") — що значення відповідає регулярному виразу. Так само перевіряють атрибут-підказку в порожньому полі: окремої умови для нього в класі Condition немає, тому пишуть загальну перевірку атрибута з його назвою (див. код). Для пошуку поля за цією підказкою з 7.12 є окремий селектор у Selectors.
A general attribute. attribute("disabled") checks that the attribute is present with any value, attribute("data-state", "open") checks for an exact value, and attributeMatching("data-id", "\d+") checks the value against a regular expression. The hint attribute shown in an empty field is checked the same way: Condition has no dedicated condition for it, so you write the general attribute check with its name (see the code). For locating a field by that hint, version 7.12 added a separate selector in Selectors.
href. Умова href("/docs/terms.pdf") вирішує конкретну проблему: Selenium повертає абсолютну адресу, навіть якщо в розмітці відносний шлях. attribute("href", "/docs/terms.pdf") тут упав би, а href враховує цю різницю.
href. The href("/docs/terms.pdf") condition solves a specific problem: Selenium returns an absolute URL even if the markup holds a relative path. attribute("href", "/docs/terms.pdf") would fail here, while href accounts for the difference.
value і exactValue. value("Kyiv") перевіряє поточне значення поля як підрядок і ігнорує різницю в невидимих пробілах, тож пройде і на «Kyiv, Ukraine». Для точного збігу є exactValue. Запис через attribute("value", ...) теж працює, але порівнює повністю і менш читабельний.
value and exactValue. value("Kyiv") checks the field's current value as a substring and ignores differences in invisible whitespace, so it passes on "Kyiv, Ukraine". For an exact match there is exactValue. Writing attribute("value", ...) also works but compares the whole value and reads worse.
Класи і стилі. cssClass("active") шукає клас серед усіх класів елемента, тож class="tab active" пройде. cssValue("color", "rgba(220, 53, 69, 1)") перевіряє обчислену CSS-властивість. Для тонких випадків є domAttribute (значення з розмітки) і domProperty (живе значення властивості DOM).
Classes and styles. cssClass("active") looks for the class among all the element's classes, so class="tab active" passes. cssValue("color", "rgba(220, 53, 69, 1)") checks a computed CSS property. For subtle cases there are domAttribute (the value from the markup) and domProperty (the live DOM property).
Підводні камені. attribute("class", "active") вимагає, щоб увесь рядок класів дорівнював «active», тому для класів беруть лише cssClass. Кольори браузер повертає в rgb чи rgba, а не в hex з макета.
Pitfalls. attribute("class", "active") requires the whole class string to equal "active", so classes should only be checked with cssClass. Browsers return colours as rgb or rgba, not as the hex from the mock-up.
$("#submit").shouldHave(attribute("disabled"));
$(".dropdown").shouldHave(attribute("data-state", "open"));
$(".order").shouldHave(attributeMatching("data-id", "\\d+"));
// атрибут-підказка порожнього поля: окремої умови немає
$("#email").shouldHave(attribute("placeholder", "name@example.com"));
$(byPlaceholder("name@example.com")).setValue("anna@example.com"); // пошук за підказкою, 7.12+
$("a.terms").shouldHave(href("/docs/terms.pdf")); // відносний шлях у розмітці
$("#city").shouldHave(value("Kyiv")); // підрядок
$("#city").shouldHave(exactValue("Kyiv, Ukraine")); // повний збіг
$(".tab.orders").shouldHave(cssClass("active"));
$(".error").shouldHave(cssValue("color", "rgba(220, 53, 69, 1)"));
$("#submit").shouldHave(attribute("disabled"));
$(".dropdown").shouldHave(attribute("data-state", "open"));
$(".order").shouldHave(attributeMatching("data-id", "\\d+"));
// the hint attribute of an empty field: no dedicated condition
$("#email").shouldHave(attribute("placeholder", "name@example.com"));
$(byPlaceholder("name@example.com")).setValue("anna@example.com"); // locate by the hint, 7.12+
$("a.terms").shouldHave(href("/docs/terms.pdf")); // relative path in the markup
$("#city").shouldHave(value("Kyiv")); // substring
$("#city").shouldHave(exactValue("Kyiv, Ukraine")); // full match
$(".tab.orders").shouldHave(cssClass("active"));
$(".error").shouldHave(cssValue("color", "rgba(220, 53, 69, 1)"));
Condition, а byPlaceholder імпортується з Selectors. attribute з одним аргументом перевіряє наявність, з двома — точне значення. У Java-рядку "\\d+" стає регулярним виразом \d+. Колір у cssValue записують так, як його повертає браузер після обчислення стилів.Condition, and byPlaceholder is imported from Selectors. attribute with one argument checks presence, with two it checks the exact value. In a Java string "\\d+" becomes the regular expression \d+. The colour in cssValue is written the way the browser returns it after computing styles.Що є. enabled і disabled спираються на isEnabled() з WebDriver. checked призначений для checkbox і radio, selected — для option у списку; обидва під капотом викликають isSelected(). focused перевіряє, що саме цей елемент зараз має фокус браузера. readonly — що є атрибут readonly, а editable — що елемент доступний для взаємодії, увімкнений і не readonly.
What there is. enabled and disabled rely on WebDriver's isEnabled(). checked is meant for checkboxes and radios, selected for an option in a list; under the hood each calls isSelected(). focused checks that this exact element currently has browser focus. readonly checks for the readonly attribute, while editable checks that the element can be interacted with, is enabled and is not readonly.
Radio button. Стан перемикача можна записати трьома способами. shouldBe(checked) — найчитабельніший і правильний. shouldBe(selected) технічно дає той самий результат, бо використовує той самий isSelected(), але слово «selected» асоціюється зі списками. shouldHave(attribute("checked")) спирається на атрибут, і в сучасних фреймворках це ненадійно: React може змінювати стан перемикача через властивість, не чіпаючи атрибут у розмітці. Тому для стану я обираю checked.
Radio button. A radio's state can be written three ways. shouldBe(checked) is the most readable and the correct one. shouldBe(selected) technically gives the same result because it uses the same isSelected(), but the word "selected" is associated with lists. shouldHave(attribute("checked")) relies on the attribute, which is unreliable in modern frameworks: React can change a radio's state through the property without touching the attribute in the markup. So for state I choose checked.
Приклад. Форма реєстрації: кнопка «Далі» вимкнена, доки не поставлено згоду з умовами; після кліку по checkbox вона стає enabled, а фокус після помилки валідації має стрибнути на поле email.
Example. A sign-up form: the Next button stays disabled until the terms box is ticked; after clicking the checkbox it becomes enabled, and after a validation error the focus should jump to the email field.
Підводні камені. Кастомні перемикачі часто ховають справжній input і малюють div зверху. Тоді checked перевіряють на прихованому інпуті, а клікають по видимій мітці. disabled також не бачить блокування через CSS-клас чи aria-disabled="true": для таких компонентів потрібна перевірка класу чи атрибута.
Pitfalls. Custom toggles often hide the real input and draw a div on top. Then you check checked on the hidden input and click the visible label. disabled also does not see blocking done through a CSS class or aria-disabled="true"; such components need a class or attribute check.
$("#next").shouldBe(disabled);
$("#terms").click();
$("#terms").shouldBe(checked);
$("#next").shouldBe(enabled).click();
// radio: правильна і рівноцінна перевірки стану
$("input[name=plan][value=pro]").click();
$("input[name=plan][value=pro]").shouldBe(checked);
$("input[name=plan][value=pro]").shouldBe(selected); // той самий isSelected()
// ненадійно: атрибут може не відображати поточний стан
// $("input[name=plan][value=pro]").shouldHave(attribute("checked"));
$("#submit").click();
$("#email").shouldBe(focused);
$("#user-id").shouldBe(readonly);
$("#nickname").shouldBe(editable);
$("#next").shouldBe(disabled);
$("#terms").click();
$("#terms").shouldBe(checked);
$("#next").shouldBe(enabled).click();
// radio: the correct and an equivalent state check
$("input[name=plan][value=pro]").click();
$("input[name=plan][value=pro]").shouldBe(checked);
$("input[name=plan][value=pro]").shouldBe(selected); // the same isSelected()
// unreliable: the attribute may not reflect the current state
// $("input[name=plan][value=pro]").shouldHave(attribute("checked"));
$("#submit").click();
$("#email").shouldBe(focused);
$("#user-id").shouldBe(readonly);
$("#nickname").shouldBe(editable);
Condition, тож пишуться без дужок: shouldBe(checked). Селектор input[name=plan][value=pro] вибирає конкретний перемикач із групи за двома атрибутами. Закоментований рядок показує форму, від якої варто відмовитися.Condition, so they are written without parentheses: shouldBe(checked). The selector input[name=plan][value=pro] picks one radio from the group by two attributes. The commented-out line shows the form worth avoiding.interactable. Умова задоволена, якщо елемент або видимий, або має CSS-властивість opacity: 0. Логіка така: прозорий елемент WebDriver вважає невидимим, але користувач усе одно може по ньому клікнути чи ввести текст. Класичний приклад — стилізоване завантаження файлу, де справжній input type=file прозорий і лежить поверх красивої кнопки.
interactable. The condition is satisfied if the element is either visible or has the CSS property opacity: 0. The reasoning: WebDriver considers a transparent element invisible, yet a user can still click it or type into it. The classic example is a styled file upload, where the real input type=file is transparent and sits on top of a pretty button.
clickable. Це комбінація двох умов: interactable і enabled. Саме її Selenide використовує всередині click(): перед кліком він чекає, доки елемент стане доступним для взаємодії й увімкненим, і лише потім клікає.
clickable. It combines two conditions: interactable and enabled. This is exactly what Selenide uses inside click(): before clicking it waits until the element is usable and enabled, and only then clicks.
Чого вони не перевіряють. Жодна з умов не з’ясовує, чи елемент перекритий іншим шаром — банером cookies, модальним вікном, липким хедером — і чи він у межах viewport. Поширене уявлення, що interactable означає «видимий, увімкнений і нічим не перекритий», неточне. Перекриття виявиться лише під час самого кліку: браузер поверне ElementClickInterceptedException, і Selenide повторюватиме спробу до таймауту, а потім упаде з поясненням, який елемент отримав би клік.
What they do not check. Neither condition finds out whether the element is covered by another layer, such as a cookie banner, a modal or a sticky header, or whether it is inside the viewport. The common idea that interactable means "visible, enabled and not covered" is inaccurate. Overlap only shows up during the click itself: the browser returns ElementClickInterceptedException, and Selenide keeps retrying until the timeout and then fails with an explanation of which element would have received the click.
Коли писати явно. shouldBe(interactable) корисний для прихованих прозорих інпутів, де visible хибно падає. shouldBe(clickable) зручно поставити перед кнопкою, яка вмикається лише після валідації, щоб у звіті стояла зрозуміла причина. Є ще editable для полів вводу: інтерактивний, увімкнений і не readonly.
When to write them explicitly. shouldBe(interactable) is useful for hidden transparent inputs where visible fails wrongly. shouldBe(clickable) is handy before a button that turns on only after validation, so the report states a clear reason. There is also editable for inputs: interactable, enabled and not readonly.
// <input type="file" id="avatar" style="opacity:0"> поверх кнопки «Завантажити фото»
$("#avatar").shouldNotBe(visible); // для WebDriver прозорий елемент невидимий
$("#avatar").shouldBe(interactable); // але з ним можна працювати
$("#avatar").uploadFromClasspath("avatar.png");
$("#email").setValue("anna@example.com");
$("#save").shouldBe(clickable).click(); // interactable + enabled
// <input type="file" id="avatar" style="opacity:0"> on top of an Upload photo button
$("#avatar").shouldNotBe(visible); // a transparent element is invisible to WebDriver
$("#avatar").shouldBe(interactable); // but it can still be used
$("#avatar").uploadFromClasspath("avatar.png");
$("#email").setValue("anna@example.com");
$("#save").shouldBe(clickable).click(); // interactable + enabled
interactable і clickable — статичні поля Condition; друге оголошене як and("clickable", interactable, enabled). uploadFromClasspath бере файл із тестових ресурсів і передає його в input type=file. Явний shouldBe(clickable) перед click() не обов’язковий, але робить причину падіння очевиднішою.interactable and clickable are static fields of Condition; the second is declared as and("clickable", interactable, enabled). uploadFromClasspath takes a file from test resources and passes it to an input type=file. An explicit shouldBe(clickable) before click() is optional but makes the failure reason more obvious.Кілька умов підряд. Найпростіший спосіб — передати їх через кому: shouldBe(visible, enabled). Selenide перевіряє їх по черзі, кожну з очікуванням. Цього досить у більшості тестів.
Several conditions in a row. The simplest way is to pass them separated by commas: shouldBe(visible, enabled). Selenide checks them in turn, each with waiting. That is enough for most tests.
Іменовані комбінації. Condition.and("активна вкладка", visible, cssClass("active")) об’єднує дві й більше умов під однією назвою, яка потрапить у звіт. allOf(...) — синонім and, є версії з назвою і без. Condition.or("стан завершення", text("Done"), text("Skipped")) і anyOf(...) задоволені, коли спрацювала хоча б одна умова. У кожної умови є ще метод .or(інша) для короткого запису двох альтернатив. not(condition) заперечує умову, хоча зазвичай замість нього пишуть shouldNot*.
Named combinations. Condition.and("active tab", visible, cssClass("active")) merges two or more conditions under one name that shows up in the report. allOf(...) is a synonym for and, with and without a name. Condition.or("finished state", text("Done"), text("Skipped")) and anyOf(...) are satisfied when at least one condition holds. Every condition also has an .or(other) method for a short way to write two alternatives. not(condition) negates a condition, though usually you write shouldNot* instead.
because. visible.because("банер має зникнути після згоди") додає пояснення до повідомлення про помилку. Кастомне повідомлення прив’язують саме до умови, а не другим параметром до shouldBe. Це корисно для неочевидних бізнес-правил, де сама перевірка не пояснює, навіщо вона.
because. visible.because("the banner must go away after consent") adds an explanation to the failure message. A custom message is attached to the condition itself, not passed as a second parameter to shouldBe. It is useful for non-obvious business rules where the check alone does not explain why it exists.
Підводні камені. Автори Selenide прямо пишуть: «або» в тесті часто означає поганий дизайн. Якщо тест приймає і «Оплачено», і «Помилка», він нічого не доводить; краще зафіксувати тестові дані так, щоб результат був однозначним, або розбити на два тести. or виправданий там, де недетермінізм — нормальна поведінка, наприклад, A/B-тест показує одну з двох кнопок.
Pitfalls. The Selenide authors say it plainly: an "or" in a test often signals poor design. If a test accepts both "Paid" and "Failed", it proves nothing; it is better to pin the test data so the outcome is unambiguous, or split it into two tests. or is justified where nondeterminism is normal behaviour, for example an A/B test that shows one of two buttons.
$("#pay").shouldBe(visible, enabled);
WebElementCondition activeTab = and("active tab", visible, cssClass("active"));
$("#tab-orders").shouldBe(activeTab);
// A/B-тест: одна з двох версій кнопки
$("#cta").shouldHave(anyOf(text("Buy now"), text("Add to cart")));
$("#cta").shouldHave(text("Buy now").or(text("Add to cart")));
$(".cookie-banner").shouldBe(hidden.because("the banner must close after consent"));
$("#price").shouldHave(exactText("90.00").because("10% discount for members"));
$("#pay").shouldBe(visible, enabled);
WebElementCondition activeTab = and("active tab", visible, cssClass("active"));
$("#tab-orders").shouldBe(activeTab);
// an A/B test: one of two button versions
$("#cta").shouldHave(anyOf(text("Buy now"), text("Add to cart")));
$("#cta").shouldHave(text("Buy now").or(text("Add to cart")));
$(".cookie-banner").shouldBe(hidden.because("the banner must close after consent"));
$("#price").shouldHave(exactText("90.00").because("10% discount for members"));
WebElementCondition — базовий тип усіх умов у Selenide 7, тому комбінацію можна зберегти в змінну чи поле. and і or з назвою вимагають щонайменше дві умови. because(...) повертає нову умову з поясненням, яке з’явиться в тексті помилки поруч з очікуваним і фактичним значенням.WebElementCondition is the base type of every condition in Selenide 7, so a combination can be stored in a variable or a field. The named and and or require at least two conditions. because(...) returns a new condition with an explanation that appears in the error text next to the expected and actual values.Що це. Для перевірок на рівні браузера, а не елемента, Selenide має webdriver(). Він повертає об’єкт з тими самими методами shouldHave і shouldNotHave, а умови беруться з класу WebDriverConditions: url (повний збіг), urlStartingWith, urlContaining, title, numberOfWindows, cookie(name) і cookie(name, value), а для поточного фрейму — currentFrameUrl і його варіації.
What it is. For checks at browser level rather than element level, Selenide has webdriver(). It returns an object with the same shouldHave and shouldNotHave methods, while the conditions come from the WebDriverConditions class: url (full match), urlStartingWith, urlContaining, title, numberOfWindows, cookie(name) and cookie(name, value), plus currentFrameUrl and its variations for the current frame.
Навіщо очікування. Після сабміту форми редирект може відбутися через кількасот мілісекунд, а SPA змінює адресу через History API без перезавантаження. webdriver().shouldHave(urlContaining("/cart")) повторює перевірку до таймауту, як і перевірки елементів.
Why waiting matters. After a form is submitted, the redirect can happen a few hundred milliseconds later, and an SPA changes the address through the History API without a reload. webdriver().shouldHave(urlContaining("/cart")) retries until the timeout, just like element checks.
Getter-и. title() і WebDriverRunner.url() повертають поточні значення миттєво. assertEquals("My App", title()) чи пошук тегу title через $ працюють нестабільно: перший не чекає, другий шукає невидимий елемент у head, і умова text на ньому не пройде, бо видимого тексту в нього немає.
Getters. title() and WebDriverRunner.url() return the current values instantly. assertEquals("My App", title()) or looking up the title tag via $ are unreliable: the first does not wait, and the second searches for an invisible element in the head, where the text condition will not pass because the tag has no visible text.
Підводні камені. url(...) порівнює всю адресу разом із параметрами й фрагментом, тож трекінгові параметри на кшталт utm_source його зламають; для шляхів краще urlContaining або urlStartingWith. Перевірка numberOfWindows(2) зручна, щоб дочекатися відкриття нової вкладки перед switchTo().window(1).
Pitfalls. url(...) compares the whole address including parameters and fragment, so tracking parameters like utm_source break it; for paths prefer urlContaining or urlStartingWith. The numberOfWindows(2) check is handy for waiting until a new tab opens before switchTo().window(1).
import static com.codeborne.selenide.Selenide.webdriver;
import static com.codeborne.selenide.WebDriverConditions.*;
$("#add-to-cart").click();
$("#go-to-cart").click();
webdriver().shouldHave(urlContaining("/cart"));
webdriver().shouldHave(title("Cart - My Shop"));
$("a.help").click();
webdriver().shouldHave(numberOfWindows(2));
switchTo().window(1);
webdriver().shouldHave(urlStartingWith("https://help.example.com"));
webdriver().shouldHave(cookie("session_id"));
webdriver() повертає Conditional<WebDriver>, тому перевірки браузера виглядають так само, як перевірки елементів. Статичний імпорт WebDriverConditions.* дає умови url, title, numberOfWindows та інші. Імпорт title звідси конфліктує з getter-ом Selenide.title() лише за іменем, але не за сигнатурою, тож обидва можна використовувати поруч.webdriver() returns a Conditional<WebDriver>, so browser checks look just like element checks. The static import of WebDriverConditions.* brings the url, title, numberOfWindows and other conditions. The title imported from here shares only its name with the Selenide.title() getter, not its signature, so both can be used side by side.Що це. Кожна умова в Selenide 7 — нащадок абстрактного класу WebElementCondition з одним методом check(Driver driver, WebElement element), який повертає CheckResult: вердикт і фактичне значення для повідомлення про помилку. Selenide викликає check у циклі очікування, тож власна умова автоматично отримує повтори, таймаут, скріншот, soft-режим і запис у звіт.
What it is. Every condition in Selenide 7 is a subclass of the abstract WebElementCondition class with a single method check(Driver driver, WebElement element), which returns a CheckResult: a verdict and the actual value for the failure message. Selenide calls check inside its waiting loop, so a custom condition automatically gets retries, the timeout, a screenshot, soft mode and a report entry.
Два способи. Швидкий — Condition.match("опис", element -> ...) із предикатом; він підходить для одноразових перевірок, але в помилці не буде фактичного значення. Повний — окремий клас, який передає в CheckResult те, що реально побачив, і має зрозумілий toString(). Конструктор приймає назву, а другий параметр missingElementSatisfiesCondition вказує, чи вважати відсутній елемент успіхом (так влаштовані hidden і заперечення exist). Метод negate() за замовчуванням дає коректний shouldNot.
Two ways. The quick one is Condition.match("description", element -> ...) with a predicate; it suits one-off checks, but the error will not contain the actual value. The full one is a dedicated class that passes whatever it really saw into CheckResult and has a readable toString(). The constructor takes a name, and the second parameter, missingElementSatisfiesCondition, says whether a missing element counts as success (that is how hidden and the negation of exist work). The default negate() gives a correct shouldNot.
Коли варто. Коли доменна перевірка повторюється в десятках тестів: ціна у форматі валюти з двома знаками, дата в локалі користувача, сума в рядку кошика, що дорівнює ціні на кількість, зображення з непорожнім naturalWidth. Getter з assertEquals тут програє: він читає значення один раз, не чекає, а при падінні показує лише два рядки без скріншота і локатора.
When it is worth it. When a domain check repeats across dozens of tests: a price in currency format with two decimals, a date in the user's locale, a cart row total equal to price times quantity, an image with a non-zero naturalWidth. A getter with assertEquals loses here: it reads the value once, does not wait, and on failure shows just two strings with no screenshot and no locator.
Трейдофи. Власна умова — це код фреймворку, який треба тестувати й підтримувати. Логіка всередині check виконується десятки разів за очікування, тому вона має бути дешевою і без побічних ефектів: жодних кліків чи важкого JavaScript. Якщо перевірка складна й одноразова, простіше дочекатися стабільного стану стандартною умовою, прочитати значення і перевірити AssertJ. Я тримаю невелику бібліотеку доменних умов у спільному модулі й покриваю її юніт-тестами на статичних HTML-сторінках.
Trade-offs. A custom condition is framework code that must be tested and maintained. The logic inside check runs dozens of times during a wait, so it must be cheap and free of side effects: no clicks and no heavy JavaScript. If a check is complex and used once, it is simpler to wait for a stable state with a standard condition, read the value and assert with AssertJ. I keep a small library of domain conditions in a shared module and cover it with unit tests against static HTML pages.
public class PriceFormat extends WebElementCondition {
private static final Pattern UAH = Pattern.compile("\\d{1,3}( \\d{3})*,\\d{2} ₴");
public PriceFormat() {
super("price in UAH format");
}
@Override
public CheckResult check(Driver driver, WebElement element) {
String actual = element.getText();
return new CheckResult(UAH.matcher(actual).matches(), actual); // фактичне значення піде в помилку
}
public static WebElementCondition priceInUah() {
return new PriceFormat();
}
}
// у тесті
$$(".product .price").first().shouldHave(priceInUah());
// одноразова перевірка без окремого класу
$("img.logo").should(match("image is loaded",
el -> !"0".equals(el.getDomProperty("naturalWidth"))));
public class PriceFormat extends WebElementCondition {
private static final Pattern UAH = Pattern.compile("\\d{1,3}( \\d{3})*,\\d{2} ₴");
public PriceFormat() {
super("price in UAH format");
}
@Override
public CheckResult check(Driver driver, WebElement element) {
String actual = element.getText();
return new CheckResult(UAH.matcher(actual).matches(), actual); // the actual value goes into the error
}
public static WebElementCondition priceInUah() {
return new PriceFormat();
}
}
// in a test
$$(".product .price").first().shouldHave(priceInUah());
// a one-off check without a dedicated class
$("img.logo").should(match("image is loaded",
el -> !"0".equals(el.getDomProperty("naturalWidth"))));
super("price in UAH format") задає назву, яку Selenide покаже в звіті. Конструктор CheckResult(boolean, Object) приймає вердикт і фактичне значення. Статична фабрика priceInUah() дозволяє писати умову в стилі вбудованих. match приймає опис і Predicate<WebElement>; getDomProperty читає живу властивість DOM. Для перевірки завантаженого зображення є і готова умова image, тут вона лише ілюструє синтаксис.super("price in UAH format") sets the name Selenide shows in the report. The CheckResult(boolean, Object) constructor takes the verdict and the actual value. The static factory priceInUah() lets you write the condition in the style of the built-in ones. match takes a description and a Predicate<WebElement>; getDomProperty reads a live DOM property. There is also a ready image condition for loaded images; here it only illustrates the syntax.Що це. Кожен метод should* і кожна дія в Selenide — це цикл: знайти елемент, перевірити умову, і якщо не вийшло, почекати pollingInterval (200 мс) і спробувати знову. Цикл триває до Configuration.timeout, який за замовчуванням дорівнює 4000 мс. Якщо умова так і не виконалась, тест падає з ElementNotFound або ElementShould.
What it is. Every should* method and every action in Selenide is a loop: find the element, check the condition, and if it fails, sleep for pollingInterval (200 ms) and try again. The loop runs until Configuration.timeout, which is 4000 ms by default. If the condition never holds, the test fails with ElementNotFound or ElementShould.
Дії теж чекають. Перед click() Selenide чекає, доки елемент з’явиться і стане придатним для взаємодії й enabled; setValue() чекає на поле. Тому писати shouldBe(visible) перед кожним кліком не обов’язково — це варто робити лише тоді, коли видимість сама по собі є частиною перевірки.
Actions wait too. Before click(), Selenide waits until the element appears and becomes interactable and enabled; setValue() waits for the field. So you do not have to put shouldBe(visible) in front of every click; do it only when visibility is itself part of what you are checking.
Чому explicit wait зайвий. WebDriverWait і ExpectedConditions вирішують ту саму задачу, яку Selenide вже вбудував у кожен рядок. Якщо дописувати їх поверх, код стає довшим, а таймаути — непередбачуваними: тест чекає спершу на своєму WebDriverWait, потім ще раз усередині Selenide.
Why an explicit wait is unnecessary. WebDriverWait and ExpectedConditions solve the very problem Selenide has already built into every line. Adding them on top only makes the code longer and the timing unpredictable: the test first waits in its own WebDriverWait and then once more inside Selenide.
Підводні камені. Не вмикай implicitlyWait на сирому драйвері: кожна внутрішня спроба пошуку тоді блокується на цей час, і негативні перевірки на кшталт shouldNot(exist) розтягуються в рази. І пам’ятай, що очікування працює лише для методів Selenide; getter на кшталт getText() повертає значення тієї миті, без повторів.
Pitfalls. Do not turn on implicitlyWait on the raw driver: every internal lookup attempt then blocks for that long, and negative checks such as shouldNot(exist) stretch several times over. And remember that waiting only applies to Selenide methods; a getter like getText() returns the value at that instant, with no retries.
open("/orders");
$("#create-order").click(); // чекає, доки кнопка стане клікабельною
$(".toast").shouldHave(text("Order created")); // повторює перевірку кожні 200 мс до 4 с
// не треба: дублює те, що Selenide вже робить
new WebDriverWait(WebDriverRunner.getWebDriver(), Duration.ofSeconds(10))
.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".toast")));
open("/orders");
$("#create-order").click(); // waits until the button is clickable
$(".toast").shouldHave(text("Order created")); // retries every 200 ms for up to 4 s
// not needed: duplicates what Selenide already does
new WebDriverWait(WebDriverRunner.getWebDriver(), Duration.ofSeconds(10))
.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".toast")));
click() і shouldHave() не мають параметра очікування, бо беруть його з Configuration.timeout і Configuration.pollingInterval. Блок з WebDriverWait показано як антиприклад: він працює, але лише додає другий, незалежний таймаут поверх вбудованого.click() and shouldHave() take no wait parameter because they read it from Configuration.timeout and Configuration.pollingInterval. The WebDriverWait block is shown as an anti-example: it works, but only adds a second, independent timeout on top of the built-in one.Що це. Configuration.timeout — максимальний час у мілісекундах, протягом якого Selenide повторює перевірку чи чекає елемент перед дією; за замовчуванням 4000. Configuration.pollingInterval — пауза між спробами, за замовчуванням 200 мс. Разом вони визначають, скільки разів умова буде перевірена до падіння.
What it is. Configuration.timeout is the maximum time in milliseconds that Selenide keeps retrying a check or waiting for an element before an action; the default is 4000. Configuration.pollingInterval is the pause between attempts, 200 ms by default. Together they decide how many times a condition is evaluated before the test fails.
Як змінити. Є три способи з однаковим результатом: присвоїти поле в коді (Configuration.timeout = 8000, зазвичай у @BeforeAll), записати selenide.timeout=8000 у файл selenide.properties у ресурсах тестів або передати системну властивість -Dselenide.timeout=8000 з Maven чи Gradle. Останній варіант зручний у CI, де стенд повільніший за локальний.
How to change them. There are three equivalent ways: assign the field in code (Configuration.timeout = 8000, usually in @BeforeAll), put selenide.timeout=8000 into a selenide.properties file in the test resources, or pass the system property -Dselenide.timeout=8000 from Maven or Gradle. The last option is handy in CI, where the stand is slower than your laptop.
Що дає збільшення чи зменшення. Більший timeout робить тести терпимішими до повільного стенду, але кожен справжній дефект тепер падає довше, а негативні перевірки теж чекають повний час. Менший pollingInterval реагує швидше на появу елемента, проте частіше смикає браузер; збільшувати його має сенс хіба що для дуже важких сторінок.
What raising or lowering does. A bigger timeout makes tests more tolerant of a slow stand, but every genuine defect now takes longer to fail, and negative checks also wait the full time. A smaller pollingInterval reacts faster when an element appears but pokes the browser more often; raising it only makes sense for very heavy pages.
Підводні камені. Не плутай ці параметри з Configuration.pageLoadTimeout: він обмежує завантаження сторінки в open() і в Selenide дорівнює 30 секундам замість 300 у чистому WebDriver. Підіймати глобальний timeout до 30 секунд, щоб «не падало», — погана ідея: це ховає реальні проблеми продуктивності й сповільнює кожне падіння.
Pitfalls. Do not confuse these settings with Configuration.pageLoadTimeout: it limits page loading in open() and in Selenide defaults to 30 seconds instead of the 300 seconds of plain WebDriver. Raising the global timeout to 30 seconds just so tests stop failing is a bad idea: it hides real performance problems and slows down every failure.
// 1. У коді, один раз для всього класу
@BeforeAll
static void setUp() {
Configuration.timeout = 8000; // мс
Configuration.pollingInterval = 100; // мс
}
// 2. src/test/resources/selenide.properties
// selenide.timeout=8000
// selenide.pollingInterval=100
// 3. Командний рядок у CI
// mvn test -Dselenide.timeout=10000
// 1. In code, once for the whole class
@BeforeAll
static void setUp() {
Configuration.timeout = 8000; // ms
Configuration.pollingInterval = 100; // ms
}
// 2. src/test/resources/selenide.properties
// selenide.timeout=8000
// selenide.pollingInterval=100
// 3. The command line in CI
// mvn test -Dselenide.timeout=10000
Configuration статичні й типу long, значення в мілісекундах. Ключі у файлі й системних властивостях мають префікс selenide. і ту саму назву, що й поле. Метод у @BeforeAll має бути static.Configuration fields are static long values in milliseconds. Keys in the file and in system properties carry the selenide. prefix and the same name as the field. The @BeforeAll method must be static.Що це. Методи should, shouldBe, shouldHave, shouldNot і їхні аналоги для колекцій мають перевантаження з другим параметром Duration. Запис $("#report").shouldBe(visible, Duration.ofSeconds(30)) чекає до 30 секунд лише в цьому виклику; Configuration.timeout лишається без змін, і наступний рядок знову чекає стандартні 4 секунди. Це не 30 спроб і не пауза на 30 секунд: якщо елемент з’явився за дві секунди, тест одразу йде далі.
What it is. should, shouldBe, shouldHave, shouldNot and their collection counterparts have an overload with a second Duration parameter. $("#report").shouldBe(visible, Duration.ofSeconds(30)) waits up to 30 seconds in this call only; Configuration.timeout stays the same, and the next line waits the standard 4 seconds again. It is neither 30 attempts nor a 30-second pause: if the element shows up after two seconds, the test moves on immediately.
Для дій. Клік теж можна обмежити власним часом через ClickOptions.withTimeout(Duration): $("#pay").click(withTimeout(Duration.ofSeconds(15))). У скачування файлів свій параметр — DownloadOptions.withTimeout.
For actions. A click can also get its own limit through ClickOptions.withTimeout(Duration): $("#pay").click(withTimeout(Duration.ofSeconds(15))). Downloads have their own parameter, DownloadOptions.withTimeout.
Коли доречно. Коли одна операція об’єктивно повільніша за решту: генерація PDF-звіту, імпорт великого файлу, обробка платежу в пісочниці банку. Локальний таймаут документує цей факт у самому тесті: читач бачить, що саме тут очікувана затримка.
When it fits. When one operation is genuinely slower than the rest: generating a PDF report, importing a large file, processing a payment in a bank sandbox. A local timeout documents that fact inside the test itself: the reader sees that a delay is expected exactly here.
Підводні камені. Якщо локальні таймаути з’являються в кожному другому рядку, проблема не в тестах, а в стенді або в глобальному значенні, і це треба обговорити з командою. Не використовуй великий локальний таймаут як ліки від flaky: він приховає гонку, а не прибере її. І не став його на негативні перевірки без потреби, бо shouldNot(exist, Duration.ofMinutes(1)) на успішному шляху чекатиме лише до зникнення, а от на помилковому — цілу хвилину.
Pitfalls. If local timeouts show up in every other line, the problem is not the tests but the stand or the global value, and that is a conversation to have with the team. Do not use a big local timeout as a cure for flakiness: it hides a race rather than removing it. And do not put one on negative checks without need, because shouldNot(exist, Duration.ofMinutes(1)) finishes as soon as the element is gone on the happy path, but waits a full minute when something is wrong.
import static com.codeborne.selenide.ClickOptions.withTimeout;
$("#generate-report").click();
// лише тут чекаємо до 30 с; глобальні 4 с не змінюються
$("#report-link").shouldBe(visible, Duration.ofSeconds(30));
// клік чекатиме кнопку до 15 с
$("#pay").click(withTimeout(Duration.ofSeconds(15)));
// колекція теж приймає Duration
$$(".invoice-row").shouldHave(size(120), Duration.ofSeconds(20));
import static com.codeborne.selenide.ClickOptions.withTimeout;
$("#generate-report").click();
// only here we wait up to 30 s; the global 4 s stays the same
$("#report-link").shouldBe(visible, Duration.ofSeconds(30));
// the click waits for the button for up to 15 s
$("#pay").click(withTimeout(Duration.ofSeconds(15)));
// a collection accepts a Duration too
$$(".invoice-row").shouldHave(size(120), Duration.ofSeconds(20));
Duration є в перевантаженнях shouldBe(condition, timeout) і shouldHave(condition, timeout); перевантаження з кількома умовами його не приймає, тому для однієї перевірки передається рівно одна умова. withTimeout — статичний фабричний метод ClickOptions, який повертає опції кліку з власним лімітом.Duration argument exists in the shouldBe(condition, timeout) and shouldHave(condition, timeout) overloads; the varargs overload with several conditions does not take it, so a timed check passes exactly one condition. withTimeout is a static factory method of ClickOptions that returns click options with their own limit.Що це. Selenide.sleep(ms) — просто пауза: усередині цикл навколо Thread.sleep, який гарантує, що мине щонайменше вказаний час. Ніякої перевірки стану сторінки там немає, і сама документація методу позначає його як небажаний.
What it is. Selenide.sleep(ms) is just a pause: inside there is a loop around Thread.sleep that guarantees at least the given time passes. There is no check of the page state at all, and the method's own documentation marks it as not recommended.
Чому шкодить. Фіксована пауза завжди або задовга, або закоротка. На швидкому стенді тест марно стоїть, і сотня таких пауз по дві секунди додає кілька хвилин до прогону. На повільному CI-агенті тих самих двох секунд не вистачає, і тест падає нестабільно. До того ж sleep нічого не каже про очікуваний стан: читач не розуміє, на що саме чекали.
Why it hurts. A fixed pause is always either too long or too short. On a fast stand the test stands idle for nothing, and a hundred two-second pauses add minutes to the run. On a slow CI agent those same two seconds are not enough, and the test fails intermittently. On top of that, a sleep says nothing about the expected state: the reader cannot tell what exactly was being waited for.
Чим замінити. Чекати на результат, який бачить користувач: $(".spinner").should(disappear), $("#status").shouldHave(text("Готово")), $$(".row").shouldHave(size(10)). Такий рядок завершується тієї миті, коли стан настав, і водночас сам є перевіркою.
What to use instead. Wait for an outcome the user can see: $(".spinner").should(disappear), $("#status").shouldHave(text("Done")), $$(".row").shouldHave(size(10)). Such a line finishes the moment the state is reached and is an assertion at the same time.
Рідкісний законний випадок. Коли треба довести, що чогось не сталося за певний час: наприклад, debounce-поле не відправляє запит частіше, ніж раз на секунду. Тоді коротка пауза з коментарем «чому» допустима. Також sleep буває корисним тимчасово під час налагодження, але в main-гілку він потрапляти не повинен.
The rare legitimate case. When you must prove that something did not happen within a period: for example, a debounced field does not send a request more than once a second. A short pause with a comment explaining why is acceptable there. A sleep can also help temporarily while debugging, but it should never reach the main branch.
// погано: 3 с навмання, на повільному CI може бути мало
$("#save").click();
sleep(3000);
$("#status").getText();
// добре: завершується, щойно стан настав
$("#save").click();
$(".spinner").should(disappear);
$("#status").shouldHave(exactText("Збережено"));
// bad: 3 s at random, may be too little on slow CI
$("#save").click();
sleep(3000);
$("#status").getText();
// good: finishes as soon as the state is reached
$("#save").click();
$(".spinner").should(disappear);
$("#status").shouldHave(exactText("Saved"));
sleep тут — статичний Selenide.sleep(long). disappear — синонім умови hidden, який читається природніше з should: елемент прихований або зовсім відсутній у DOM. exactText порівнює весь текст без урахування регістру й зайвих пробілів.sleep here is the static Selenide.sleep(long). disappear is a synonym of the hidden condition that reads more naturally with should: the element is hidden or not in the DOM at all. exactText compares the whole text, ignoring case and extra spaces.Лоадер. Найпростіше — чекати, доки індикатор зникне: $(".spinner").should(disappear). Умова disappear проходить і тоді, коли елемент прихований, і тоді, коли його прибрали з DOM. Але є пастка: якщо спінер з’являється із затримкою в 100–300 мс, перевірка може пройти ще до його появи. Надійніше чекати не на зникнення спінера, а на появу результату.
Loader. The simplest approach is to wait for the indicator to go away: $(".spinner").should(disappear). The disappear condition passes both when the element is hidden and when it has been removed from the DOM. But there is a trap: if the spinner shows up with a 100–300 ms delay, the check can pass before it even appears. It is more reliable to wait for the result to appear rather than for the spinner to vanish.
AJAX. Selenide не стежить за мережевими запитами, і це правильно: користувач теж не бачить запитів, він бачить дані. Тому після дії чекай на наслідок — оновлений рядок таблиці, новий текст, потрібну кількість карток: $$(".product").shouldHave(size(12)). Коли стан неочевидний, розробники часто додають службовий атрибут на кшталт data-loaded="true", і тоді перевірка shouldHave(attribute("data-loaded", "true")) стає однозначною.
AJAX. Selenide does not track network requests, and rightly so: a user does not see requests either, they see data. So after an action wait for its consequence — an updated table row, new text, the right number of cards: $$(".product").shouldHave(size(12)). When the state is not obvious, developers often add a service attribute such as data-loaded="true", and then shouldHave(attribute("data-loaded", "true")) becomes unambiguous.
Анімації. У Selenide 7 є умова animated: вона двічі поспіль у кадрах requestAnimationFrame вимірює позицію й розмір елемента і вважає його анімованим, якщо вони змінилися. Отже $(".modal").shouldNotBe(animated) чекає, доки вікно перестане рухатися. У неактивній вкладці перевірка кидає помилку, бо браузер там не малює кадри.
Animations. Selenide 7 has an animated condition: it measures the element's position and size in two consecutive requestAnimationFrame frames and treats it as animated if they changed. So $(".modal").shouldNotBe(animated) waits until the dialog stops moving. In an inactive tab the check throws an error, because the browser does not paint frames there.
Радикальніший шлях. Вимкнути CSS-анімації й переходи у тестовому оточенні: фронтенд за прапорцем додає стиль із нульовою тривалістю, або тест вставляє його сам через JavaScript. Тести стають швидшими й стабільнішими, але саму анімацію тоді перевіряє вже хтось інший.
A more radical route. Turn off CSS animations and transitions in the test environment: the front end adds a zero-duration style behind a flag, or the test injects it itself through JavaScript. Tests get faster and more stable, but then someone else has to cover the animation itself.
$("#search").setValue("laptop").pressEnter();
// краще чекати результат, ніж лише зникнення спінера
$(".spinner").should(disappear);
$$(".product").shouldHave(size(12));
$("#results").shouldHave(attribute("data-loaded", "true"));
// дочекатися, доки модальне вікно доїде
$("#open-cart").click();
$(".modal").shouldBe(visible).shouldNotBe(animated);
$(".modal button.checkout").click();
// вимкнути анімації для всієї сторінки в тестовому оточенні
executeJavaScript(
"const s = document.createElement('style');" +
"s.textContent = '*{transition:none !important;animation:none !important}';" +
"document.head.appendChild(s);");
$("#search").setValue("laptop").pressEnter();
// waiting for the result beats waiting only for the spinner to vanish
$(".spinner").should(disappear);
$$(".product").shouldHave(size(12));
$("#results").shouldHave(attribute("data-loaded", "true"));
// wait until the modal has finished sliding in
$("#open-cart").click();
$(".modal").shouldBe(visible).shouldNotBe(animated);
$(".modal button.checkout").click();
// turn animations off for the whole page in the test environment
executeJavaScript(
"const s = document.createElement('style');" +
"s.textContent = '*{transition:none !important;animation:none !important}';" +
"document.head.appendChild(s);");
shouldBe(visible).shouldNotBe(animated) — ланцюжок: кожна перевірка повертає той самий елемент, тож спершу чекаємо появи, потім зупинки руху. animated — статичне поле Condition, додане у 7.0.7. executeJavaScript виконує рядок коду в сторінці; тут він додає в head стиль, що обнуляє переходи й анімації для всіх елементів.shouldBe(visible).shouldNotBe(animated) is a chain: each check returns the same element, so we first wait for it to appear and then for the motion to stop. animated is a static field of Condition, added in 7.0.7. executeJavaScript runs a string of code in the page; here it adds a style to head that zeroes transitions and animations for every element.Контекст. Тест проходить дев’ять разів із десяти, а в CI падає раз на кілька прогонів. Перший інстинкт — підняти Configuration.timeout, але це лікує лише одну причину з кількох і водночас сповільнює кожне справжнє падіння. Спершу треба зрозуміти, до якого класу належить нестабільність.
Context. A test passes nine times out of ten, and in CI it fails once every few runs. The first instinct is to raise Configuration.timeout, but that treats only one cause out of several while slowing down every genuine failure. First you need to know which class of flakiness you are dealing with.
Крок 1: прочитати падіння. Повідомлення Selenide вже містить більшість відповідей: рядок Element should ... з очікуваною умовою й локатором, Element: з HTML знайденого вузла, Actual value: з фактичним значенням на момент останньої спроби, Timeout:, а також шляхи до скріншота й збереженої сторінки. Якщо фактичне значення «майже правильне» (старе число, попередній статус), це гонка або повільний бекенд. Якщо ElementNotFound, а на скріншоті інша сторінка — проблема в навігації чи даних.
Step 1: read the failure. A Selenide message already holds most answers: the Element should ... line with the expected condition and locator, Element: with the HTML of the node that was found, Actual value: with the real value at the last attempt, Timeout:, plus paths to the screenshot and the saved page source. If the actual value is almost right (an old number, a previous status), it is a race or a slow backend. If it is ElementNotFound and the screenshot shows a different page, the problem is navigation or data.
Крок 2: класифікувати. Повільне середовище: на скріншоті ще лоадер, фактичне значення з’являється пізніше — тут доречний локальний таймаут або розмова про продуктивність. Анімація: клік падає з ElementClickInterceptedException, бо елемент ще їде чи його перекриває оверлей — рішення shouldNotBe(animated) або вимкнені переходи. Гонка з перемальовуванням: тест прочитав getter до оновлення або клікнув по кнопці, яку React одразу замінив — рішення перевіряти умовою, а не getter’ом, і чекати на кінцевий стан. Дані: тест залежить від порядку, спільного акаунта чи часу доби — рішення ізольовані дані й фіксований час.
Step 2: classify. Slow environment: the screenshot still shows a loader and the value arrives later — a local timeout or a performance discussion is appropriate. Animation: the click fails with ElementClickInterceptedException because the element is still moving or an overlay covers it — the fix is shouldNotBe(animated) or disabled transitions. Race with re-rendering: the test read a getter before the update or clicked a button React replaced right away — the fix is asserting with a condition rather than a getter and waiting for the final state. Data: the test depends on order, a shared account or the time of day — the fix is isolated data and a fixed clock.
Крок 3: відтворити. Прогнати тест 50 разів через @RepeatedTest локально і в CI-контейнері, увімкнути headless із тим самим browserSize, що в CI, штучно сповільнити мережу через DevTools. Короткий pollingInterval рідко є причиною, але, змінивши його, можна перевірити, чи залежить результат від таймінгу.
Step 3: reproduce. Run the test 50 times with @RepeatedTest locally and inside the CI container, use headless with the same browserSize as CI, and slow the network down artificially through DevTools. A short pollingInterval is rarely the cause, but changing it tells you whether the outcome depends on timing.
Трейдоф. Ретраї на рівні раннера дають зелений звіт швидко, але ховають класи дефектів; я вмикаю їх лише разом із метрикою «пройшов після повтору» і розбором кожного такого випадку.
Trade-off. Retries at runner level turn the report green quickly but hide whole classes of defects; I enable them only together with a passed-on-retry metric and a review of every such case.
// типове повідомлення Selenide (скорочено)
// Element should have exact text "Paid" {#order-1042 .status}
// Element: '<span class="status">Pending</span>'
// Actual value: text="Pending"
// Screenshot: file:/build/reports/tests/1727600000.0.png
// Page source: file:/build/reports/tests/1727600000.0.html
// Timeout: 4 s.
class OrderStatusFlakyTest {
@BeforeAll
static void likeCi() {
Configuration.headless = true;
Configuration.browserSize = "1366x768"; // той самий розмір, що в CI
}
@RepeatedTest(50) // відтворити нестабільність локально
void paidOrderShowsPaidStatus() {
open("/orders/1042");
$("#pay").click();
// гонка: getText() читає одразу, без очікування
// assertEquals("Paid", $("#order-1042 .status").getText());
$("#order-1042 .status").shouldHave(exactText("Paid"));
}
}
// a typical Selenide message (shortened)
// Element should have exact text "Paid" {#order-1042 .status}
// Element: '<span class="status">Pending</span>'
// Actual value: text="Pending"
// Screenshot: file:/build/reports/tests/1727600000.0.png
// Page source: file:/build/reports/tests/1727600000.0.html
// Timeout: 4 s.
class OrderStatusFlakyTest {
@BeforeAll
static void likeCi() {
Configuration.headless = true;
Configuration.browserSize = "1366x768"; // the same size as in CI
}
@RepeatedTest(50) // reproduce the flakiness locally
void paidOrderShowsPaidStatus() {
open("/orders/1042");
$("#pay").click();
// race: getText() reads immediately, with no waiting
// assertEquals("Paid", $("#order-1042 .status").getText());
$("#order-1042 .status").shouldHave(exactText("Paid"));
}
}
ElementShould: умова з локатором у фігурних дужках, HTML вузла, фактичне значення, скріншот, сторінка і таймаут. @RepeatedTest(50) з JUnit 5 запускає метод 50 разів як окремі тести. Закоментований assertEquals з getText() — класичне джерело гонки; рядок з shouldHave(exactText(...)) повторює перевірку до таймауту.ElementShould message: the condition with the locator in braces, the node HTML, the actual value, the screenshot, the page source and the timeout. @RepeatedTest(50) from JUnit 5 runs the method 50 times as separate tests. The commented-out assertEquals with getText() is a classic source of races; the shouldHave(exactText(...)) line retries until the timeout.Що це. $$(".product") повертає ElementsCollection — ліниву колекцію всіх елементів за селектором. Як і SelenideElement, вона нічого не шукає при створенні: список вузлів збирається заново при кожній перевірці чи зверненні.
What it is. $$(".product") returns an ElementsCollection, a lazy collection of all elements matching the selector. Like a SelenideElement, it looks nothing up when created: the list of nodes is gathered again on every check or access.
Доступ до елементів. first() — перший елемент, last() — останній, get(i) — елемент за індексом, який рахується з нуля, тож get(0) те саме, що first(). Кожен із цих методів повертає лінивий SelenideElement, на якому працюють звичайні click() і shouldHave(). Варіанти first(n) і last(n) повертають підколекцію з перших або останніх n елементів.
Getting elements. first() is the first element, last() the last one, get(i) the element at a zero-based index, so get(0) is the same as first(). Each of these returns a lazy SelenideElement that supports the usual click() and shouldHave(). The first(n) and last(n) variants return a sub-collection with the first or last n elements.
Розмір. Перевірка кількості — $$(".product").shouldHave(size(10)), де size імпортується з CollectionCondition. Вона чекає, доки елементів стане рівно десять. Метод size() без аргументів — це getter: повертає кількість тієї миті, не чекаючи, поки список довантажиться.
Size. Checking the count is $$(".product").shouldHave(size(10)), where size is imported from CollectionCondition. It waits until there are exactly ten elements. The no-argument size() method is a getter: it returns the count at that instant without waiting for the list to finish loading.
Підводні камені. Класична помилка — assertEquals(10, $$(".product").size()) одразу після пошуку: список ще порожній або неповний, і тест мерехтить. Друга — вважати, що get(1) означає перший елемент. Третя — забути, що $$ повертає і приховані елементи, якщо вони відповідають селектору.
Pitfalls. The classic mistake is assertEquals(10, $$(".product").size()) right after the search: the list is still empty or partial, and the test flickers. Another is assuming get(1) means the first element. A third is forgetting that $$ also returns hidden elements if they match the selector.
import static com.codeborne.selenide.CollectionCondition.size;
ElementsCollection products = $$(".product"); // ще нічого не шукали
products.shouldHave(size(10)); // чекає рівно 10 елементів
products.first().shouldHave(text("Laptop")); // те саме, що get(0)
products.get(2).$("button.add").click(); // третій елемент, індекс з нуля
products.last().shouldHave(text("Mouse"));
products.first(3).shouldHave(size(3)); // підколекція з перших трьох
int now = products.size(); // getter: без очікування, лише для логів
import static com.codeborne.selenide.CollectionCondition.size;
ElementsCollection products = $$(".product"); // nothing looked up yet
products.shouldHave(size(10)); // waits for exactly 10 elements
products.first().shouldHave(text("Laptop")); // same as get(0)
products.get(2).$("button.add").click(); // the third element, zero-based index
products.last().shouldHave(text("Mouse"));
products.first(3).shouldHave(size(3)); // a sub-collection of the first three
int now = products.size(); // getter: no waiting, only for logs
size(10) з CollectionCondition — умова для колекції, а size() без аргументів — метод самої колекції; назви схожі, поведінка різна. get(2).$("button.add") — пошук усередині третього елемента, увесь ланцюжок лишається лінивим.size(10) from CollectionCondition is a collection condition, while the no-argument size() is a method of the collection itself; the names are similar but the behaviour differs. get(2).$("button.add") searches inside the third element, and the whole chain stays lazy.Що це. Колекцію можна звузити умовою, не пишучи складного CSS. filterBy(condition) (синонім filter) лишає елементи, що задовольняють умову. excludeWith(condition) (синонім exclude) навпаки, відкидає їх. findBy(condition) (синонім find) повертає перший елемент, що відповідає умові. Усі вони нічого не видаляють зі сторінки і лишаються лінивими: результат обчислюється заново при кожній перевірці.
What it is. You can narrow a collection with a condition instead of writing complex CSS. filterBy(condition) (alias filter) keeps the elements that satisfy the condition. excludeWith(condition) (alias exclude) does the opposite and drops them. findBy(condition) (alias find) returns the first element that matches. None of them removes anything from the page, and they all stay lazy: the result is recomputed on every check.
Коли доречно. Коли критерій — не структура DOM, а стан чи текст: видимі пункти меню, неархівні задачі, рядок з потрібним ім’ям. $$(".task").excludeWith(cssClass("archived")).shouldHave(size(3)) читається як речення і не залежить від того, як саме верстальник позначив архівні задачі в розмітці.
When it fits. When the criterion is not DOM structure but state or text: visible menu items, non-archived tasks, the row with a given name. $$(".task").excludeWith(cssClass("archived")).shouldHave(size(3)) reads like a sentence and does not depend on how exactly the markup flags archived tasks.
Приклад. У списку сповіщень частина прихована за «Показати ще». $$(".notification").filterBy(visible) дає лише ті, що бачить користувач, і на них можна перевірити кількість чи тексти. findBy(text("Payment failed")) повертає конкретне сповіщення, у якому далі клікають кнопку.
Example. In a notifications list some items are hidden behind Show more. $$(".notification").filterBy(visible) gives only the ones the user sees, and you can check their count or texts. findBy(text("Payment failed")) returns one specific notification where you then click a button.
Підводні камені. Фільтрація відбувається на боці тесту: Selenide спершу збирає всі елементи за селектором, а потім для кожного перевіряє умову. На списку з тисячами вузлів це повільно, і там краще звузити сам селектор. findBy бере перший збіг, тому неточна умова на кшталт text("Anna") може знайти «Annabel». І пам’ятай, що text у фільтрі — це пошук підрядка без урахування регістру.
Pitfalls. Filtering happens on the test side: Selenide first gathers every element for the selector and then checks the condition for each one. On a list with thousands of nodes that is slow, and a narrower selector is better there. findBy takes the first match, so a loose condition like text("Anna") can find Annabel. And remember that text in a filter is a case-insensitive substring search.
ElementsCollection tasks = $$(".task");
// лише видимі задачі, без архівних
tasks.filterBy(visible)
.excludeWith(cssClass("archived"))
.shouldHave(size(3));
// перша задача з точним текстом, далі дія всередині неї
tasks.findBy(exactText("Pay invoice #1042"))
.$("button.done")
.click();
// після дії фільтр обчислюється заново
tasks.filterBy(cssClass("done")).shouldHave(size(1));
ElementsCollection tasks = $$(".task");
// only visible tasks, without archived ones
tasks.filterBy(visible)
.excludeWith(cssClass("archived"))
.shouldHave(size(3));
// the first task with an exact text, then an action inside it
tasks.findBy(exactText("Pay invoice #1042"))
.$("button.done")
.click();
// after the action the filter is evaluated again
tasks.filterBy(cssClass("done")).shouldHave(size(1));
filterBy і excludeWith повертають нову ElementsCollection, тому їх можна складати в ланцюжок і завершувати умовою колекції. findBy повертає SelenideElement, тож після нього йдуть вкладений пошук $ і звичайні дії. Умови visible, cssClass, exactText — ті самі, що для окремого елемента.filterBy and excludeWith return a new ElementsCollection, so they chain and end with a collection condition. findBy returns a SelenideElement, so it is followed by a nested $ lookup and ordinary actions. visible, cssClass and exactText are the same conditions used for a single element.Розмір. CollectionCondition дає size(n), sizeGreaterThan(n), sizeGreaterThanOrEqual(n), sizeLessThan(n), sizeLessThanOrEqual(n) і sizeNotEqual(n). Щоб перевірити, що колекція не порожня, пишуть sizeGreaterThan(0); для порожньої є поле empty, яке дорівнює size(0).
Size. CollectionCondition provides size(n), sizeGreaterThan(n), sizeGreaterThanOrEqual(n), sizeLessThan(n), sizeLessThanOrEqual(n) and sizeNotEqual(n). To check that a collection is not empty you write sizeGreaterThan(0); for an empty one there is the empty field, which equals size(0).
Тексти. texts("a", "b") вимагає, щоб елементів було стільки ж, скільки очікуваних рядків, і щоб кожен у тому ж порядку містив свій текст; за замовчуванням це підрядок без урахування регістру, а з Configuration.textCheck = FULL_TEXT — повний збіг (так стане за замовчуванням у Selenide 8). exactTexts завжди порівнює текст повністю, але теж без регістру; exactTextsCaseSensitive — з регістром. textsInAnyOrder і exactTextsCaseSensitiveInAnyOrder ігнорують порядок. containExactTextsCaseSensitive лише вимагає, щоб серед елементів були потрібні тексти, решта не заважає.
Texts. texts("a", "b") requires as many elements as expected strings, each containing its text in the same order; by default that is a case-insensitive substring, and with Configuration.textCheck = FULL_TEXT it is a full match (which becomes the default in Selenide 8). exactTexts always compares the whole text, but also ignores case; exactTextsCaseSensitive respects case. textsInAnyOrder and exactTextsCaseSensitiveInAnyOrder ignore order. containExactTextsCaseSensitive only requires the expected texts to be present among the elements, and extra ones do not matter.
itemWithText. itemWithText("Paid") проходить, якщо хоча б один елемент має саме такий текст. У коді Selenide це перевірка «список текстів містить рядок», тобто точний збіг, а не підрядок, як часто вважають. Для гнучкіших правил є anyMatch, allMatch і noneMatch з описом і предикатом.
itemWithText. itemWithText("Paid") passes if at least one element has exactly that text. In the Selenide code it is a list-of-texts contains-string check, so it is an exact match, not a substring as people often assume. For looser rules there are anyMatch, allMatch and noneMatch, which take a description and a predicate.
Getter texts(). Метод колекції texts() повертає List<String> з поточними текстами і не чекає. Його зручно логувати, але для перевірки краще умова texts(...): вона повторюється до таймауту і в помилці показує обидва списки.
The texts() getter. The collection method texts() returns a List<String> of the current texts and does not wait. It is handy for logging, but for assertions the texts(...) condition is better: it retries until the timeout and shows both lists in the error.
import static com.codeborne.selenide.CollectionCondition.*;
ElementsCollection statuses = $$("#orders .status");
statuses.shouldHave(sizeGreaterThan(0)); // колекція не порожня
statuses.shouldHave(texts("Paid", "New", "Shipped")); // порядок важливий, підрядок
statuses.shouldHave(exactTexts("Paid", "New", "Shipped"));// повний текст, без регістру
statuses.shouldHave(textsInAnyOrder("New", "Shipped", "Paid"));
statuses.shouldHave(itemWithText("Shipped")); // хоча б один точно такий
statuses.shouldHave(noneMatch("no empty status",
el -> el.getText().isBlank()));
$$("#archive .order").shouldBe(CollectionCondition.empty); // те саме, що size(0)
List<String> now = statuses.texts(); // getter: лише для логів
import static com.codeborne.selenide.CollectionCondition.*;
ElementsCollection statuses = $$("#orders .status");
statuses.shouldHave(sizeGreaterThan(0)); // the collection is not empty
statuses.shouldHave(texts("Paid", "New", "Shipped")); // order matters, substring
statuses.shouldHave(exactTexts("Paid", "New", "Shipped"));// full text, case ignored
statuses.shouldHave(textsInAnyOrder("New", "Shipped", "Paid"));
statuses.shouldHave(itemWithText("Shipped")); // at least one exactly like this
statuses.shouldHave(noneMatch("no empty status",
el -> el.getText().isBlank()));
$$("#archive .order").shouldBe(CollectionCondition.empty); // same as size(0)
List<String> now = statuses.texts(); // getter: logging only
CollectionCondition.* дає і умови розміру, і текстові умови колекцій. Предикат у noneMatch отримує сирий WebElement, а перший аргумент — опис, який з’явиться в повідомленні про помилку. CollectionCondition.empty пишуть з назвою класу, бо в Condition теж є поле empty для порожнього поля вводу, і два статичні імпорти конфліктують; за змістом це те саме, що size(0).CollectionCondition.* brings both size conditions and collection text conditions. The predicate in noneMatch receives a raw WebElement, and the first argument is a description that appears in the error message. CollectionCondition.empty is written with the class name because Condition also has an empty field for a blank input, and the two static imports clash; in meaning it is the same as size(0).Що це. Типова задача: у таблиці користувачів знайти рядок з «Anna Koval» і натиснути в ньому «Видалити». У Selenide це один ланцюжок: $$("#users tbody tr").findBy(text("Anna Koval")).$("button.delete").click(). Рядок шукається за вмістом, а кнопка — лише всередині нього, тож тест не залежить від позиції запису в таблиці.
What it is. A typical task: find the row with Anna Koval in a users table and press Delete in it. In Selenide this is one chain: $$("#users tbody tr").findBy(text("Anna Koval")).$("button.delete").click(). The row is found by its content and the button only inside it, so the test does not depend on the record's position in the table.
Перевірка стовпців. Щоб перевірити цілий стовпець, збирають колекцію клітинок: $$("#users tbody td:nth-child(3)").shouldHave(exactTexts("Admin", "Editor", "Viewer")). Для значень атрибутів є CollectionCondition.attributes("data-status", ...), яке порівнює атрибут кожного елемента з очікуваним списком.
Checking columns. To verify a whole column you collect its cells: $$("#users tbody td:nth-child(3)").shouldHave(exactTexts("Admin", "Editor", "Viewer")). For attribute values there is CollectionCondition.attributes("data-status", ...), which compares each element's attribute with the expected list.
Коли доречно. Будь-які табличні або карткові списки: замовлення, користувачі, файли. Шаблон «знайди контейнер за текстом, потім дій усередині» стійкіший за XPath з індексами на кшталт //tr[3]/td[5]/button, який ламається від будь-якого нового рядка.
When it fits. Any tabular or card list: orders, users, files. The find-the-container-by-text-then-act-inside pattern is more robust than an indexed XPath like //tr[3]/td[5]/button, which breaks with every new row.
Підводні камені. text шукає підрядок, тож «Anna» зачепить і «Annabel»; для імен краще exactText на конкретній клітинці або унікальний атрибут рядка. Пагінація: якщо запису немає на першій сторінці, findBy просто впаде після таймауту, тому тест має спершу відфільтрувати таблицю пошуком. Віртуальний скрол рендерить лише видимі рядки, і $$ бачить тільки їх, тож кількість елементів у DOM не дорівнює кількості записів. Після видалення рядок зникає, і перевіряти треба всю колекцію, наприклад умовою noneMatch, а не звертатися до старого рядка. Методів shouldNot* у колекції немає, тож заперечення формулюють через предикат або розмір.
Pitfalls. text is a substring search, so Anna also matches Annabel; for names prefer exactText on a specific cell or a unique row attribute. Pagination: if the record is not on the first page, findBy simply fails after the timeout, so the test should filter the table with search first. Virtual scrolling renders only the visible rows and $$ sees only those, so the number of DOM elements is not the number of records. After a delete the row disappears, and you should check the whole collection, for example with noneMatch, rather than touch the old row. A collection has no shouldNot* methods, so a negation is expressed through a predicate or a size.
import static com.codeborne.selenide.CollectionCondition.*;
ElementsCollection rows = $$("#users tbody tr");
// знайти рядок за вмістом клітинки й діяти лише всередині нього
SelenideElement anna = rows.findBy(text("anna.koval@example.com"));
anna.$("td.role").shouldHave(exactText("Editor"));
anna.$("button.delete").click();
$(".confirm-dialog button.ok").click();
// рядка більше немає: жодна клітинка не містить цей email
$$("#users tbody td.email").shouldHave(noneMatch("deleted user is gone",
el -> el.getText().equals("anna.koval@example.com")));
// перевірити цілий стовпець і атрибути
$$("#users tbody td:nth-child(3)").shouldHave(exactTexts("Admin", "Viewer"));
rows.shouldHave(attributes("data-status", "active", "blocked"));
import static com.codeborne.selenide.CollectionCondition.*;
ElementsCollection rows = $$("#users tbody tr");
// find the row by a cell value and act only inside it
SelenideElement anna = rows.findBy(text("anna.koval@example.com"));
anna.$("td.role").shouldHave(exactText("Editor"));
anna.$("button.delete").click();
$(".confirm-dialog button.ok").click();
// the row is gone: no cell holds this email any more
$$("#users tbody td.email").shouldHave(noneMatch("deleted user is gone",
el -> el.getText().equals("anna.koval@example.com")));
// check a whole column and the attributes
$$("#users tbody td:nth-child(3)").shouldHave(exactTexts("Admin", "Viewer"));
rows.shouldHave(attributes("data-status", "active", "blocked"));
findBy повертає лінивий SelenideElement, тож змінна anna — це не знімок рядка, а правило пошуку. Пошук за email надійніший за ім’я, бо email унікальний. td:nth-child(3) — CSS-псевдоклас, що вибирає третю клітинку в кожному рядку. attributes приймає назву атрибута і значення в тому ж порядку, що й елементи.findBy returns a lazy SelenideElement, so the anna variable is a lookup rule rather than a snapshot of the row. Searching by email is more reliable than by name because the email is unique. td:nth-child(3) is a CSS pseudo-class that picks the third cell of every row. attributes takes the attribute name and the values in the same order as the elements.Що це. Усі чотири дії — методи самого SelenideElement, тож писати new Actions(driver) не потрібно. click() — звичайний лівий клік, hover() наводить курсор без кліку, doubleClick() — подвійний клік, contextClick() — клік правою кнопкою, який відкриває контекстне меню.
What it is. All four actions are methods of the SelenideElement itself, so there is no need to write new Actions(driver). click() is a normal left click, hover() moves the pointer over the element without clicking, doubleClick() is a double click and contextClick() is a right click that opens the context menu.
Як вони чекають. Перед кліком Selenide шукає елемент і чекає до Configuration.timeout, доки він стане clickable, тобто взаємодіяльним і не вимкненим. Тому кнопка, яка з’являється після AJAX-запиту, не потребує окремого очікування. Кожен метод повертає той самий елемент, і після дії можна одразу писати перевірку.
How they wait. Before clicking, Selenide finds the element and waits up to Configuration.timeout for it to become clickable, meaning interactable and not disabled. So a button that appears after an AJAX request needs no separate wait. Every method returns the same element, so an assertion can follow the action directly.
Приклад. Меню користувача розкривається лише при наведенні: $(".avatar").hover(), а далі клік по пункту «Вийти». Рядок таблиці відкривається на редагування подвійним кліком, а видалення файлу в менеджері документів живе в контекстному меню.
Example. A user menu only opens on hover: $(".avatar").hover(), then a click on the Log out item. A table row switches to edit mode on a double click, and deleting a file in a document manager lives in the context menu.
Підводні камені. Метод називається саме doubleClick(), а не dblclick() чи doubleclick(), — на співбесіді це люблять перевіряти. Hover тримається лише до наступного руху миші: якщо між наведенням і кліком по підменю викликати дію на іншому елементі, меню закриється. Для наведення не в центр є hover(HoverOptions.withOffset(x, y)).
Pitfalls. The method is called exactly doubleClick(), not dblclick() or doubleclick(), and interviewers like to check that. A hover lasts only until the next pointer move: if you act on another element between hovering and clicking the submenu, the menu closes. To hover away from the centre there is hover(HoverOptions.withOffset(x, y)).
$(".avatar").hover();
$(".user-menu").shouldBe(visible);
$(".user-menu a.logout").click();
$("#orders tr.order-1042").doubleClick();
$("#orders tr.order-1042 input.qty").shouldBe(visible);
$(".file[data-name='report.pdf']").contextClick();
$(".context-menu").$(byText("Delete")).click();
byText("Delete") з класу Selectors шукає елемент з точним текстом усередині контекстного меню. Селектор [data-name='report.pdf'] — звичайний CSS за атрибутом, одинарні лапки всередині подвійних Java-рядка не потребують екранування.byText("Delete") from the Selectors class finds an element with that exact text inside the context menu. The selector [data-name='report.pdf'] is a plain CSS attribute selector; single quotes inside a double-quoted Java string need no escaping.Що це. click(ClickOptions) приймає налаштування кліку. usingJavaScript() викликає клік через JS замість нативного WebDriver. withOffset(x, y) клікає не в центр, а зі зсувом від центру. holdingKeys(Keys.SHIFT) тримає клавіші під час кліку, наприклад для виділення діапазону. force() пропускає перевірку, що елемент видимий і ввімкнений. withTimeout(Duration) — не очікування елемента, а таймаут завантаження сторінки після кліку, корисний лише для дуже повільних переходів понад pageLoadTimeout.
What it is. click(ClickOptions) takes click settings. usingJavaScript() performs the click through JS instead of the native WebDriver click. withOffset(x, y) clicks at an offset from the centre. holdingKeys(Keys.SHIFT) holds keys during the click, for example to select a range. force() skips the check that the element is visible and enabled. withTimeout(Duration) is not a wait for the element but the page load timeout after the click, useful only for very slow navigations beyond pageLoadTimeout.
Коли JS-клік виправданий. Коли нативний клік фізично неможливий, хоча користувач натиснути може: кастомний елемент без розмірів, старий браузер з багами нативного кліку. Глобально те саме вмикає Configuration.clickViaJs, але це майже завжди погана ідея.
When a JS click is justified. When a native click is physically impossible although a user can press the element: a custom element without size, or an old browser with native click bugs. Configuration.clickViaJs turns the same thing on globally, but that is almost always a bad idea.
Приклад. Клік зі зсувом потрібен для слайдера ціни: $(".range").click(withOffset(80, 0)) ставить повзунок правіше центру. Shift-клік по рядках таблиці виділяє кілька записів.
Example. A click with an offset is needed for a price slider: $(".range").click(withOffset(80, 0)) moves the handle right of centre. A Shift click on table rows selects several records.
Підводні камені. JS-клік натискає навіть на кнопку, яку перекрив банер cookies, модалка чи прозорий шар. Нативний клік у такому разі впаде з ElementClickInterceptedException, і це справжній баг, який бачить користувач. Тому «виправити» таке падіння через usingJavaScript() або force() означає сховати дефект. Спершу з’ясуй, що перекриває елемент: закрий банер, дочекайся зникнення лоадера або прокрути елемент у центр.
Pitfalls. A JS click presses even a button covered by a cookie banner, a modal or a transparent layer. A native click would fail with ElementClickInterceptedException, and that is a real bug the user sees. So fixing such a failure with usingJavaScript() or force() means hiding a defect. First find out what covers the element: close the banner, wait for the loader to disappear or scroll the element to the centre.
import static com.codeborne.selenide.ClickOptions.*;
// зсув на 80 px праворуч від центру слайдера
$(".price-range").click(withOffset(80, 0));
// виділити діапазон рядків із затиснутим Shift
$$("#files tr").get(0).click();
$$("#files tr").get(4).click(usingDefaultMethod().holdingKeys(Keys.SHIFT));
// JS-клік: лише коли нативний неможливий, а не щоб обійти перекриття
$("#legacy-widget .trigger").click(usingJavaScript());
import static com.codeborne.selenide.ClickOptions.*;
// move 80 px right of the slider centre
$(".price-range").click(withOffset(80, 0));
// select a range of rows with Shift held down
$$("#files tr").get(0).click();
$$("#files tr").get(4).click(usingDefaultMethod().holdingKeys(Keys.SHIFT));
// JS click: only when a native one is impossible, never to bypass an overlay
$("#legacy-widget .trigger").click(usingJavaScript());
withOffset, usingDefaultMethod і usingJavaScript повертають незмінний об’єкт ClickOptions, а методи holdingKeys, offset чи force створюють його копію з новим параметром. Зсув рахується від центру елемента в пікселях, від’ємні значення зсувають ліворуч і вгору.withOffset, usingDefaultMethod and usingJavaScript return an immutable ClickOptions object, and the methods holdingKeys, offset or force create a copy with a new parameter. The offset is measured in pixels from the element centre; negative values move left and up.setValue і val. setValue(text) — основний спосіб заповнити поле: він очищає input чи textarea, вводить текст через sendKeys і генерує подію change. val(text) — повний синонім, назва запозичена з jQuery. Якщо потрібно ввести значення одним JS-викликом, є setValue(withText("...").usingMethod(SetValueMethod.JS)) або глобальний Configuration.fastSetValue.
setValue and val. setValue(text) is the main way to fill a field: it clears the input or textarea, enters the text through sendKeys and fires a change event. val(text) is a full synonym, a name borrowed from jQuery. To set the value in a single JS call there is setValue(withText("...").usingMethod(SetValueMethod.JS)) or the global Configuration.fastSetValue.
type. type(text) імітує живого користувача: друкує по символу з паузою 200 мс і за замовчуванням спершу очищає поле. Це потрібно для автопідказок і полів, які реагують на кожне натискання. Паузу й очищення налаштовує TypeOptions.text(...).withDelay(...).clearFirst(false).
type. type(text) imitates a real user: it prints one character at a time with a 200 ms pause and clears the field first by default. It is meant for autosuggest and for fields that react to every keystroke. The pause and the clearing are tuned with TypeOptions.text(...).withDelay(...).clearFirst(false).
append і sendKeys. append(text) дописує текст у кінець без очищення і теж генерує change. sendKeys — метод Selenium без очищення і без події change, його беруть для спецклавіш.
append and sendKeys. append(text) adds text to the end without clearing and also fires change. sendKeys is the Selenium method with no clearing and no change event; it is used for special keys.
Підводні камені. type з довгим текстом робить тест повільним, тож для звичайних полів він зайвий. JS-режим швидкий, але валідація, яка слухає натискання клавіш, може не спрацювати, і тест пройде там, де користувач побачить помилку. Для паролів додавай sensitive() у SetValueOptions або TypeOptions: у звіті значення буде замасковане.
Pitfalls. type with a long text makes a test slow, so it is unnecessary for ordinary fields. JS mode is fast, but validation that listens to keystrokes may not fire, and the test passes where the user would see an error. For passwords add sensitive() to SetValueOptions or TypeOptions: the value is masked in the report.
import static com.codeborne.selenide.SetValueOptions.withText;
$("#email").setValue("anna@example.com"); // очистити, ввести, change
$("#email").val("anna@example.com"); // те саме
$("#password").setValue(withText("s3cret").sensitive()); // у звіті буде маска
// автопідказка: друкуємо по літері
$("#city").type(TypeOptions.text("Льві").withDelay(Duration.ofMillis(100)));
$$(".suggestions li").findBy(text("Львів")).click();
$("#comment").append(" (терміново)"); // дописати в кінець
$("#search").sendKeys(Keys.ARROW_DOWN); // спецклавіша без change
import static com.codeborne.selenide.SetValueOptions.withText;
$("#email").setValue("anna@example.com"); // clear, type, change
$("#email").val("anna@example.com"); // the same
$("#password").setValue(withText("s3cret").sensitive()); // masked in the report
// autosuggest: type letter by letter
$("#city").type(TypeOptions.text("Lvi").withDelay(Duration.ofMillis(100)));
$$(".suggestions li").findBy(text("Lviv")).click();
$("#comment").append(" (urgent)"); // add to the end
$("#search").sendKeys(Keys.ARROW_DOWN); // a special key, no change event
withText(...) створює SetValueOptions, а sensitive() підміняє текст, який бачить звіт, маскою з зірочок. TypeOptions.text(...) — builder для type: withDelay задає паузу між символами, clearFirst(false) вимикає очищення. findBy(text(...)) повертає першу підказку, що містить потрібний текст.withText(...) creates SetValueOptions, and sensitive() replaces the text shown in the report with a mask of asterisks. TypeOptions.text(...) is a builder for type: withDelay sets the pause between characters and clearFirst(false) turns off clearing. findBy(text(...)) returns the first suggestion containing the given text.Варіанти. Перший — $("#name").clear(): у ядрі Selenide він викликає стандартний WebElement.clear() і після цього сам генерує події change і blur, чого чистий Selenium не робить. Другий — setValue(""): поле очищається, а порожній рядок нічого не додає. Третій — модуль selenide-clear-with-shortcut: з ним clear() працює як людина, виділяє весь текст і натискає Backspace.
The variants. The first is $("#name").clear(): in Selenide core it calls the standard WebElement.clear() and then fires change and blur events itself, which plain Selenium does not do. The second is setValue(""): the field is cleared and the empty string adds nothing. The third is the selenide-clear-with-shortcut module: with it, clear() behaves like a person, selecting all the text and pressing Backspace.
Чому бувають проблеми. Керовані поля React і Vue тримають значення у стані компонента. WebElement.clear() змінює DOM-властивість напряму, не генеруючи клавіатурних подій, тож фреймворк може не дізнатися про зміну і при наступному рендері повернути старе значення або не зняти помилку валідації.
Why problems happen. Controlled inputs in React and Vue keep their value in component state. WebElement.clear() changes the DOM property directly without keyboard events, so the framework may never learn about the change and restore the old value on the next render, or fail to clear a validation error.
Приклад. Форма профілю з React: після clear() поле виглядає порожнім, але кнопка «Зберегти» відправляє старе ім’я. Підключення модуля з гарячими клавішами розв’язує це без змін у тестах.
Example. A React profile form: after clear() the field looks empty, but the Save button submits the old name. Adding the shortcut module solves this without touching the tests.
Підводні камені. Завжди перевіряй результат, а не лише дію: shouldHave(exactValue("")) або shouldBe(empty). Якщо поле має маску, наприклад телефону, воно після очищення може одразу підставити префікс, і перевірка на порожнечу впаде цілком обґрунтовано.
Pitfalls. Always check the result, not just the action: shouldHave(exactValue("")) or shouldBe(empty). If the field has a mask, a phone number for example, it may insert a prefix right after clearing, and the emptiness check will then fail for a good reason.
// 1. ядро Selenide: WebElement.clear() + події change і blur
$("#name").clear();
// 2. через setValue з порожнім рядком
$("#name").setValue("");
// 3. з модулем selenide-clear-with-shortcut той самий clear()
// виділяє весь текст і натискає Backspace
$("#name").clear();
$("#name").shouldHave(exactValue(""));
$("#name-error").shouldNotBe(visible);
// 1. Selenide core: WebElement.clear() + change and blur events
$("#name").clear();
// 2. via setValue with an empty string
$("#name").setValue("");
// 3. with the selenide-clear-with-shortcut module the same clear()
// selects all the text and presses Backspace
$("#name").clear();
$("#name").shouldHave(exactValue(""));
$("#name-error").shouldNotBe(visible);
selenide-clear-with-shortcut підключається як звичайна тестова залежність із групою com.codeborne і підміняє команду clear глобально, код тестів не змінюється. exactValue("") перевіряє атрибут value на точний збіг, на відміну від value(...), яка шукає підрядок.selenide-clear-with-shortcut module is added as an ordinary test dependency with the com.codeborne group and replaces the clear command globally, so test code stays the same. exactValue("") checks the value attribute for an exact match, unlike value(...), which looks for a substring.Що є. Для частих клавіш є готові методи: pressEnter(), pressTab() і pressEscape(). Вони перевіряють, що елемент видимий, і повертають його ж, тож після них можна продовжити ланцюжок. Для будь-якої іншої клавіші є press(Keys...), яка теж ланцюгова. Класичний sendKeys(Keys.ENTER) із Selenium теж працює: для Enter вибирай pressEnter() чи sendKeys(Keys.ENTER), результат у браузері однаковий, різниця лише в ланцюжку і читабельності.
What exists. Common keys have ready methods: pressEnter(), pressTab() and pressEscape(). They check that the element is visible and return it, so you can continue the chain. For any other key there is press(Keys...), which is chainable too. The classic Selenium sendKeys(Keys.ENTER) also works: for Enter choose pressEnter() or sendKeys(Keys.ENTER); the browser sees the same thing, the difference is only chaining and readability.
Комбінації. Keys.chord(Keys.CONTROL, "a") збирає натискання, яке затискає Ctrl, натискає A і відпускає все разом. Його передають у sendKeys або press. Для складніших послідовностей, наприклад утримати Shift і натиснути кілька стрілок, є actions() — звичайний Selenium Actions з поточним драйвером.
Combinations. Keys.chord(Keys.CONTROL, "a") builds a keystroke that holds Ctrl, presses A and releases everything together. You pass it to sendKeys or press. For more complex sequences, such as holding Shift and pressing several arrows, there is actions(), a regular Selenium Actions bound to the current driver.
Приклад. Пошук запускається Enter, модальне вікно закривається Escape, а в редакторі Ctrl+A і Delete очищають увесь текст.
Example. Search is started with Enter, a modal closes on Escape, and in an editor Ctrl+A followed by Delete clears all the text.
Підводні камені. На macOS «виділити все» — це Cmd+A, тобто Keys.COMMAND, а не Control, і тест, написаний на Windows, на Mac-агенті тихо нічого не виділить. Клавіші йдуть у той елемент, на якому викликано метод, тож якщо фокус з поля вже пішов, натискання Escape на сторінці може не закрити модалку.
Pitfalls. On macOS select all is Cmd+A, that is Keys.COMMAND rather than Control, and a test written on Windows silently selects nothing on a Mac agent. Keys go to the element the method was called on, so if focus has already left the field, pressing Escape may not close the modal.
$("#search").setValue("selenide").pressEnter();
$(".modal input").pressEscape();
$("#editor").press(Keys.END).press(Keys.BACK_SPACE);
// Ctrl+A на Windows і Linux, Cmd+A на macOS
Keys modifier = System.getProperty("os.name").contains("Mac") ? Keys.COMMAND : Keys.CONTROL;
$("#editor").sendKeys(Keys.chord(modifier, "a"), Keys.DELETE);
// виділити три символи ліворуч
actions().keyDown(Keys.SHIFT)
.sendKeys(Keys.LEFT, Keys.LEFT, Keys.LEFT)
.keyUp(Keys.SHIFT)
.perform();
$("#search").setValue("selenide").pressEnter();
$(".modal input").pressEscape();
$("#editor").press(Keys.END).press(Keys.BACK_SPACE);
// Ctrl+A on Windows and Linux, Cmd+A on macOS
Keys modifier = System.getProperty("os.name").contains("Mac") ? Keys.COMMAND : Keys.CONTROL;
$("#editor").sendKeys(Keys.chord(modifier, "a"), Keys.DELETE);
// select three characters to the left
actions().keyDown(Keys.SHIFT)
.sendKeys(Keys.LEFT, Keys.LEFT, Keys.LEFT)
.keyUp(Keys.SHIFT)
.perform();
Keys — перелік спецклавіш Selenium. Keys.chord(...) повертає рядок, у якому модифікатор затиснутий до кінця комбінації. actions() — статичний метод Selenide, що повертає Actions; ланцюжок нічого не робить, доки не викликано perform().Keys is the Selenium enum of special keys. Keys.chord(...) returns a string where the modifier stays held until the end of the combination. actions() is a static Selenide method returning Actions; the chain does nothing until perform() is called.Select. Для нативного <select> є три способи вибору. selectOption("Україна") шукає опцію за видимим текстом, selectOption(2) — за індексом від нуля, а selectOptionByValue("ua") — за атрибутом value. Є ще selectOptionContainingText для часткового збігу. Усі методи приймають кілька значень для множинного вибору. Прочитати результат можна через getSelectedOption(), який повертає елемент опції, і перевірити його звичайними умовами.
Select. For a native <select> there are three ways to choose. selectOption("Ukraine") looks the option up by visible text, selectOption(2) by zero-based index, and selectOptionByValue("ua") by the value attribute. There is also selectOptionContainingText for a partial match. All of them accept several values for multi-select. You read the result with getSelectedOption(), which returns the option element, and check it with ordinary conditions.
Checkbox і radio. setSelected(true) ставить прапорець, лише якщо він ще не стоїть, а setSelected(false) знімає його, тож метод ідемпотентний, на відміну від click(). Для групи radio $(byName("delivery")).selectRadio("courier") знаходить кнопку з потрібним value і клікає по ній. Стан перевіряють умовою checked.
Checkbox and radio. setSelected(true) ticks a box only if it is not ticked yet, and setSelected(false) unticks it, so the method is idempotent, unlike click(). For a radio group, $(byName("delivery")).selectRadio("courier") finds the button with the given value and clicks it. State is checked with the checked condition.
Підводні камені. Умова selectedText не про вибрану опцію: вона перевіряє текст, виділений мишею. Кастомні дропдауни на div і li методи selectOption не підтримують, там відкривають список кліком і клікають по пункту. setSelected відмовиться працювати з невидимим input, а стилізовані прапорці часто ховають справжній input, тоді клікають по label.
Pitfalls. The selectedText condition is not about the selected option: it checks text highlighted with the mouse. Custom dropdowns built from div and li are not supported by selectOption; there you open the list with a click and click the item. setSelected refuses to work with an invisible input, and styled checkboxes often hide the real input, in which case you click the label.
$("#country").selectOption("Ukraine");
$("#country").getSelectedOption().shouldHave(exactText("Ukraine"), value("ua"));
$("#country").selectOptionByValue("pl");
$("#country").selectOption(0);
$("#newsletter").setSelected(true);
$("#newsletter").shouldBe(checked);
$(byName("delivery")).selectRadio("courier");
$("input[name=delivery][value=courier]").shouldBe(checked);
$(".dropdown-toggle").click();
$$(".dropdown-menu li").findBy(exactText("Express")).click();
getSelectedOption() повертає SelenideElement обраного <option>, тож до нього застосовні exactText і value з очікуванням. byName з Selectors шукає за атрибутом name, а selectRadio перебирає всі знайдені кнопки групи й клікає ту, в якої збігся value.getSelectedOption() returns a SelenideElement for the chosen <option>, so exactText and value apply to it with waiting. byName from Selectors searches by the name attribute, and selectRadio goes through every matching button in the group and clicks the one whose value matches.Що відбувається без скролу. Нативний click() у WebDriver сам прокручує елемент у видиму область, тож кнопка внизу довгої сторінки зазвичай натискається без підготовки. Скрол потрібен тоді, коли автоматичного недостатньо: елемент перекриває липка шапка, лінивий список дозавантажується лише при прокрутці, або треба зробити скріншот конкретного блоку.
What happens without scrolling. A native WebDriver click() scrolls the element into view by itself, so a button at the bottom of a long page usually gets clicked without preparation. Scrolling is needed when the automatic one is not enough: a sticky header covers the element, a lazy list only loads more on scroll, or you need a screenshot of a specific block.
Методи. scrollTo() прокручує вікно так, що верх елемента опиняється біля верху viewport. scrollIntoView(instant().block(center)) викликає браузерний scrollIntoView з параметрами. scrollIntoCenter() з версії 7.6 — коротка форма для центрування. Старі перевантаження scrollIntoView(true) і scrollIntoView("{...}") позначені як deprecated на користь ScrollIntoViewOptions.
The methods. scrollTo() scrolls the window so that the top of the element ends up near the top of the viewport. scrollIntoView(instant().block(center)) calls the browser scrollIntoView with options. scrollIntoCenter(), available since 7.6, is the short form for centring. The old overloads scrollIntoView(true) and scrollIntoView("{...}") are deprecated in favour of ScrollIntoViewOptions.
Приклад. На сторінці товару липка шапка висотою 80 px. Після scrollTo() кнопка «Купити» ховається під нею, і клік падає з ElementClickInterceptedException. scrollIntoCenter() ставить кнопку посередині екрана, і клік проходить. Тому поширений рецепт scrollTo().click() правильний за духом, але на реальних сторінках центрування надійніше.
Example. A product page has an 80 px sticky header. After scrollTo() the Buy button hides under it, and the click fails with ElementClickInterceptedException. scrollIntoCenter() puts the button in the middle of the screen and the click goes through. So the common recipe scrollTo().click() is right in spirit, but on real pages centring is more reliable.
Підводні камені. Скрол усередині прокручуваного контейнера, наприклад таблиці з власним скролбаром, scrollTo() не зробить, бо рухає вікно. Там допоможе scrollIntoView, який прокручує всіх предків. Анімований scroll-behavior: smooth методи Selenide ігнорують і скролять миттєво, тож окремо чекати завершення прокрутки не треба.
Pitfalls. scrollTo() will not scroll inside a scrollable container such as a table with its own scrollbar, because it moves the window. scrollIntoView helps there, since it scrolls every ancestor. Selenide methods ignore an animated scroll-behavior: smooth and scroll instantly, so there is no need to wait for the scroll to finish.
import static com.codeborne.selenide.ScrollIntoViewOptions.Block.center;
import static com.codeborne.selenide.ScrollIntoViewOptions.instant;
// кнопка може сховатися під липкою шапкою після scrollTo()
$("#buy").scrollIntoCenter().click();
// те саме явними опціями
$("#buy").scrollIntoView(instant().block(center)).click();
// рядок у таблиці з власним скролбаром
$("#orders-table tr.order-1042").scrollIntoView(instant().block(center))
.shouldBe(visible);
import static com.codeborne.selenide.ScrollIntoViewOptions.Block.center;
import static com.codeborne.selenide.ScrollIntoViewOptions.instant;
// the button may hide under a sticky header after scrollTo()
$("#buy").scrollIntoCenter().click();
// the same with explicit options
$("#buy").scrollIntoView(instant().block(center)).click();
// a row in a table with its own scrollbar
$("#orders-table tr.order-1042").scrollIntoView(instant().block(center))
.shouldBe(visible);
instant() створює ScrollIntoViewOptions з миттєвою прокруткою, а block(center) вирівнює елемент по вертикалі в центр. Block — вкладений enum зі значеннями start, center, end і nearest. Усі методи скролу повертають елемент, тож клік чи перевірка йдуть одразу в ланцюжку.instant() creates ScrollIntoViewOptions with instant scrolling, and block(center) aligns the element vertically to the centre. Block is a nested enum with the values start, center, end and nearest. All scroll methods return the element, so a click or an assertion follows in the same chain.Що це. У Selenide 7 є один метод — dragAndDrop(DragAndDropOptions). Ціль задають через DragAndDropOptions.to("#basket") з CSS-селектором або to(element). Старі методи на кшталт dragAndDropTo(...) прибрали у 7.0, тож код із давніх статей не скомпілюється.
What it is. Selenide 7 has a single method, dragAndDrop(DragAndDropOptions). The target is given with DragAndDropOptions.to("#basket") using a CSS selector, or with to(element). Old methods such as dragAndDropTo(...) were removed in 7.0, so code from older articles will not compile.
Два способи. За замовчуванням to(...) використовує JavaScript: скрипт генерує HTML5-події dragstart, dragenter, dragover, drop і dragend з фейковим dataTransfer. Це надійно працює для нативного HTML5 drag and drop. Метод .usingSeleniumActions() перемикає на Selenium Actions: натиснути мишу, перемістити, відпустити.
Two methods. By default to(...) uses JavaScript: a script fires the HTML5 events dragstart, dragenter, dragover, drop and dragend with a fake dataTransfer. That works reliably for native HTML5 drag and drop. .usingSeleniumActions() switches to Selenium Actions: press the mouse, move, release.
Коли що. Якщо сторінка побудована на атрибуті draggable і подіях drag, бери JS-режим. Якщо бібліотека сортування слухає mousedown, mousemove і mouseup або pointer-події, JS-скрипт їх не згенерує, і потрібен режим Actions. Перед перетягуванням Selenide перевіряє, що ціль видима.
Which to use. If the page relies on the draggable attribute and drag events, use JS mode. If a sorting library listens to mousedown, mousemove and mouseup or pointer events, the JS script will not fire them and you need Actions mode. Before dragging, Selenide checks that the target is visible.
Підводні камені. Деякі бібліотеки реагують лише на рух у кілька кроків і з мінімальною відстанню, тоді й один dragAndDrop з Actions може не спрацювати, і доводиться писати actions() з кількома moveByOffset. Завжди перевіряй результат: порядок елементів у списку чи новий вміст кошика, а не відсутність винятку.
Pitfalls. Some libraries react only to movement in several steps and over a minimum distance, and then even dragAndDrop with Actions may fail, so you write actions() with several moveByOffset calls. Always check the outcome, such as the order of items in the list or the new basket content, rather than the absence of an exception.
import static com.codeborne.selenide.DragAndDropOptions.to;
// нативний HTML5 drag and drop: JS-режим за замовчуванням
$("#product-42").dragAndDrop(to("#basket"));
$("#basket .item").shouldHave(text("Product 42"));
// бібліотека на подіях миші: режим Actions
$$("#tasks li").get(0).dragAndDrop(to($$("#tasks li").get(2)).usingSeleniumActions());
$$("#tasks li").shouldHave(texts("Write tests", "Review", "Fix bug"));
// бібліотека, якій потрібен рух у кілька кроків
actions().clickAndHold($("#card-7"))
.moveByOffset(20, 0).moveByOffset(150, 40)
.release($("#column-done"))
.perform();
import static com.codeborne.selenide.DragAndDropOptions.to;
// native HTML5 drag and drop: JS mode by default
$("#product-42").dragAndDrop(to("#basket"));
$("#basket .item").shouldHave(text("Product 42"));
// a library built on mouse events: Actions mode
$$("#tasks li").get(0).dragAndDrop(to($$("#tasks li").get(2)).usingSeleniumActions());
$$("#tasks li").shouldHave(texts("Write tests", "Review", "Fix bug"));
// a library that needs movement in several steps
actions().clickAndHold($("#card-7"))
.moveByOffset(20, 0).moveByOffset(150, 40)
.release($("#column-done"))
.perform();
to(...) — статична фабрика, що повертає незмінний запис DragAndDropOptions з методом JS, а usingSeleniumActions() повертає його копію з методом Actions. texts(...) перевіряє порядок елементів колекції. clickAndHold, moveByOffset і release — звичайні кроки Selenium Actions, які виконуються лише після perform().to(...) is a static factory returning an immutable DragAndDropOptions record with the JS method, and usingSeleniumActions() returns a copy with the Actions method. texts(...) checks the order of the collection items. clickAndHold, moveByOffset and release are ordinary Selenium Actions steps that run only after perform().Що це. Статичний executeJavaScript(js, args...) виконує скрипт у поточному вікні й повертає результат, приведений до потрібного типу через generic <T>. Аргументи доступні в скрипті як arguments[0], arguments[1], і туди можна передати SelenideElement — у скрипт потрапить відповідний DOM-вузол. Для асинхронного коду є executeAsyncJavaScript: останній аргумент — callback, який треба викликати, щоб повернути результат.
What it is. The static executeJavaScript(js, args...) runs a script in the current window and returns the result cast to the needed type through the generic <T>. Arguments are available in the script as arguments[0], arguments[1], and you can pass a SelenideElement there: the script receives the matching DOM node. For asynchronous code there is executeAsyncJavaScript: the last argument is a callback you must call to return the result.
Коли доречно. Читати стан, якого немає в DOM: значення з localStorage, прапорець готовності застосунку, дані аналітики в window.dataLayer. Налаштовувати середовище перед тестом: вимкнути анімації стилем, підставити feature flag. Діагностувати: вивести в лог стан компонента при падінні.
When it fits. Reading state that is not in the DOM: a value in localStorage, an application readiness flag, analytics data in window.dataLayer. Preparing the environment before a test: turning animations off with a style, setting a feature flag. Diagnostics: logging a component state on failure.
Приклад. Тест аналітики після кліку «Купити» читає window.dataLayer і перевіряє, що подія purchase містить правильну суму. Інакше це довелося б перевіряти лише вручну в DevTools.
Example. An analytics test clicks Buy, reads window.dataLayer and checks that the purchase event carries the right amount. Otherwise this could only be checked by hand in DevTools.
Коли шкодить. Коли JS замінює дію користувача: arguments[0].value = ... замість setValue, arguments[0].click() замість click, прибирання перекриття через display = none. Такий тест проходить, хоча користувач не зміг би виконати дію. Результат скрипта не чекає: якщо стан ще не встановився, отримаєш старе значення, тому для очікування загортай читання в умову, а не в sleep.
When it hurts. When JS replaces a user action: arguments[0].value = ... instead of setValue, arguments[0].click() instead of click, removing an overlay with display = none. Such a test passes although a user could not have done the action. A script result does not wait: if the state is not settled yet you get the old value, so wrap the read in a condition for waiting, not in sleep.
// прочитати значення з localStorage
String token = executeJavaScript("return localStorage.getItem('authToken');");
// передати елемент як аргумент
Long width = executeJavaScript("return arguments[0].getBoundingClientRect().width;", $("#banner"));
// асинхронний скрипт: callback — останній аргумент
String status = executeAsyncJavaScript(
"const done = arguments[arguments.length - 1];" +
"fetch('/api/health').then(r => r.text()).then(done);");
// дочекатися стану застосунку без sleep
Selenide.Wait().until(d -> Boolean.TRUE.equals(
executeJavaScript("return window.appReady === true;")));
// read a value from localStorage
String token = executeJavaScript("return localStorage.getItem('authToken');");
// pass an element as an argument
Long width = executeJavaScript("return arguments[0].getBoundingClientRect().width;", $("#banner"));
// an async script: the callback is the last argument
String status = executeAsyncJavaScript(
"const done = arguments[arguments.length - 1];" +
"fetch('/api/health').then(r => r.text()).then(done);");
// wait for application state without sleep
Selenide.Wait().until(d -> Boolean.TRUE.equals(
executeJavaScript("return window.appReady === true;")));
Long або Double, рядки як String, об’єкти як Map. У скрипті обов’язковий return, інакше повернеться null. Selenide.Wait() повертає SelenideWait з поточним таймаутом, а until повторює лямбду, доки вона не дасть true.Long or Double, strings as String and objects as Map. The script needs a return, otherwise you get null. Selenide.Wait() returns a SelenideWait with the current timeout, and until repeats the lambda until it yields true.Що це. iframe — окремий документ усередині сторінки, і WebDriver бачить у кожен момент лише один контекст. Поки драйвер у головному документі, $("#card-number") всередині фрейму платіжної форми не знайдеться, скільки не чекай. Перемикання дає switchTo() з класу Selenide.
What it is. An iframe is a separate document inside the page, and WebDriver sees only one context at a time. While the driver is in the main document, $("#card-number") inside a payment form frame will never be found, however long you wait. Switching is done with switchTo() from the Selenide class.
Методи. switchTo().frame("payment") шукає фрейм за name або id, frame(0) — за індексом, frame($("iframe.stripe")) — за елементом. На відміну від Selenium, ці методи чекають до таймауту, поки фрейм з’явиться, і мають перевантаження з власним Duration. parentFrame() піднімає на рівень вище, defaultContent() повертає в корінь сторінки. Для вкладених фреймів є innerFrame("outer", "inner"): він спершу йде в корінь, а потім по черзі в кожен фрейм зі списку.
The methods. switchTo().frame("payment") finds a frame by name or id, frame(0) by index and frame($("iframe.stripe")) by element. Unlike Selenium, these methods wait up to the timeout for the frame to appear, and they have overloads with a custom Duration. parentFrame() goes one level up and defaultContent() returns to the page root. For nested frames there is innerFrame("outer", "inner"): it first goes to the root and then into each listed frame in turn.
Приклад. Оплата через вбудований віджет: перейти у фрейм, заповнити номер картки, повернутися в defaultContent() і перевірити статус замовлення в основній сторінці.
Example. Paying through an embedded widget: switch into the frame, fill in the card number, go back to defaultContent() and check the order status in the main page.
Підводні камені. Найчастіша помилка — забути повернутися: наступний тест чи крок шукає елементи сторінки всередині фрейму й падає за таймаутом з незрозумілим повідомленням. Тому роботу з фреймом варто загорнути в метод Page Object, який завжди завершується defaultContent(), найкраще у finally. Індекси фреймів різні браузери можуть рахувати по-різному, тож надійніше звертатися за іменем чи елементом. Після перезавантаження сторінки контекст фрейму теж скидається.
Pitfalls. The most common mistake is forgetting to switch back: the next step or test looks for page elements inside the frame and fails on timeout with a confusing message. So wrap frame work in a Page Object method that always ends with defaultContent(), ideally in finally. Browsers may count frame indexes differently, so addressing by name or element is more reliable. A page reload also resets the frame context.
public class CheckoutPage {
public void payWithCard(String number, String expiry) {
switchTo().frame($("iframe[title='Secure payment']"));
try {
$("#card-number").setValue(number);
$("#card-expiry").setValue(expiry);
$("button[type=submit]").click();
} finally {
switchTo().defaultContent(); // завжди повертаємось у корінь сторінки
}
$(".order-status").shouldHave(text("Paid"));
}
}
// вкладені фрейми: корінь -> editor -> preview
switchTo().innerFrame("editor", "preview");
public class CheckoutPage {
public void payWithCard(String number, String expiry) {
switchTo().frame($("iframe[title='Secure payment']"));
try {
$("#card-number").setValue(number);
$("#card-expiry").setValue(expiry);
$("button[type=submit]").click();
} finally {
switchTo().defaultContent(); // always return to the page root
}
$(".order-status").shouldHave(text("Paid"));
}
}
// nested frames: root -> editor -> preview
switchTo().innerFrame("editor", "preview");
switchTo() повертає SelenideTargetLocator — обгортку над Selenium TargetLocator з очікуваннями. frame(WebElement) приймає SelenideElement, бо той реалізує WebElement. Блок try/finally гарантує повернення в корінь навіть тоді, коли перевірка всередині фрейму впала.switchTo() returns a SelenideTargetLocator, a wrapper over the Selenium TargetLocator with waiting. frame(WebElement) accepts a SelenideElement because it implements WebElement. The try/finally block guarantees a return to the root even if an assertion inside the frame failed.Що це. Кожна вкладка чи вікно для WebDriver — окремий handle, і команди йдуть лише в поточне. Коли посилання з target=_blank відкриває нову вкладку, драйвер лишається в старій. Перемикання робить switchTo().window(...), який приймає індекс від нуля або рядок: ім’я вікна, його handle чи заголовок сторінки.
What it is. Every tab or window is a separate handle for WebDriver, and commands go only to the current one. When a link with target=_blank opens a new tab, the driver stays in the old one. Switching is done with switchTo().window(...), which takes a zero-based index or a string: the window name, its handle or the page title.
Чим Selenide кращий. window(1) і window("Invoice") чекають до таймауту, доки вкладка з’явиться, тож не треба писати цикл по getWindowHandles(). Є перевантаження з власним Duration для повільних попапів. Кількість вікон перевіряють умовою webdriver().shouldHave(numberOfWindows(2)). Нову порожню вкладку відкриває switchTo().newWindow(WindowType.TAB), а closeWindow() закриває поточну.
Why Selenide is better here. window(1) and window("Invoice") wait up to the timeout for the tab to appear, so there is no loop over getWindowHandles(). There are overloads with a custom Duration for slow popups. The number of windows is checked with webdriver().shouldHave(numberOfWindows(2)). switchTo().newWindow(WindowType.TAB) opens a new empty tab, and closeWindow() closes the current one.
Приклад. Кнопка «Відкрити рахунок» відкриває PDF-перегляд у новій вкладці. Тест перевіряє, що вкладок дві, перемикається на другу, перевіряє заголовок і адресу, закриває її і повертається на першу.
Example. The Open invoice button opens a PDF preview in a new tab. The test checks there are two tabs, switches to the second, checks the title and URL, closes it and returns to the first.
Підводні камені. Після closeWindow() драйвер не повертається сам: він вказує на закрите вікно, і наступна команда впаде з NoSuchWindowException, тому одразу роби switchTo().window(0). Порядок індексів у різних браузерах не гарантований, тож для стабільності краще запам’ятати handle вихідної вкладки через getWebDriver().getWindowHandle() і повертатися за ним. Закриття останнього вікна фактично завершує сесію браузера.
Pitfalls. After closeWindow() the driver does not move on its own: it points at a closed window, and the next command fails with NoSuchWindowException, so call switchTo().window(0) right away. Index order is not guaranteed across browsers, so for stability remember the original tab handle with getWebDriver().getWindowHandle() and return by it. Closing the last window effectively ends the browser session.
String mainTab = WebDriverRunner.getWebDriver().getWindowHandle();
$("#open-invoice").click(); // посилання з target=_blank
webdriver().shouldHave(numberOfWindows(2));
switchTo().window(1);
webdriver().shouldHave(urlContaining("/invoices/1042"));
$("h1").shouldHave(text("Invoice #1042"));
closeWindow(); // драйвер ще дивиться на закриту вкладку
switchTo().window(mainTab);
$("#open-invoice").shouldBe(visible);
String mainTab = WebDriverRunner.getWebDriver().getWindowHandle();
$("#open-invoice").click(); // a link with target=_blank
webdriver().shouldHave(numberOfWindows(2));
switchTo().window(1);
webdriver().shouldHave(urlContaining("/invoices/1042"));
$("h1").shouldHave(text("Invoice #1042"));
closeWindow(); // the driver still points at the closed tab
switchTo().window(mainTab);
$("#open-invoice").shouldBe(visible);
getWindowHandle() — звичайний метод Selenium, що повертає рядковий ідентифікатор поточної вкладки. numberOfWindows і urlContaining — умови з WebDriverConditions, які перевіряють стан браузера з очікуванням. closeWindow() — статичний метод Selenide, обгортка над WebDriver.close().getWindowHandle() is a plain Selenium method returning the string id of the current tab. numberOfWindows and urlContaining are conditions from WebDriverConditions that check browser state with waiting. closeWindow() is a static Selenide method wrapping WebDriver.close().Що це. Нативні діалоги браузера блокують сторінку: поки alert відкритий, жоден клік чи пошук елемента не виконається. Selenide дає три статичні методи, і кожен повертає текст діалогу. confirm() натискає OK, dismiss() — Cancel, prompt("текст") вводить рядок у поле prompt і натискає OK.
What it is. Native browser dialogs block the page: while an alert is open, no click or element lookup will run. Selenide provides three static methods, and each returns the dialog text. confirm() presses OK, dismiss() presses Cancel, and prompt("text") types a string into the prompt field and presses OK.
Варіанти з перевіркою. confirm("Delete order?") і dismiss("Delete order?") спершу порівнюють текст діалогу з очікуваним і падають, якщо він інший. Для prompt є prompt(expectedText, inputText). Таймаут і очікуваний текст можна задати через ModalOptions.withExpectedText(...) і withTimeout(Duration). Усі методи чекають появи діалогу. Якщо текст треба лише прочитати, switchTo().alert() теж чекає діалог до таймауту і повертає звичайний Selenium Alert з getText(), accept() і dismiss().
Variants with a check. confirm("Delete order?") and dismiss("Delete order?") first compare the dialog text with the expected one and fail if it differs. For prompt there is prompt(expectedText, inputText). A timeout and expected text can be set via ModalOptions.withExpectedText(...) and withTimeout(Duration). Every method waits for the dialog to appear. If you only need to read the text, switchTo().alert() also waits up to the timeout and returns a plain Selenium Alert with getText(), accept() and dismiss().
Приклад. Кнопка видалення показує confirm. Позитивний тест підтверджує і перевіряє, що рядок зник, негативний скасовує і перевіряє, що рядок лишився. Це дві різні поведінки, і обидві варто покрити.
Example. The delete button shows a confirm. The positive test accepts it and checks the row is gone; the negative test cancels and checks the row is still there. These are two different behaviours, and both deserve a test.
Підводні камені. Ці методи працюють лише з нативними window.alert, confirm і prompt. Модальне вікно, намальоване на div, — звичайний елемент сторінки, і його закривають кліком. Діалог beforeunload при виході зі сторінки поводиться по-різному в браузерах і в headless-режимі, тож його краще покривати окремим тестом.
Pitfalls. These methods only work with native window.alert, confirm and prompt. A modal drawn with a div is an ordinary page element and is closed with a click. The beforeunload dialog on leaving a page behaves differently across browsers and in headless mode, so it is better covered by a separate test.
$("#orders tr.order-1042 .delete").click();
dismiss("Delete order #1042?");
$("#orders tr.order-1042").shouldBe(visible);
$("#orders tr.order-1042 .delete").click();
String text = confirm(ModalOptions.withExpectedText("Delete order #1042?"));
$("#orders tr.order-1042").shouldNot(exist);
$("#rename").click();
prompt("Rename the list", "Groceries");
$(".list-title").shouldHave(exactText("Groceries"));
$("#legacy-save").click();
Alert alert = switchTo().alert(Duration.ofSeconds(10));
assertEquals("Saved", alert.getText());
alert.accept();
confirm, dismiss і prompt — статичні методи Selenide, кожен повертає рядок із текстом діалогу. Перевантаження з String або ModalOptions додає перевірку тексту перед натисканням. switchTo().alert(Duration) чекає появи діалогу вказаний час і повертає org.openqa.selenium.Alert.confirm, dismiss and prompt are static Selenide methods, each returning the dialog text as a string. An overload taking a String or ModalOptions adds a text check before pressing. switchTo().alert(Duration) waits the given time for the dialog and returns an org.openqa.selenium.Alert.Що це. Системний діалог вибору файлу WebDriver не контролює, тому файл передають прямо в поле <input type=file>. Selenide має два методи. uploadFile(new File("...")) бере файл з диска, uploadFromClasspath("files/avatar.png") — з ресурсів тестового проєкту, що зручно для тестових даних у src/test/resources. Обидва приймають кілька файлів для полів з атрибутом multiple.
What it is. WebDriver cannot control the operating system file picker, so the file is passed straight into the <input type=file> field. Selenide has two methods. uploadFile(new File("...")) takes a file from disk, and uploadFromClasspath("files/avatar.png") takes it from the test project resources, which is handy for test data in src/test/resources. Both accept several files for fields with the multiple attribute.
Як це працює. Selenide перевіряє, що файл існує і що елемент справді input, а потім передає абсолютний шлях через sendKeys. Кнопка «Обрати файл» у дизайні часто є стилізованим label, а сам input прихований, але драйвери за стандартом W3C за замовчуванням приймають файл і для прихованого поля. На віддаленому браузері в Grid чи Selenoid це теж працює: Selenide ставить LocalFileDetector, і файл передається на ноду.
How it works. Selenide checks that the file exists and that the element really is an input, then passes the absolute path through sendKeys. The Choose file button in a design is often a styled label with the input hidden, but W3C drivers accept a file for a hidden field by default. It also works with a remote browser on Grid or Selenoid: Selenide sets a LocalFileDetector, and the file is transferred to the node.
Підводні камені. Шукай саме input, а не видиму кнопку: клік по кнопці відкриє системний діалог, з яким тест нічого не зробить. Після завантаження перевіряй результат на сторінці — назву файлу, прев’ю, повідомлення про успіх, — бо сам метод лише передає шлях. Перевір і негативні випадки: завеликий файл, недозволене розширення.
Pitfalls. Target the input, not the visible button: clicking the button opens the system dialog, which the test cannot handle. After uploading, check the outcome on the page, such as the file name, a preview or a success message, because the method only passes a path. Cover the negative cases too: a file that is too large or has a disallowed extension.
// файл лежить у src/test/resources/files/avatar.png
$("input[type=file]#avatar").uploadFromClasspath("files/avatar.png");
$(".avatar-preview img").shouldBe(visible);
// файл із диска
File contract = new File("build/test-data/contract.pdf");
$("#attachment").uploadFile(contract);
$(".attachment-name").shouldHave(exactText("contract.pdf"));
// кілька файлів у поле з атрибутом multiple
$("#gallery").uploadFromClasspath("files/1.jpg", "files/2.jpg");
$$(".gallery-item").shouldHave(size(2));
// the file lives in src/test/resources/files/avatar.png
$("input[type=file]#avatar").uploadFromClasspath("files/avatar.png");
$(".avatar-preview img").shouldBe(visible);
// a file from disk
File contract = new File("build/test-data/contract.pdf");
$("#attachment").uploadFile(contract);
$(".attachment-name").shouldHave(exactText("contract.pdf"));
// several files into a field with the multiple attribute
$("#gallery").uploadFromClasspath("files/1.jpg", "files/2.jpg");
$$(".gallery-item").shouldHave(size(2));
uploadFromClasspath приймає шлях відносно кореня classpath, тому files/avatar.png відповідає файлу в src/test/resources/files. uploadFile приймає File... і повертає перший завантажений файл. size(2) — умова колекції з CollectionCondition.uploadFromClasspath takes a path relative to the classpath root, so files/avatar.png maps to a file in src/test/resources/files. uploadFile takes File... and returns the first uploaded file. size(2) is a collection condition from CollectionCondition.Що це. $("a.export").download() клікає або звертається за посиланням, чекає, поки файл скачається, і повертає java.io.File. Як саме відбувається скачування, визначає Configuration.fileDownload, за замовчуванням HTTPGET. Файли складаються в Configuration.downloadsFolder, за замовчуванням build/downloads, у підпапку конкретного скачування.
What it is. $("a.export").download() clicks or requests the link, waits for the file to download and returns a java.io.File. How the download happens is decided by Configuration.fileDownload, HTTPGET by default. Files go to Configuration.downloadsFolder, build/downloads by default, into a subfolder for that particular download.
Налаштування. DownloadOptions дає builder: using(FOLDER) змінює режим для одного виклику, withExtension("pdf"), withName(...) чи withNameMatching(regex) відфільтровують потрібний файл, якщо сторінка тягне ще щось, а withTimeout(Duration) задає час на велике вивантаження. Для прямого URL без кліку є статичний Selenide.download(url).
Settings. DownloadOptions offers a builder: using(FOLDER) changes the mode for one call, withExtension("pdf"), withName(...) or withNameMatching(regex) pick the right file if the page fetches something else too, and withTimeout(Duration) gives time for a large export. For a direct URL without a click there is the static Selenide.download(url).
Приклад. Звіт продажів експортується в CSV. Тест скачує файл, перевіряє назву і читає рядки: заголовок, кількість записів, суму в останньому рядку. Для PDF чи XLSX використовують бібліотеки на кшталт PDFBox чи Apache POI, щоб дістати текст і порівняти.
Example. A sales report is exported to CSV. The test downloads the file, checks its name and reads the lines: the header, the number of records and the total in the last line. For PDF or XLSX you use libraries like PDFBox or Apache POI to extract the text and compare.
Підводні камені. Якщо файл не з’явився, у 7.x кидається FileNotDownloadedError, у старих версіях це був FileNotFoundException. Сервер з відповіддю 40x теж дає цю помилку, а 50x — звичайний RuntimeException. Перевірка лише факту скачування мало що дає: баг зазвичай у вмісті, наприклад порожній CSV чи неправильне кодування кирилиці.
Pitfalls. If the file never appears, 7.x throws FileNotDownloadedError; older versions threw FileNotFoundException. A 40x server response also produces that error, while a 50x gives a plain RuntimeException. Checking only that something downloaded tells you little: the bug is usually in the content, such as an empty CSV or broken Cyrillic encoding.
import static com.codeborne.selenide.DownloadOptions.using;
import static com.codeborne.selenide.FileDownloadMode.FOLDER;
File report = $("#export-csv").download(
using(FOLDER).withExtension("csv").withTimeout(Duration.ofSeconds(20)));
assertEquals("sales-2026-09.csv", report.getName());
List<String> lines = Files.readAllLines(report.toPath(), StandardCharsets.UTF_8);
assertEquals("date;product;amount", lines.get(0));
assertEquals(43, lines.size()); // заголовок + 42 записи
// прямий URL без кліку
File terms = Selenide.download("https://shop.example.com/files/terms.pdf");
import static com.codeborne.selenide.DownloadOptions.using;
import static com.codeborne.selenide.FileDownloadMode.FOLDER;
File report = $("#export-csv").download(
using(FOLDER).withExtension("csv").withTimeout(Duration.ofSeconds(20)));
assertEquals("sales-2026-09.csv", report.getName());
List<String> lines = Files.readAllLines(report.toPath(), StandardCharsets.UTF_8);
assertEquals("date;product;amount", lines.get(0));
assertEquals(43, lines.size()); // header + 42 records
// a direct URL without a click
File terms = Selenide.download("https://shop.example.com/files/terms.pdf");
using(FOLDER) створює DownloadOptions з режимом для цього виклику, а withExtension і withTimeout повертають копію з доданим параметром. download(...) повертає File, який далі читає звичайний java.nio.file.Files. Перевірки вмісту тут — JUnit assertEquals, бо файл уже на диску і чекати нічого не треба.using(FOLDER) creates DownloadOptions with a mode for this call, and withExtension and withTimeout return a copy with the added parameter. download(...) returns a File, which the ordinary java.nio.file.Files then reads. The content checks are JUnit assertEquals, because the file is already on disk and there is nothing to wait for.HTTPGET. Режим за замовчуванням. Selenide не клікає, а бере href посилання і робить власний GET-запит з cookies поточної сесії. Він найшвидший і працює з будь-яким браузером і Grid. Обмеження: лише для <a href>. Файл, який генерується після POST форми, JavaScript-кліку чи з blob-URL, так не скачати, а якщо сервер перевіряє не лише cookies, а й заголовки, запит може отримати інший результат, ніж браузер.
HTTPGET. The default mode. Selenide does not click; it takes the link href and makes its own GET request with the current session cookies. It is the fastest and works with any browser and with Grid. The limitation: it only works for <a href>. A file generated after a form POST, a JavaScript click or from a blob URL cannot be fetched this way, and if the server checks headers besides cookies, the request may get a different result than the browser would.
PROXY. Браузер ходить через вбудований проксі Selenide, а той перехоплює відповідь із файлом. Працює для будь-якого елемента, включно з відправкою форми. Потрібні Configuration.proxyEnabled = true і артефакт selenide-proxy. Ламається, коли браузер на віддаленій машині не бачить машину з тестами, і не працює з власним драйвером, створеним без проксі Selenide.
PROXY. The browser goes through Selenide's embedded proxy, which intercepts the response with the file. It works for any element, form submission included. It needs Configuration.proxyEnabled = true and the selenide-proxy artifact. It breaks when a browser on a remote machine cannot reach the machine running the tests, and it does not work with a custom driver created without the Selenide proxy.
FOLDER. Selenide клікає, а браузер кладе файл у свою папку завантажень, звідки Selenide його забирає. Це найближче до поведінки користувача. Локально працює одразу, а на Grid чи Selenoid папка всередині контейнера, тому потрібні модулі selenide-grid або selenide-selenoid.
FOLDER. Selenide clicks, the browser saves the file to its downloads folder, and Selenide picks it up from there. This is closest to user behaviour. Locally it works at once, but on Grid or Selenoid the folder is inside the container, so you need the selenide-grid or selenide-selenoid module.
CDP. Скачування через Chrome DevTools Protocol, лише для браузерів на Chromium. Не потребує проксі, а з версії 7.2 з тими ж модулями працює на Grid і Selenoid.
CDP. Downloading through the Chrome DevTools Protocol, for Chromium browsers only. It needs no proxy, and since 7.2, with the same modules, it works on Grid and Selenoid.
Як обрати. Якщо експорт — звичайні посилання, лишай HTTPGET. Для форм і JS-кнопок у Chrome-only наборі бери CDP, для крос-браузерного — FOLDER з модулем твого Grid. PROXY варто обирати, коли проксі вже потрібен для інших задач і мережа дозволяє. Режим можна змінити для окремого виклику через DownloadOptions.using(...), не чіпаючи глобальну конфігурацію.
How to choose. If exports are ordinary links, keep HTTPGET. For forms and JS buttons in a Chrome-only suite use CDP; for a cross-browser suite use FOLDER with the module for your Grid. Choose PROXY when you already need the proxy for other tasks and the network allows it. The mode can be changed for a single call via DownloadOptions.using(...) without touching the global configuration.
Режим | Як отримує файл | Елементи | Віддалений браузер
---------+----------------------------------+------------------+----------------------------
HTTPGET | власний GET за href з cookies | лише a href | працює без змін
PROXY | перехоплення в проксі Selenide | будь-які, форми | потрібна мережева видимість
FOLDER | клік, файл у папці браузера | будь-які | selenide-grid / selenoid
CDP | клік, події DevTools | будь-які | лише Chromium, ті ж модулі
// selenide.properties
selenide.fileDownload=HTTPGET
// Java: окремий виклик в іншому режимі
File invoice = $("#generate-invoice").download(DownloadOptions.using(FileDownloadMode.CDP));
Mode | How it gets the file | Elements | Remote browser
---------+----------------------------------+------------------+----------------------------
HTTPGET | its own GET to href with cookies | a href only | works as is
PROXY | interception in Selenide proxy | any, forms too | needs network visibility
FOLDER | click, file in browser folder | any | selenide-grid / selenoid
CDP | click, DevTools events | any | Chromium only, same modules
// selenide.properties
selenide.fileDownload=HTTPGET
// Java: a single call in another mode
File invoice = $("#generate-invoice").download(DownloadOptions.using(FileDownloadMode.CDP));
FileDownloadMode за трьома питаннями: як файл потрапляє до тесту, з якими елементами це працює і що потрібно для Grid. Рядок selenide.fileDownload задає режим глобально через файл властивостей, а DownloadOptions.using(...) перевизначає його лише для одного download.FileDownloadMode enum by three questions: how the file reaches the test, which elements it works with, and what Grid needs. The selenide.fileDownload line sets the mode globally through the properties file, while DownloadOptions.using(...) overrides it for a single download only.Що це. Configuration — клас зі статичними полями, які задають поведінку Selenide для всього процесу. Найчастіше змінюють такі. browser — який браузер запускати: chrome (за замовчуванням), firefox, edge або safari. headless — запуск без вікна, за замовчуванням false. browserSize — розмір вікна рядком у форматі "ШИРИНАxВИСОТА", за замовчуванням 1366x768. baseUrl — адреса стенду для відносних шляхів. timeout — скільки мілісекунд чекають перевірки й дії, за замовчуванням 4000.
What it is. Configuration is a class of static fields that define Selenide behaviour for the whole process. These are changed most often. browser is which browser to launch: chrome (the default), firefox, edge or safari. headless runs without a window and defaults to false. browserSize is the window size as a "WIDTHxHEIGHT" string, 1366x768 by default. baseUrl is the stand address for relative paths. timeout is how many milliseconds assertions and actions wait, 4000 by default.
Менш очевидні. pageLoadStrategy визначає, коли open() вважає сторінку завантаженою: normal (за замовчуванням, після події load), eager (коли готовий DOM) чи none. pageLoadTimeout обмежує саме завантаження сторінки, у Selenide це 30 секунд замість 300 у чистому WebDriver. reportsFolder — куди класти скріншоти й HTML при падінні.
Less obvious ones. pageLoadStrategy decides when open() treats a page as loaded: normal (the default, after the load event), eager (once the DOM is ready) or none. pageLoadTimeout limits the page load itself; Selenide uses 30 seconds instead of the 300 in plain WebDriver. reportsFolder is where screenshots and HTML go on failure.
Коли задавати. До першого open(), зазвичай у @BeforeAll базового класу, бо браузер створюється з тими налаштуваннями, які діяли в момент старту. Змінити browserSize посеред прогону можна, але вже відкритий браузер цього не помітить.
When to set them. Before the first open(), usually in @BeforeAll of a base class, because the browser is created with whatever settings are active when it starts. You can change browserSize mid-run, but an already open browser will not notice.
Підводні камені. Поля статичні, тож зміна в одному тесті впливає на всі наступні, а в паралельному прогоні ще й на сусідні потоки. Фіксований browserSize важливий для CI: у headless-режимі вікно інакше може бути маленьким, і адаптивна верстка сховає кнопки в бургер-меню.
Pitfalls. The fields are static, so a change in one test affects every later test, and in a parallel run the neighbouring threads as well. A fixed browserSize matters in CI: in headless mode the window may otherwise be small, and a responsive layout hides buttons inside a burger menu.
public abstract class BaseUiTest {
@BeforeAll
static void configure() {
Configuration.browser = "firefox";
Configuration.headless = true;
Configuration.browserSize = "1920x1080";
Configuration.baseUrl = "https://stage.shop.example.com";
Configuration.timeout = 6000;
Configuration.pageLoadStrategy = "eager";
Configuration.reportsFolder = "target/selenide-reports";
}
}
public static, тому їх присвоюють напряму. timeout і pageLoadTimeout задаються в мілісекундах типу long. pageLoadStrategy — рядок, а не enum, тож одрук на кшталт "eagr" вилізе лише при старті браузера. Метод у @BeforeAll статичний, бо JUnit 5 викликає його один раз на клас.public static, so you assign them directly. timeout and pageLoadTimeout are long values in milliseconds. pageLoadStrategy is a string rather than an enum, so a typo like "eagr" only shows up when the browser starts. The @BeforeAll method is static because JUnit 5 calls it once per class.Що це. Поле Configuration.browserCapabilities має тип MutableCapabilities, тому туди можна покласти ChromeOptions, FirefoxOptions чи EdgeOptions. Під час створення драйвера Selenide зливає їх зі своїми налаштуваннями: розміром вікна, headless, стратегією завантаження, проксі. Окремого поля на кшталт chromeOptions немає.
What it is. The Configuration.browserCapabilities field is of type MutableCapabilities, so you can put ChromeOptions, FirefoxOptions or EdgeOptions there. When the driver is created, Selenide merges them with its own settings: window size, headless, load strategy and proxy. There is no separate field such as chromeOptions.
Коли потрібно. Аргументи запуску (--lang=uk, --disable-notifications), налаштування профілю через setExperimentalOption("prefs", ...), наприклад заборона діалогу збереження паролів, специфічні для Grid опції на кшталт selenoid:options. Для шляху до нестандартного бінарника браузера є окреме поле browserBinary, для версії на Grid — browserVersion.
When you need it. Launch arguments (--lang=uk, --disable-notifications), profile preferences via setExperimentalOption("prefs", ...), for example disabling the save-password prompt, and Grid-specific options such as selenoid:options. For a path to a non-standard browser binary there is a separate browserBinary field, and for the version on Grid there is browserVersion.
Приклад. Тест локалізації має запускати Chrome українською і без спливаючих запитів на сповіщення, а на Selenoid ще й із записом відео. Усе це збирається в один об’єкт опцій до першого open().
Example. A localisation test must run Chrome in Ukrainian and without notification pop-ups, and on Selenoid it must also record a video. All of that is assembled into one options object before the first open().
Підводні камені. Не дублюй те, що вже має власне поле: --headless в аргументах разом із Configuration.headless=false дає плутанину, а --window-size конфліктує з browserSize. Опції одного браузера не діють на інший: якщо тест запустили з -Dselenide.browser=firefox, покладені ChromeOptions фактично проігноруються, тож для крос-браузерного набору опції вибирають залежно від Configuration.browser. І змінювати browserCapabilities після старту браузера марно.
Pitfalls. Do not duplicate what already has its own field: --headless in the arguments together with Configuration.headless=false causes confusion, and --window-size clashes with browserSize. Options for one browser do nothing for another: if the run was started with -Dselenide.browser=firefox, the ChromeOptions you set are effectively ignored, so a cross-browser suite picks options based on Configuration.browser. And changing browserCapabilities after the browser has started is pointless.
ChromeOptions options = new ChromeOptions()
.addArguments("--lang=uk", "--disable-notifications");
options.setExperimentalOption("prefs", Map.of(
"credentials_enable_service", false,
"profile.password_manager_enabled", false));
options.setCapability("selenoid:options", Map.of(
"enableVNC", true,
"enableVideo", true));
Configuration.browser = "chrome";
Configuration.browserCapabilities = options;
open("/login");
addArguments повертає той самий об’єкт, тому виклики можна ланцюжити. setExperimentalOption("prefs", Map) передає Chrome налаштування профілю, які зазвичай змінюють у chrome://settings. setCapability з ключем у форматі постачальник:options — стандартний W3C-спосіб передати розширені параметри, які читає лише Grid чи Selenoid.addArguments returns the same object, so calls can be chained. setExperimentalOption("prefs", Map) hands Chrome profile preferences that you would normally change in chrome://settings. setCapability with a key in the vendor:options format is the standard W3C way to pass extension parameters that only Grid or Selenoid reads.Що це. Кожне поле Configuration має відповідну властивість із префіксом selenide.: selenide.browser, selenide.timeout, selenide.remote, selenide.baseUrl тощо. Їх можна покласти у файл selenide.properties у корені classpath (зазвичай src/test/resources) або передати системними властивостями JVM через -D.
What it is. Every Configuration field has a matching property with the selenide. prefix: selenide.browser, selenide.timeout, selenide.remote, selenide.baseUrl and so on. You can put them into a selenide.properties file at the classpath root (usually src/test/resources) or pass them as JVM system properties with -D.
Пріоритет. Selenide читає файл, а поверх нього накладає системні властивості, тому -Dselenide.browser=firefox перемагає рядок у файлі. Значення зчитуються один раз при ініціалізації класу Configuration, а пряме присвоєння в коді робиться пізніше і перемагає все. Звідси практичне правило: у коді не хардкодь те, що має перемикатися з CI, інакше -D просто не спрацює.
Precedence. Selenide reads the file and lays system properties on top, so -Dselenide.browser=firefox beats the line in the file. Values are read once when the Configuration class initialises, and a direct assignment in code happens later and beats everything. Hence a practical rule: do not hardcode in code whatever CI must switch, or -D simply will not work.
Приклад. У файлі лежать спокійні локальні значення, а CI-джоба запускає mvn test -Dselenide.remote=http://selenoid:4444/wd/hub -Dselenide.headless=true -Dselenide.baseUrl=https://stage.example.com. У Maven системні властивості до тестового JVM зазвичай доносить Surefire, у Gradle їх треба явно передати в задачу test через systemProperty, інакше вони залишаться в процесі самого Gradle.
Example. The file holds calm local values, and the CI job runs mvn test -Dselenide.remote=http://selenoid:4444/wd/hub -Dselenide.headless=true -Dselenide.baseUrl=https://stage.example.com. In Maven, Surefire usually forwards system properties to the test JVM; in Gradle you must pass them into the test task explicitly via systemProperty, otherwise they stay in the Gradle process itself.
Configuration.config(). З версії 7.5.1 метод повертає копію поточних налаштувань як SelenideConfig. Її можна змінити, не чіпаючи глобальних значень, і передати, наприклад, у new SelenideDriver(config) для другого браузера.
Configuration.config(). Since 7.5.1 this method returns a copy of the current settings as a SelenideConfig. You can change it without touching the global values and pass it, for example, to new SelenideDriver(config) for a second browser.
# src/test/resources/selenide.properties (локальні значення)
selenide.browser=chrome
selenide.browserSize=1920x1080
selenide.timeout=5000
selenide.baseUrl=http://localhost:3000
# CI перевизначає їх системними властивостями:
# mvn test -Dselenide.remote=http://selenoid:4444/wd/hub -Dselenide.headless=true
# build.gradle.kts: без цього -D не дійде до тестового JVM
# tasks.test {
# systemProperties(System.getProperties().filterKeys { it.toString().startsWith("selenide.") }.mapKeys { it.key.toString() })
# }
# src/test/resources/selenide.properties (local values)
selenide.browser=chrome
selenide.browserSize=1920x1080
selenide.timeout=5000
selenide.baseUrl=http://localhost:3000
# CI overrides them with system properties:
# mvn test -Dselenide.remote=http://selenoid:4444/wd/hub -Dselenide.headless=true
# build.gradle.kts: without this, -D never reaches the test JVM
# tasks.test {
# systemProperties(System.getProperties().filterKeys { it.toString().startsWith("selenide.") }.mapKeys { it.key.toString() })
# }
.properties: пари ключ=значення, коментарі через #. Ключ дорівнює імені поля Configuration з префіксом selenide.. Фрагмент для Gradle Kotlin DSL фільтрує системні властивості процесу Gradle за префіксом і копіює їх у JVM, де виконуються тести..properties file: key=value pairs with # comments. The key equals the Configuration field name with the selenide. prefix. The Gradle Kotlin DSL fragment filters the Gradle process system properties by prefix and copies them into the JVM where tests run.Що це. Досить задати Configuration.remote — адресу хаба. Тоді замість локального драйвера Selenide створює RemoteWebDriver з поточними browser, browserVersion, browserSize і browserCapabilities. Для Selenoid адреса зазвичай виглядає як http://selenoid:4444/wd/hub, для Grid 4 підходить і коротка http://grid:4444. Тести при цьому не змінюються.
What it is. Setting Configuration.remote, the hub address, is enough. Instead of a local driver, Selenide then creates a RemoteWebDriver with the current browser, browserVersion, browserSize and browserCapabilities. For Selenoid the address usually looks like http://selenoid:4444/wd/hub, and for Grid 4 the short http://grid:4444 works too. The tests themselves do not change.
Що дає Selenide додатково. Для віддаленого драйвера він сам ставить LocalFileDetector, тож uploadFile передає файл з машини тестів на ноду без додаткового коду. Таймаути HTTP-з’єднання з хабом регулюють remoteConnectionTimeout (10 секунд за замовчуванням) і remoteReadTimeout (90 секунд). Для Selenoid через selenoid:options вмикають VNC і відеозапис.
What Selenide adds. For a remote driver it sets a LocalFileDetector itself, so uploadFile transfers the file from the test machine to the node with no extra code. HTTP connection timeouts to the hub are controlled by remoteConnectionTimeout (10 seconds by default) and remoteReadTimeout (90 seconds). For Selenoid, VNC and video recording are switched on through selenoid:options.
Скачування файлів. Режим HTTPGET працює скрізь, бо тест сам робить запит. Щоб режими FOLDER чи CDP забирали файл із теки браузера на віддаленій ноді, потрібні модулі selenide-grid або selenide-selenoid: вони вміють дістати файл з ноди.
Downloading files. The HTTPGET mode works everywhere, because the test makes the request itself. For the FOLDER or CDP modes to fetch a file from the browser folder on a remote node, you need the selenide-grid or selenide-selenoid modules: they know how to pull the file off the node.
Підводні камені. Кожен тест тепер ходить мережею, тому повільний хаб дає таймаути на рівні з’єднання, а не на рівні умов. Кількість паралельних потоків не повинна перевищувати кількість слотів, інакше сесії стоятимуть у черзі і падатимуть за таймаутом черги. Вбудований проксі Selenide на Grid працює лише тоді, коли нода може достукатися до машини з тестами.
Pitfalls. Every test now goes over the network, so a slow hub produces timeouts at connection level rather than condition level. The number of parallel threads must not exceed the number of slots, or sessions queue up and die on the queue timeout. The embedded Selenide proxy works on Grid only when the node can reach the machine running the tests.
@BeforeAll
static void remoteBrowser() {
Configuration.remote = System.getProperty("selenide.remote", "http://selenoid:4444/wd/hub");
Configuration.browser = "chrome";
Configuration.browserVersion = "128.0";
Configuration.browserSize = "1920x1080";
Configuration.remoteReadTimeout = 120_000;
ChromeOptions options = new ChromeOptions();
options.setCapability("selenoid:options", Map.of(
"enableVNC", true,
"enableVideo", true,
"name", "checkout-suite"));
Configuration.browserCapabilities = options;
}
System.getProperty(ключ, запасне) дозволяє перевизначити адресу з CI, лишивши локальне значення. 120_000 — число з підкресленням для читабельності, тут це дві хвилини в мілісекундах. Ключі enableVNC, enableVideo і name читає Selenoid; Selenium Grid їх просто проігнорує.System.getProperty(key, fallback) lets CI override the address while keeping a local value. 120_000 is a number with an underscore for readability, two minutes in milliseconds here. The enableVNC, enableVideo and name keys are read by Selenoid; Selenium Grid simply ignores them.Що це. WebDriverProvider — функціональний інтерфейс з одним методом WebDriver createDriver(Capabilities capabilities). Ти пишеш клас, який створює драйвер так, як потрібно проєкту, і записуєш повне ім’я цього класу в Configuration.browser. Selenide викличе провайдер там, де зазвичай створює браузер сам.
What it is. WebDriverProvider is a functional interface with a single method, WebDriver createDriver(Capabilities capabilities). You write a class that builds the driver the way your project needs and put that class's fully qualified name into Configuration.browser. Selenide calls the provider wherever it would normally create the browser itself.
Чим кращий за setWebDriver. Життєвим циклом і далі керує Selenide: драйвер прив’язується до потоку, закривається наприкінці прогону й перестворюється, якщо браузер упав. Параметр capabilities містить те, що Selenide вже зібрав із конфігурації, і документація радить змерджити його у свої опції, щоб не втратити розмір вікна чи інші налаштування.
Why it beats setWebDriver. Selenide still owns the lifecycle: the driver is bound to a thread, closed at the end of the run and recreated if the browser crashed. The capabilities parameter contains what Selenide has already collected from configuration, and the documentation recommends merging it into your options so you do not lose the window size or other settings.
Коли потрібен. Нестандартний браузер чи драйвер (Appium, хмарний провайдер зі своєю авторизацією), складна логіка вибору опцій, корпоративний сертифікат. Для звичайного Chrome з аргументами достатньо browserCapabilities.
When you need it. A non-standard browser or driver (Appium, a cloud provider with its own authentication), complex logic for choosing options, a corporate certificate. For a regular Chrome with arguments, browserCapabilities is enough.
Підводні камені. Javadoc інтерфейсу застерігає: драйвер, створений провайдером, не може користуватися вбудованим проксі Selenide і його текою завантажень, тому для режимів PROXY і FOLDER автори радять розширювати DriverFactory. Не кешуй драйвер у статичному полі всередині провайдера: WebDriver не потокобезпечний, а Selenide сам зберігає екземпляр для кожного потоку.
Pitfalls. The interface javadoc warns that a driver created by a provider cannot use the embedded Selenide proxy or its downloads folder, so for the PROXY and FOLDER modes the authors suggest extending DriverFactory instead. Do not cache the driver in a static field inside the provider: WebDriver is not thread-safe, and Selenide already stores one instance per thread.
public class CloudChromeProvider implements WebDriverProvider {
@Override
public WebDriver createDriver(Capabilities capabilities) {
ChromeOptions options = new ChromeOptions();
options.merge(capabilities); // зберігаємо налаштування Selenide
options.setCapability("cloud:options", Map.of(
"token", System.getenv("CLOUD_TOKEN"),
"build", System.getenv().getOrDefault("CI_PIPELINE_ID", "local")));
try {
return new RemoteWebDriver(new URL("https://hub.cloud.example.com/wd/hub"), options);
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
}
}
// у базовому класі тестів
Configuration.browser = CloudChromeProvider.class.getName();
public class CloudChromeProvider implements WebDriverProvider {
@Override
public WebDriver createDriver(Capabilities capabilities) {
ChromeOptions options = new ChromeOptions();
options.merge(capabilities); // keep the Selenide settings
options.setCapability("cloud:options", Map.of(
"token", System.getenv("CLOUD_TOKEN"),
"build", System.getenv().getOrDefault("CI_PIPELINE_ID", "local")));
try {
return new RemoteWebDriver(new URL("https://hub.cloud.example.com/wd/hub"), options);
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
}
}
// in the base test class
Configuration.browser = CloudChromeProvider.class.getName();
options.merge(capabilities) додає до опцій усе, що підготував Selenide. getName() повертає повне ім’я класу з пакетом, і саме рядок, а не об’єкт, записують у Configuration.browser: Selenide створює провайдер рефлексією. Ключ cloud:options тут умовний, у реального постачальника він свій.options.merge(capabilities) adds everything Selenide prepared to your options. getName() returns the fully qualified class name, and it is that string, not an object, that goes into Configuration.browser: Selenide creates the provider via reflection. The cloud:options key here is illustrative; a real vendor has its own.Що це. При proxyEnabled=true Selenide піднімає на машині з тестами HTTP-проксі на базі BrowserUp і налаштовує браузер ходити через нього. У гілці 7.x для цього треба додати артефакт com.codeborne:selenide-proxy, який приносить сам проксі-сервер і Netty. Через WebDriverRunner.getSelenideProxy() тест отримує доступ до трафіку браузера.
What it is. With proxyEnabled=true, Selenide starts a BrowserUp-based HTTP proxy on the test machine and configures the browser to go through it. In the 7.x line you have to add the com.codeborne:selenide-proxy artifact, which brings the proxy server itself and Netty. Through WebDriverRunner.getSelenideProxy() the test gets access to the browser's traffic.
Що це дає. Режим скачування PROXY: файл перехоплюється з відповіді навіть тоді, коли його віддає POST-форма чи JavaScript, а не посилання. Фільтри запитів і відповідей: можна додати заголовок, записати всі виклики API чи підмінити відповідь через responseMocker(). Також проксі додає заголовок авторизації для open(url, BASIC, credentials).
What it gives you. The PROXY download mode: a file is captured from the response even when a POST form or JavaScript serves it instead of a link. Request and response filters: you can add a header, record every API call or replace a response via responseMocker(). The proxy also adds the authorisation header for open(url, BASIC, credentials).
Чому вимкнено за замовчуванням. Браузер має достукатися до проксі на машині з тестами. Локально це завжди так, а з Grid, Selenoid у хмарі чи Docker-мережі — не завжди. Адресу й порт, які бачить браузер, задають proxyHost і proxyPort; за замовчуванням Selenide визначає IP сам, а порт вибирає випадковий.
Why it is off by default. The browser has to reach the proxy on the test machine. Locally that is always true, but with Grid, a cloud Selenoid or a Docker network it is not guaranteed. The host and port the browser sees are set with proxyHost and proxyPort; by default Selenide detects the IP itself and picks a random port.
Підводні камені. Проксі додає накладні витрати на кожен запит і ще одну точку відмови з HTTPS-сертифікатами. Він не працює з драйвером, переданим через setWebDriver без проксі-сервера. Мок відповіді через проксі — інструмент для точкових сценаріїв на кшталт «сервіс повернув 500», а не заміна контрактних тестів.
Pitfalls. The proxy adds overhead to every request and one more failure point around HTTPS certificates. It does not work with a driver handed over via setWebDriver without a proxy server. Mocking a response through the proxy is a tool for targeted scenarios like "the service returned 500", not a replacement for contract tests.
// потрібна залежність com.codeborne:selenide-proxy
Configuration.proxyEnabled = true;
open("/orders");
SelenideProxyServer proxy = WebDriverRunner.getSelenideProxy();
List<String> apiCalls = new ArrayList<>();
proxy.addRequestFilter("api-log", (request, contents, messageInfo) -> {
apiCalls.add(request.method() + " " + messageInfo.getUrl());
return null; // null = пропустити запит далі
});
proxy.responseMocker().mockText("orders-empty",
urlEndsWith(GET, "/api/orders"), () -> "[]");
refresh();
$(".empty-state").shouldBe(visible);
proxy.responseMocker().reset("orders-empty");
// requires the com.codeborne:selenide-proxy dependency
Configuration.proxyEnabled = true;
open("/orders");
SelenideProxyServer proxy = WebDriverRunner.getSelenideProxy();
List<String> apiCalls = new ArrayList<>();
proxy.addRequestFilter("api-log", (request, contents, messageInfo) -> {
apiCalls.add(request.method() + " " + messageInfo.getUrl());
return null; // null = let the request through
});
proxy.responseMocker().mockText("orders-empty",
urlEndsWith(GET, "/api/orders"), () -> "[]");
refresh();
$(".empty-state").shouldBe(visible);
proxy.responseMocker().reset("orders-empty");
null пропускає запит далі, а повернена відповідь зупинила б його. urlEndsWith(GET, ...) — матчер із com.codeborne.selenide.proxy.RequestMatchers, а GET імпортують з RequestMatcher.HttpMethod. Ім’я мока потрібне, щоб потім зняти саме його через reset.null lets the request through, while returning a response would stop it. urlEndsWith(GET, ...) is a matcher from com.codeborne.selenide.proxy.RequestMatchers, and GET is imported from RequestMatcher.HttpMethod. The mock name lets you remove exactly that mock later with reset.Модель. Статичні $ і open працюють із драйвером поточного потоку: Selenide тримає окремий WebDriver на кожен потік і створює його при першому open() у цьому потоці. Тому паралельність вмикають на рівні раннера, а тести переписувати не треба. У JUnit 5 це ключі junit.jupiter.execution.parallel.* у junit-platform.properties, у TestNG — атрибути parallel і thread-count у suite.
The model. The static $ and open work with the current thread's driver: Selenide keeps a separate WebDriver per thread and creates it on the first open() in that thread. So parallelism is switched on at runner level and tests do not need rewriting. In JUnit 5 that means the junit.jupiter.execution.parallel.* keys in junit-platform.properties; in TestNG, the parallel and thread-count suite attributes.
Що не потокобезпечне. Configuration — статичні поля, спільні для всіх потоків. Якщо один тест ставить Configuration.timeout = 20000 «для себе», він змінює таймаут і сусідам, що йдуть паралельно. Налаштування задають один раз до старту, а для точкових випадків користуються shouldBe(condition, Duration). Слухачі SelenideLogger, зокрема Allure, реєструються на потік, тому їх додають у @BeforeEach, а не лише в @BeforeAll.
What is not thread-safe. Configuration is static fields shared by all threads. If one test sets Configuration.timeout = 20000 "for itself", it changes the timeout for its neighbours running in parallel as well. Settings are made once before the run, and targeted cases use shouldBe(condition, Duration). SelenideLogger listeners, Allure included, are registered per thread, so they are added in @BeforeEach, not only in @BeforeAll.
Два браузери в одному тесті. Для чату між двома користувачами беруть new SelenideDriver(new SelenideConfig().browser("firefox")) з власними open і $, або inNewBrowser(() -> ...), який відкриває тимчасовий браузер і закриває його після блоку.
Two browsers in one test. For a chat between two users you take new SelenideDriver(new SelenideConfig().browser("firefox")) with its own open and $, or inNewBrowser(() -> ...), which opens a temporary browser and closes it after the block.
Трейдофи. Паралельність множить не лише швидкість, а й конфлікти даних: два потоки, що логіняться тим самим користувачем чи редагують той самий кошик, дають «випадкові» падіння. Тому кожен тест створює власні дані через API або бере їх з пулу. Режим same_thread для класу з тестами, що ділять стан, і @ResourceLock у JUnit 5 рятують точково. Ще одна межа — ресурси: вісім локальних Chrome з’їдають CPU ноутбука, і таймаути почнуть спрацьовувати від повільності, а не від багів. Кількість потоків узгоджують із кількістю слотів Grid, а виграш міряють реальним часом прогону.
Trade-offs. Parallelism multiplies not just speed but data conflicts: two threads logging in as the same user or editing the same cart produce "random" failures. So each test creates its own data through the API or takes it from a pool. same_thread mode for a class whose tests share state, and @ResourceLock in JUnit 5, help in specific spots. Another limit is resources: eight local Chrome instances eat a laptop's CPU, and timeouts start firing because of slowness rather than bugs. The thread count is matched to the Grid slot count, and the gain is measured by real run time.
// src/test/resources/junit-platform.properties
// junit.jupiter.execution.parallel.enabled=true
// junit.jupiter.execution.parallel.mode.default=concurrent
// junit.jupiter.execution.parallel.config.strategy=fixed
// junit.jupiter.execution.parallel.config.fixed.parallelism=4
class ChatTest {
@Test
void messageReachesSecondUser() {
User alice = api.createUser(); // власні дані для кожного тесту
User bob = api.createUser();
open("/chat?as=" + alice.id()); // браузер цього потоку
SelenideDriver bobBrowser = new SelenideDriver(Configuration.config().browser("firefox"));
try {
bobBrowser.open("/chat?as=" + bob.id());
$("#message").setValue("Привіт").pressEnter();
bobBrowser.$(".message").shouldHave(text("Привіт"));
} finally {
bobBrowser.close();
}
}
}
// src/test/resources/junit-platform.properties
// junit.jupiter.execution.parallel.enabled=true
// junit.jupiter.execution.parallel.mode.default=concurrent
// junit.jupiter.execution.parallel.config.strategy=fixed
// junit.jupiter.execution.parallel.config.fixed.parallelism=4
class ChatTest {
@Test
void messageReachesSecondUser() {
User alice = api.createUser(); // own data for every test
User bob = api.createUser();
open("/chat?as=" + alice.id()); // this thread's browser
SelenideDriver bobBrowser = new SelenideDriver(Configuration.config().browser("firefox"));
try {
bobBrowser.open("/chat?as=" + bob.id());
$("#message").setValue("Hello").pressEnter();
bobBrowser.$(".message").shouldHave(text("Hello"));
} finally {
bobBrowser.close();
}
}
}
Configuration.config() повертає копію глобальних налаштувань, а browser("firefox") змінює лише її. SelenideDriver має власні методи open і $, не пов’язані зі статичним браузером потоку, тому закривати його треба явно в finally. api.createUser() — умовний клієнт тестових даних.Configuration.config() returns a copy of the global settings, and browser("firefox") changes only that copy. SelenideDriver has its own open and $ methods, independent of the thread's static browser, so it must be closed explicitly in finally. api.createUser() stands for a test data client.Що це. Для мобільних тестів є окремий модуль com.codeborne:selenide-appium. Він переносить звичний стиль $, should і розумних очікувань на нативні застосунки Android та iOS, а під капотом працює Appium Java client з AndroidDriver чи IOSDriver. Окремого класу-драйвера під назвою SelenideAppiumDriver немає: драйвер створює твій код, а Selenide ним керує.
What it is. Mobile tests have a separate module, com.codeborne:selenide-appium. It brings the familiar $, should and smart waiting style to native Android and iOS apps, while the Appium Java client with AndroidDriver or IOSDriver works underneath. There is no separate driver class called SelenideAppiumDriver: your code creates the driver and Selenide manages it.
Як підключити. Пишеш WebDriverProvider, що створює AndroidDriver з UiAutomator2Options (або IOSDriver з XCUITestOptions), записуєш його ім’я в Configuration.browser і викликаєш SelenideAppium.launchApp() замість open(). Елементи шукають через AppiumBy: accessibilityId, id, xpath. Модуль додає власні дії на кшталт tap, свайпи й скрол, а також SelenideAppium.back() і перемикання контекстів для гібридних застосунків.
How to wire it. You write a WebDriverProvider that creates an AndroidDriver with UiAutomator2Options (or an IOSDriver with XCUITestOptions), put its name into Configuration.browser and call SelenideAppium.launchApp() instead of open(). Elements are found through AppiumBy: accessibilityId, id, xpath. The module adds its own actions such as tap, swipes and scrolling, plus SelenideAppium.back() and context switching for hybrid apps.
Коли доречно. Коли команда вже пише веб-тести на Selenide і хоче той самий стиль, звіти й очікування для мобільного клієнта. Page Object лишається тим самим підходом, лише локатори інші.
When it fits. When the team already writes web tests with Selenide and wants the same style, reports and waiting for the mobile client. Page Objects stay the same approach, only the locators differ.
Підводні камені. Мобільні локатори крихкіші за веб: XPath по дереву UIAutomator повільний, тому з розробниками домовляються про accessibility id. Таймаути на емуляторі доводиться піднімати, бо анімації й холодний старт повільніші. І не всі веб-методи мають сенс: hover чи скачування файлу в нативному застосунку не працюють.
Pitfalls. Mobile locators are more fragile than web ones: XPath over the UIAutomator tree is slow, so you agree on accessibility ids with the developers. Timeouts on an emulator have to go up, because animations and cold starts are slower. And not every web method makes sense: hover or downloading a file in a native app simply does not work.
public class AndroidAppProvider implements WebDriverProvider {
@Override
public WebDriver createDriver(Capabilities capabilities) {
UiAutomator2Options options = new UiAutomator2Options();
options.merge(capabilities);
options.setDeviceName("Android Emulator");
options.setApp(new File("build/app-debug.apk").getAbsolutePath());
try {
return new AndroidDriver(new URL("http://127.0.0.1:4723/"), options);
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
}
}
class LoginMobileTest {
@Test
void userLogsIn() {
Configuration.browser = AndroidAppProvider.class.getName();
Configuration.timeout = 10_000; // емулятор повільніший за браузер
SelenideAppium.launchApp();
$(AppiumBy.accessibilityId("email")).setValue("anna@example.com");
$(AppiumBy.accessibilityId("password")).setValue("secret");
$(AppiumBy.accessibilityId("login")).click();
$(AppiumBy.accessibilityId("greeting")).shouldHave(text("Anna"));
}
}
public class AndroidAppProvider implements WebDriverProvider {
@Override
public WebDriver createDriver(Capabilities capabilities) {
UiAutomator2Options options = new UiAutomator2Options();
options.merge(capabilities);
options.setDeviceName("Android Emulator");
options.setApp(new File("build/app-debug.apk").getAbsolutePath());
try {
return new AndroidDriver(new URL("http://127.0.0.1:4723/"), options);
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
}
}
class LoginMobileTest {
@Test
void userLogsIn() {
Configuration.browser = AndroidAppProvider.class.getName();
Configuration.timeout = 10_000; // an emulator is slower than a browser
SelenideAppium.launchApp();
$(AppiumBy.accessibilityId("email")).setValue("anna@example.com");
$(AppiumBy.accessibilityId("password")).setValue("secret");
$(AppiumBy.accessibilityId("login")).click();
$(AppiumBy.accessibilityId("greeting")).shouldHave(text("Anna"));
}
}
UiAutomator2Options — клас опцій Appium для Android; setApp приймає абсолютний шлях до APK. Адреса http://127.0.0.1:4723/ — локальний Appium-сервер. SelenideAppium.launchApp() створює драйвер через провайдер і запускає застосунок, а $ з AppiumBy повертає елемент з тими самими очікуваннями, що й у вебі.UiAutomator2Options is the Appium options class for Android; setApp takes an absolute path to the APK. http://127.0.0.1:4723/ is a local Appium server. SelenideAppium.launchApp() creates the driver through the provider and starts the app, and $ with AppiumBy returns an element with the same waiting as on the web.Що це. Коли падає перевірка чи дія Selenide, бібліотека сама робить скріншот сторінки і зберігає її HTML (page source), а шляхи до обох файлів дописує в текст помилки. За це відповідають прапорці Configuration.screenshots і Configuration.savePageSource, обидва за замовчуванням true.
What it is. When a Selenide assertion or action fails, the library takes a screenshot of the page and saves its HTML (page source) by itself, then appends the paths to both files to the error text. This is controlled by the Configuration.screenshots and Configuration.savePageSource flags, both true by default.
Де лежать. У теці Configuration.reportsFolder, за замовчуванням build/reports/tests — це зручно для Gradle. У Maven-проєктах її зазвичай перенаправляють у target, наприклад target/selenide-reports, щоб mvn clean прибирав старі файли. Якщо задати reportsUrl, наприклад адресу артефактів збірки в CI, у повідомленні з’являться клікабельні посилання замість локальних шляхів.
Where they go. Into the Configuration.reportsFolder directory, build/reports/tests by default, which suits Gradle. Maven projects usually redirect it into target, for example target/selenide-reports, so that mvn clean removes old files. If you set reportsUrl, for example the address of build artifacts in CI, the message shows clickable links instead of local paths.
Вручну. Selenide.screenshot("after-payment") знімає всю сторінку в будь-який момент і повертає шлях до файлу, а $(".chart").screenshot() знімає лише один елемент. Це корисно для кроків, які не падають, але які хочеться бачити у звіті.
By hand. Selenide.screenshot("after-payment") captures the whole page at any moment and returns the file path, while $(".chart").screenshot() captures a single element. That is useful for steps that do not fail but that you want to see in the report.
Підводні камені. HTML-знімок без стилів і картинок іноді виглядає «зламаним»; для повної копії є savePageSourceWithResources, але вона важча. Скріншот показує лише видиму частину вікна, тож у маленькому headless-вікні потрібного елемента на ньому може не бути. Вимикати screenshots заради швидкості не варто: без них розбір падінь у CI перетворюється на вгадування.
Pitfalls. An HTML snapshot without styles and images sometimes looks "broken"; for a full copy there is savePageSourceWithResources, but it is heavier. A screenshot shows only the visible part of the window, so in a small headless window the element you care about may not be on it. Turning screenshots off for speed is a bad idea: without them, analysing CI failures turns into guesswork.
Configuration.reportsFolder = "target/selenide-reports";
Configuration.screenshots = true;
Configuration.savePageSource = true;
Configuration.reportsUrl = System.getenv("CI_JOB_URL") + "/artifacts/file/";
open("/checkout");
$("#pay").click();
String page = Selenide.screenshot("after-payment");
File chart = $(".revenue-chart").screenshot();
// On failure the message ends with something like:
// Screenshot: file:/.../target/selenide-reports/1727600000000.0.png
// Page source: file:/.../target/selenide-reports/1727600000000.0.html
reportsUrl додається перед ім’ям файлу замість локального шляху, тому воно має вказувати на теку артефактів конкретної збірки. Selenide.screenshot(name) повертає рядок з адресою файлу, а $(...).screenshot() повертає File або null, якщо знімок не вдалося зробити через технічну проблему. Ім’я автоматичного знімка складається з часової позначки й лічильника.reportsUrl is put in front of the file name instead of the local path, so it must point at the artifact folder of a specific build. Selenide.screenshot(name) returns a string with the file URL, and $(...).screenshot() returns a File, or null when a technical problem prevented the snapshot. An automatic snapshot name is built from a timestamp and a counter.Проблема. Автоматичний скріншот Selenide робить лише тоді, коли падає його власна дія чи should. Якщо тест упав на assertEquals з JUnit, на AssertJ-перевірці значення з API чи на винятку в коді підготовки, знімка не буде, хоча браузер у цей момент показує щось корисне.
The problem. Selenide takes an automatic screenshot only when one of its own actions or should calls fails. If the test fails on a JUnit assertEquals, on an AssertJ check of an API value or on an exception in setup code, there is no snapshot, even though the browser shows something useful at that moment.
JUnit 5. Розширення ScreenShooterExtension з пакета com.codeborne.selenide.junit5 знімає екран після будь-якого проваленого тесту. Конструктор new ScreenShooterExtension(true) вмикає знімки й для успішних тестів, а метод to(folder) — просто скорочений запис для Configuration.reportsFolder. Поруч часто ставлять TextReportExtension, який друкує в лог таблицю кроків Selenide для кожного тесту.
JUnit 5. The ScreenShooterExtension from the com.codeborne.selenide.junit5 package captures the screen after any failed test. The constructor new ScreenShooterExtension(true) also enables snapshots for passing tests, and the to(folder) method is just a shortcut for Configuration.reportsFolder. It is often paired with TextReportExtension, which prints a table of Selenide steps for each test into the log.
JUnit 4 і TestNG. У JUnit 4 є правило ScreenShooter.failedTests() з артефакту selenide-junit4, у TestNG — слухач com.codeborne.selenide.testng.ScreenShooter з selenide-testng, який підключають через @Listeners. Історично саме JUnit 4-правило дало назву цьому підходу.
JUnit 4 and TestNG. JUnit 4 has the ScreenShooter.failedTests() rule from the selenide-junit4 artifact, and TestNG has the com.codeborne.selenide.testng.ScreenShooter listener from selenide-testng, attached via @Listeners. Historically it was the JUnit 4 rule that gave this approach its name.
Підводні камені. Ці розширення знімають статичний браузер Selenide; для драйверів, створених через new SelenideDriver(), знімка не буде, про що прямо пише документація TestNG-слухача. Знімки для всіх успішних тестів швидко роздувають артефакти CI, тож цей режим вмикають тимчасово. А якщо браузер уже закрито в @AfterEach до розширення, знімати буде нічого.
Pitfalls. These extensions capture the static Selenide browser; drivers created with new SelenideDriver() get no snapshot, as the TestNG listener documentation states outright. Snapshots for every passing test quickly bloat CI artifacts, so that mode is switched on temporarily. And if the browser is already closed in @AfterEach before the extension runs, there is nothing left to capture.
// JUnit 5
@ExtendWith({ScreenShooterExtension.class, TextReportExtension.class})
class PriceTest {
@Test
void totalMatchesApi() {
open("/cart");
int apiTotal = api.cartTotal();
int uiTotal = Integer.parseInt($(".total").text().replaceAll("\\D", ""));
assertEquals(apiTotal, uiTotal); // при падінні теж буде скріншот
}
}
// JUnit 5 з налаштуваннями: знімати й успішні тести
@RegisterExtension
static ScreenShooterExtension screenshots = new ScreenShooterExtension(true).to("target/screens");
// TestNG
@Listeners(com.codeborne.selenide.testng.ScreenShooter.class)
public class PriceTestNg { /* ... */ }
// JUnit 5
@ExtendWith({ScreenShooterExtension.class, TextReportExtension.class})
class PriceTest {
@Test
void totalMatchesApi() {
open("/cart");
int apiTotal = api.cartTotal();
int uiTotal = Integer.parseInt($(".total").text().replaceAll("\\D", ""));
assertEquals(apiTotal, uiTotal); // a failure here also gets a screenshot
}
}
// JUnit 5 with settings: capture passing tests too
@RegisterExtension
static ScreenShooterExtension screenshots = new ScreenShooterExtension(true).to("target/screens");
// TestNG
@Listeners(com.codeborne.selenide.testng.ScreenShooter.class)
public class PriceTestNg { /* ... */ }
@ExtendWith приймає масив класів розширень, тож кілька підключаються одним рядком. @RegisterExtension на статичному полі дозволяє створити розширення з параметрами. replaceAll("\\D", "") прибирає з тексту все, крім цифр. У TestNG слухач указують повним ім’ям, бо ScreenShooter з JUnit 4 має таку саму коротку назву.@ExtendWith takes an array of extension classes, so several are attached in one line. @RegisterExtension on a static field lets you create an extension with parameters. replaceAll("\\D", "") strips everything but digits from the text. In TestNG the listener is given by its full name, because the JUnit 4 ScreenShooter has the same short name.Що це. Інтеграцію дає артефакт io.qameta.allure:allure-selenide від команди Allure. Клас AllureSelenide — це не JUnit-розширення, а слухач подій Selenide (LogEventListener). Його реєструють через SelenideLogger.addListener("AllureSelenide", new AllureSelenide()), і після цього кожна дія та перевірка Selenide стає кроком у звіті Allure, а при падінні до кроку додаються скріншот і HTML сторінки.
What it is. The integration comes from the io.qameta.allure:allure-selenide artifact made by the Allure team. The AllureSelenide class is not a JUnit extension but a Selenide event listener (LogEventListener). You register it with SelenideLogger.addListener("AllureSelenide", new AllureSelenide()), and from then on every Selenide action and assertion becomes a step in the Allure report, and on failure the step gets a screenshot and the page HTML attached.
Налаштування. Метод screenshots(true) вмикає знімок при падінні, savePageSource(false) вимикає HTML, якщо він не потрібен і лише роздуває звіт. Сам звіт будує Allure-адаптер раннера: allure-junit5 чи allure-testng, результати пишуться в allure-results, а HTML генерує Allure CLI або плагін Maven чи Gradle.
Settings. screenshots(true) enables a snapshot on failure, and savePageSource(false) turns off the HTML when it is not needed and only bloats the report. The report itself is produced by the runner's Allure adapter, allure-junit5 or allure-testng; results are written to allure-results, and the HTML is generated by the Allure CLI or a Maven or Gradle plugin.
Приклад. Тест оформлення замовлення у звіті показує кроки на кшталт «$(#email) set value anna@example.com», «$(.total) should have text 120», а на впалому кроці — картинку й HTML. Разом із as() кроки читаються як людський сценарій.
Example. A checkout test in the report shows steps such as "$(#email) set value anna@example.com" and "$(.total) should have text 120", and the failed step carries the picture and the HTML. Together with as(), the steps read like a human scenario.
Підводні камені. SelenideLogger тримає слухачів окремо для кожного потоку. Якщо зареєструвати AllureSelenide один раз у @BeforeAll, а тести йдуть паралельно в інших потоках, кроків там не буде. Надійніше додавати слухача в @BeforeEach базового класу: повторна реєстрація з тим самим ім’ям просто замінює попереднього. Просто оголосити @ExtendWith(AllureSelenide.class) не допоможе, бо це не розширення JUnit.
Pitfalls. SelenideLogger keeps listeners separately for each thread. If you register AllureSelenide once in @BeforeAll while tests run in parallel on other threads, those threads get no steps. It is more reliable to add the listener in @BeforeEach of a base class: registering again under the same name just replaces the previous one. Merely declaring @ExtendWith(AllureSelenide.class) will not help, because it is not a JUnit extension.
// залежності: io.qameta.allure:allure-selenide та io.qameta.allure:allure-junit5
public abstract class BaseUiTest {
@BeforeEach
void allureSteps() {
// на кожен потік, бо слухачі SelenideLogger прив’язані до потоку
SelenideLogger.addListener("AllureSelenide",
new AllureSelenide().screenshots(true).savePageSource(false));
}
@AfterEach
void removeAllureSteps() {
SelenideLogger.removeListener("AllureSelenide");
}
}
// dependencies: io.qameta.allure:allure-selenide and io.qameta.allure:allure-junit5
public abstract class BaseUiTest {
@BeforeEach
void allureSteps() {
// per thread, because SelenideLogger listeners are bound to a thread
SelenideLogger.addListener("AllureSelenide",
new AllureSelenide().screenshots(true).savePageSource(false));
}
@AfterEach
void removeAllureSteps() {
SelenideLogger.removeListener("AllureSelenide");
}
}
addListener — унікальне ім’я в межах потоку, за ним слухача потім знімають через removeListener. Налаштування AllureSelenide ланцюжаться, бо кожен метод повертає той самий об’єкт. SelenideLogger лежить у пакеті com.codeborne.selenide.logevents, AllureSelenide — у io.qameta.allure.selenide.addListener is a name unique within the thread, used later to remove the listener with removeListener. AllureSelenide settings chain because every method returns the same object. SelenideLogger lives in the com.codeborne.selenide.logevents package, and AllureSelenide in io.qameta.allure.selenide.Що це. Soft assertions — режим, у якому провалена should-перевірка не зупиняє тест одразу. Selenide записує помилку, тест іде далі, а наприкінці кидає одну AssertionError зі списком усіх провалених перевірок і скріншотами до кожної.
What it is. Soft assertions are a mode in which a failed should check does not stop the test immediately. Selenide records the error, the test carries on, and at the end it throws a single AssertionError listing all failed checks with a screenshot for each.
Як увімкнути. Потрібні дві речі разом. Перша — Configuration.assertionMode = AssertionMode.SOFT (або -Dselenide.assertionMode=SOFT): збирач помилок реагує лише в цьому режимі. Друга — інтеграція з раннером, яка наприкінці тесту викине зібране: у JUnit 5 це @ExtendWith(SoftAssertsExtension.class), у TestNG — @Listeners(SoftAsserts.class) з артефакту selenide-testng, у JUnit 4 — правило SoftAsserts з selenide-junit4. Без режиму розширення нічого не збере, без розширення режим нікому звітувати.
How to enable it. You need two things together. The first is Configuration.assertionMode = AssertionMode.SOFT (or -Dselenide.assertionMode=SOFT): the error collector only acts in this mode. The second is a runner integration that throws what was collected at the end of the test: in JUnit 5 that is @ExtendWith(SoftAssertsExtension.class), in TestNG @Listeners(SoftAsserts.class) from the selenide-testng artifact, in JUnit 4 the SoftAsserts rule from selenide-junit4. Without the mode the extension collects nothing; without the extension the mode has nobody to report to.
Приклад. Сторінка профілю: треба перевірити ім’я, email, телефон і аватар. У звичайному режимі перша ж розбіжність зупиняє тест, і про решту три дізнаєшся після виправлення першої. У soft-режимі одним прогоном отримуєш повний список.
Example. A profile page where you check the name, email, phone and avatar. In normal mode the first mismatch stops the test, and you learn about the other three only after fixing the first. In soft mode a single run gives you the full list.
Підводні камені. М’якими стають лише методи should*. Дії click, setValue та інші падають як завжди: якщо кнопки немає, тест зупиниться. Звичайні assertEquals і AssertJ цей механізм не перехоплює. І оскільки assertionMode — статичне поле, увімкнення його в одному класі впливає на всі наступні тести процесу, тож режим краще задавати свідомо на рівні прогону чи окремого набору.
Pitfalls. Only should* methods become soft. click, setValue and other actions fail as usual: if the button is missing, the test stops. Plain assertEquals and AssertJ are not captured by this mechanism. And since assertionMode is a static field, switching it on in one class affects every later test in the process, so set the mode deliberately at run or suite level.
@ExtendWith(SoftAssertsExtension.class)
class ProfilePageTest {
@BeforeEach
void softMode() {
Configuration.assertionMode = AssertionMode.SOFT;
}
@AfterEach
void strictMode() {
Configuration.assertionMode = AssertionMode.STRICT; // не зачепити інші класи
}
@Test
void profileShowsUserData() {
open("/profile");
$("#name").shouldHave(exactText("Anna Koval"));
$("#email").shouldHave(exactText("anna@example.com"));
$("#phone").shouldHave(exactText("+380 50 123 4567"));
$("#avatar").shouldBe(visible);
// усі розбіжності прийдуть однією помилкою після тесту
}
}
@ExtendWith(SoftAssertsExtension.class)
class ProfilePageTest {
@BeforeEach
void softMode() {
Configuration.assertionMode = AssertionMode.SOFT;
}
@AfterEach
void strictMode() {
Configuration.assertionMode = AssertionMode.STRICT; // do not affect other classes
}
@Test
void profileShowsUserData() {
open("/profile");
$("#name").shouldHave(exactText("Anna Koval"));
$("#email").shouldHave(exactText("anna@example.com"));
$("#phone").shouldHave(exactText("+380 50 123 4567"));
$("#avatar").shouldBe(visible);
// every mismatch arrives as one error after the test
}
}
AssertionMode — enum із двома значеннями, STRICT (за замовчуванням) і SOFT. SoftAssertsExtension лежить у com.codeborne.selenide.junit5: перед тестом він підключає збирач помилок, а після тесту кидає зібране. Повернення STRICT в @AfterEach захищає інші класи, але в паралельному прогоні статичне поле все одно спільне.AssertionMode is an enum with two values, STRICT (the default) and SOFT. SoftAssertsExtension lives in com.codeborne.selenide.junit5: before a test it attaches the error collector, and after the test it throws what was collected. Restoring STRICT in @AfterEach protects other classes, but in a parallel run the static field is still shared.Суть вибору. Fail-fast зупиняє тест на першій розбіжності, soft збирає всі розбіжності й звітує наприкінці. Жоден режим не кращий загалом: він має відповідати тому, чи залежать перевірки одна від одної.
The core of the choice. Fail-fast stops the test at the first mismatch; soft collects every mismatch and reports at the end. Neither mode is better in general: it should match whether the checks depend on each other.
Коли soft. Кілька незалежних фактів про один і той самий екран: поля профілю, тексти валідації форми, колонки рядка таблиці, локалізовані підписи. Тут перша помилка нічого не каже про решту, а повний список за один прогін економить цикл «виправив, перезапустив, побачив наступне». Особливо цінно для довгих E2E, де дістатися до екрана коштує хвилину.
When soft. Several independent facts about one screen: profile fields, form validation texts, the columns of a table row, localised labels. Here the first failure says nothing about the rest, and a full list in one run saves the "fix, rerun, see the next one" loop. It is especially valuable for long E2E tests where reaching the screen costs a minute.
Коли fail-fast. Кроки, де наступний має сенс лише після успіху попереднього: логін, додавання в кошик, оплата. Якщо кошик порожній, десять перевірок на сторінці оплати впадуть каскадом, і в звіті буде одинадцять помилок замість однієї справжньої причини. Каскад ще й повільний: кожна наступна перевірка чекає повний таймаут.
When fail-fast. Steps where the next one only makes sense after the previous succeeded: login, adding to the cart, payment. If the cart is empty, ten checks on the payment page fail in a cascade, and the report shows eleven errors instead of one real cause. The cascade is also slow: every following check waits the full timeout.
Приховані витрати soft. Звіт стає шумнішим, а метрика «кількість упалих тестів» перестає розрізняти одну розбіжність і десять. Тест, що падає наприкінці, займає слот довше. Глобальний assertionMode=SOFT на весь проєкт — поширена помилка: він робить м’якими й передумови, які мали б зупиняти тест.
Hidden costs of soft. The report gets noisier, and the "number of failed tests" metric no longer distinguishes one mismatch from ten. A test that fails at the end holds its slot longer. A global assertionMode=SOFT for the whole project is a common mistake: it also softens preconditions that ought to stop the test.
Практичний компроміс. За замовчуванням fail-fast. Soft вмикаю точково: або окремим класом з SoftAssertsExtension, або групую незалежні перевірки в один метод Page Object. Для значень, які не є UI-перевірками (відповідь API, дані з бази), беру AssertJ SoftAssertions.assertSoftly: вона не залежить від режиму Selenide і не зачіпає інші тести. Критичні передумови всередині soft-тесту все одно пишу діями або перевіряю до початку м’якого блоку.
A practical compromise. Fail-fast by default. Soft is enabled selectively: either as a separate class with SoftAssertsExtension, or by grouping independent checks into one Page Object method. For values that are not UI checks (an API response, database data), I use AssertJ SoftAssertions.assertSoftly: it does not depend on the Selenide mode and does not affect other tests. Critical preconditions inside a soft test are still written as actions or checked before the soft block begins.
@Test
void orderSummaryIsCorrect() {
// передумови: має зупинити тест одразу
open("/cart");
$$(".cart-item").shouldHave(size(2));
$("#checkout").click();
// незалежні значення з API: збираємо всі розбіжності
OrderDto order = api.lastOrder();
SoftAssertions.assertSoftly(soft -> {
soft.assertThat(order.items()).hasSize(2);
soft.assertThat(order.currency()).isEqualTo("UAH");
soft.assertThat(order.deliveryType()).isEqualTo("courier");
});
// UI-перевірки одного екрана, теж незалежні
summaryPage.shouldShowTotals("1 250 ₴", "50 ₴", "1 300 ₴");
}
@Test
void orderSummaryIsCorrect() {
// preconditions: must stop the test immediately
open("/cart");
$$(".cart-item").shouldHave(size(2));
$("#checkout").click();
// independent API values: collect every mismatch
OrderDto order = api.lastOrder();
SoftAssertions.assertSoftly(soft -> {
soft.assertThat(order.items()).hasSize(2);
soft.assertThat(order.currency()).isEqualTo("UAH");
soft.assertThat(order.deliveryType()).isEqualTo("courier");
});
// UI checks on one screen, also independent
summaryPage.shouldShowTotals("1 250 ₴", "50 ₴", "1 300 ₴");
}
SoftAssertions.assertSoftly з AssertJ приймає лямбду з об’єктом soft і наприкінці блоку кидає одну помилку з усіма розбіжностями. Режим Selenide на цей блок не впливає. summaryPage.shouldShowTotals — умовний метод Page Object, всередині якого незалежні shouldHave можна зробити м’якими через клас із soft-режимом.SoftAssertions.assertSoftly takes a lambda with a soft object and throws one error with every mismatch at the end of the block. The Selenide mode does not affect this block. summaryPage.shouldShowTotals stands for a Page Object method whose independent shouldHave calls can be made soft through a class running in soft mode.Мета. Кожне падіння в CI має приходити з достатніми доказами: що бачив браузер, що було в DOM, які кроки виконались, які запити пішли і в якому оточенні. Якщо для розбору треба «запустити локально ще раз», конвеєр налаштовано погано, а флакі-тести так і лишаються загадкою.
The goal. Every CI failure should arrive with enough evidence: what the browser saw, what was in the DOM, which steps ran, which requests went out and in which environment. If analysing it requires "running it locally again", the pipeline is set up badly and flaky tests stay a mystery.
Стабільне оточення. Headless-браузер у Docker-образі або Selenoid чи Grid з фіксованою версією браузера, завжди явний browserSize, однакові мова й часовий пояс. Таймаути задаються властивостями під середовище: на спільному CI-агенті вони зазвичай трохи більші, ніж на ноутбуці, але піднімати їх до хвилини означає ховати проблеми.
A stable environment. A headless browser in a Docker image, or Selenoid or Grid with a pinned browser version, an explicit browserSize every time, the same language and time zone. Timeouts are set through properties per environment: on a shared CI agent they are usually a little higher than on a laptop, but raising them to a minute means hiding problems.
Артефакти. reportsFolder публікується як артефакт збірки навіть при падінні, а reportsUrl робить шляхи в логах клікабельними. Allure-результати з історією між збірками показують, чи тест флакі давно. JUnit XML дає CI рідний звіт. Для складних випадків є відео: VNC і запис у Selenoid або модуль selenide-video-recorder, де анотацією @Video позначають лише тести під розслідуванням. Логи браузера через webdriverLogsEnabled допомагають із JS-помилками, але займають місце, тому їх вмикають за потреби.
Artifacts. reportsFolder is published as a build artifact even on failure, and reportsUrl makes the paths in the logs clickable. Allure results with history across builds show whether a test has been flaky for a while. JUnit XML gives CI its native report. For hard cases there is video: VNC and recording in Selenoid, or the selenide-video-recorder module, where only the tests under investigation are marked with the @Video annotation. Browser logs via webdriverLogsEnabled help with JS errors but take space, so they are switched on when needed.
Політика перезапусків. Автоматичний retry (Surefire rerunFailingTestsCount, TestNG retry analyzer) рятує від інфраструктурного шуму, але маскує справжню нестабільність. Компроміс: максимум один перезапуск, а тести, що пройшли лише з другої спроби, окремо позначаються у звіті й потрапляють у беклог на стабілізацію.
Retry policy. Automatic retries (Surefire rerunFailingTestsCount, a TestNG retry analyzer) protect you from infrastructure noise but mask real instability. The compromise: at most one rerun, and tests that passed only on the second attempt are flagged separately in the report and go into the stabilisation backlog.
Трейдоф. Кожен артефакт коштує місця і часу: відео всіх тестів і HTML з ресурсами для тисячі тестів забивають сховище. Тому базовий набір — скріншот, HTML і кроки — завжди, а важке — вибірково.
The trade-off. Every artifact costs space and time: video for every test and HTML with resources for a thousand tests fill up storage. So the baseline, a screenshot, HTML and steps, is always on, while the heavy stuff is selective.
ui-tests:
runs-on: ubuntu-latest
services:
selenoid:
image: aerokube/selenoid:latest-release
ports: ["4444:4444"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: "21" }
- name: Run UI tests
run: >
mvn -B test
-Dselenide.remote=http://localhost:4444/wd/hub
-Dselenide.browserSize=1920x1080
-Dselenide.timeout=6000
-Dselenide.reportsFolder=target/selenide-reports
-Dsurefire.rerunFailingTestsCount=1
- name: Publish artifacts # навіть якщо тести впали
if: always()
uses: actions/upload-artifact@v4
with:
name: ui-test-evidence
path: |
target/selenide-reports
target/allure-results
target/surefire-reports
ui-tests:
runs-on: ubuntu-latest
services:
selenoid:
image: aerokube/selenoid:latest-release
ports: ["4444:4444"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: "21" }
- name: Run UI tests
run: >
mvn -B test
-Dselenide.remote=http://localhost:4444/wd/hub
-Dselenide.browserSize=1920x1080
-Dselenide.timeout=6000
-Dselenide.reportsFolder=target/selenide-reports
-Dsurefire.rerunFailingTestsCount=1
- name: Publish artifacts # even when tests failed
if: always()
uses: actions/upload-artifact@v4
with:
name: ui-test-evidence
path: |
target/selenide-reports
target/allure-results
target/surefire-reports
services піднімає контейнер Selenoid поруч з агентом, тож адреса хаба — localhost:4444. Символ > у YAML склеює рядки команди через пробіл. if: always() змушує крок публікації виконатися й після падіння тестів, інакше докази зникнуть разом з агентом. Для реального Selenoid ще потрібен файл browsers.json з образами браузерів; тут він опущений.services block starts a Selenoid container next to the agent, so the hub address is localhost:4444. The > symbol in YAML folds the command lines into one with spaces. if: always() makes the publish step run even after tests fail, otherwise the evidence disappears with the agent. A real Selenoid also needs a browsers.json file with browser images; it is omitted here.Що це. Page Object — клас, що описує сторінку через її елементи й дії користувача, щоб тести говорили мовою бізнесу, а не селекторів. У Selenide він стає набагато простішим, бо елементи ліниві: поле private final SelenideElement email = $("#email") можна оголосити одразу, без PageFactory.initElements і без драйвера в конструкторі.
What it is. A Page Object is a class that describes a page through its elements and user actions, so tests speak the language of the business rather than selectors. In Selenide it becomes much simpler because elements are lazy: a field private final SelenideElement email = $("#email") can be declared straight away, with no PageFactory.initElements and no driver in the constructor.
Два стилі. Перший — поля з $ і $$, об’єкт створюється через new. Другий — анотації @FindBy і ініціалізація через page(LoginPage.class) або open("/login", LoginPage.class); тоді Selenide сам підставить лінивий SelenideElement, ElementsCollection чи компонент-Container, а анотація @As дасть полю людське ім’я у звітах. Перший стиль зазвичай читається легше, другий зручний під час міграції зі старого PageFactory.
Two styles. The first uses fields with $ and $$, and the object is created with new. The second uses @FindBy annotations and initialisation through page(LoginPage.class) or open("/login", LoginPage.class); Selenide then injects a lazy SelenideElement, ElementsCollection or Container component, and the @As annotation gives a field a human name in reports. The first style usually reads better; the second is handy when migrating from an old PageFactory.
Приклад. Метод сторінки повертає наступну сторінку: loginAs(...) повертає DashboardPage. Так тест будується ланцюжком і одразу видно, куди веде дія. Перевірки на кшталт shouldSeeError(...) можна тримати в Page Object, але без бізнес-логіки тесту.
Example. A page method returns the next page: loginAs(...) returns a DashboardPage. The test then reads as a chain, and it is obvious where each action leads. Checks such as shouldSeeError(...) may live in the Page Object, as long as the test's business logic does not.
Підводні камені. Не повертай із методів сторінки рядки чи булеві значення для асертів у тесті: так губляться очікування, і тест стає flaky. Краще повертати SelenideElement або робити перевірку всередині методу з умовою Selenide. Не роби сторінку-гігант на 80 полів: винось шапку, таблицю чи модалку в окремі компоненти.
Pitfalls. Do not return strings or booleans from page methods for assertions in the test: waiting is lost and the test becomes flaky. Return a SelenideElement instead, or do the check inside the method with a Selenide condition. And do not build an 80-field monster page: extract the header, a table or a modal into separate components.
// стиль 1: поля з $, створення через new
public class LoginPage {
private final SelenideElement email = $("#email").as("Поле email");
private final SelenideElement password = $("#password");
private final SelenideElement error = $(".form-error");
public DashboardPage loginAs(String user, String pass) {
email.setValue(user);
password.setValue(pass).pressEnter();
return new DashboardPage();
}
public LoginPage shouldSeeError(String message) {
error.shouldHave(text(message));
return this;
}
}
// стиль 2: @FindBy + page()
public class DashboardPage {
@FindBy(css = ".greeting") @As("Привітання")
private SelenideElement greeting;
public void shouldGreet(String name) {
greeting.shouldHave(text(name));
}
}
@Test
void userSeesGreeting() {
open("/login", LoginPage.class)
.loginAs("anna@example.com", "secret")
.shouldGreet("Anna");
}
// style 1: fields with $, created with new
public class LoginPage {
private final SelenideElement email = $("#email").as("Email field");
private final SelenideElement password = $("#password");
private final SelenideElement error = $(".form-error");
public DashboardPage loginAs(String user, String pass) {
email.setValue(user);
password.setValue(pass).pressEnter();
return new DashboardPage();
}
public LoginPage shouldSeeError(String message) {
error.shouldHave(text(message));
return this;
}
}
// style 2: @FindBy + page()
public class DashboardPage {
@FindBy(css = ".greeting") @As("Greeting")
private SelenideElement greeting;
public void shouldGreet(String name) {
greeting.shouldHave(text(name));
}
}
@Test
void userSeesGreeting() {
open("/login", LoginPage.class)
.loginAs("anna@example.com", "secret")
.shouldGreet("Anna");
}
open("/login", LoginPage.class) відкриває сторінку і повертає екземпляр класу, ініціалізований фабрикою Selenide, тому поля з @FindBy уже лінивi. @FindBy тут — стандартна анотація Selenium з org.openqa.selenium.support, а @As — анотація Selenide для імені в звітах. Методи повертають наступну сторінку або this, що дає fluent-ланцюжок у тесті.open("/login", LoginPage.class) opens the page and returns an instance initialised by Selenide's factory, so @FindBy fields are already lazy. @FindBy here is the standard Selenium annotation from org.openqa.selenium.support, while @As is a Selenide annotation for the report name. Methods return the next page or this, which gives a fluent chain in the test.Контекст. На реальній сторінці повторюються не сторінки, а компоненти: картка товару, рядок таблиці, datepicker, модалка підтвердження. Їх описують окремим класом, який шукає все відносно свого кореня, щоб один і той самий компонент працював і на каталозі, і в кошику.
Context. On a real page it is components that repeat, not pages: a product card, a table row, a date picker, a confirmation modal. They are described as a separate class that searches everything relative to its root, so the same component works both in the catalogue and in the cart.
Варіант 1: Container. Клас реалізує маркерний інтерфейс Container, корінь позначається полем з @Self, а внутрішні елементи — @FindBy відносно кореня. Такий компонент створює фабрика Selenide: полем сторінки з @FindBy, списком List<ProductCard> або напряму через $(".product", ProductCard.class) і $$(".product", ProductCard.class). Абстрактний клас ElementsContainer із методом getSelf() з версії 7.0 видалено, тож старий код під час оновлення переписують на Container.
Option 1: Container. The class implements the Container marker interface, the root is a field marked with @Self, and inner elements use @FindBy relative to the root. Selenide's factory creates such a component: as a page field with @FindBy, as a List<ProductCard>, or directly through $(".product", ProductCard.class) and $$(".product", ProductCard.class). The abstract ElementsContainer class with its getSelf() method was removed in 7.0, so old code gets rewritten to Container during the upgrade.
Варіант 2: звичайний клас. Конструктор приймає SelenideElement root, а всі поля будуються як root.$(".title"). Жодної рефлексії й анотацій, усе видно в коді, легко передати корінь, знайдений через findBy(text(...)). Для списку компонентів пишеш $$(".product").asFixedIterable() або метод card(String name).
Option 2: a plain class. The constructor takes a SelenideElement root, and every field is built as root.$(".title"). No reflection and no annotations, everything is visible in the code, and it is easy to pass a root found with findBy(text(...)). For a list of components you use $$(".product").asFixedIterable() or a method like card(String name).
Трейдоф. Container зручний, коли в команді вже прийняті @FindBy і хочеться декларативних сторінок; мінус — магія фабрики, яку новачок не бачить, і прив’язка до конструктора без аргументів. Звичайний клас простіший для рев’ю, дебагу й композиції, тому в нових проєктах я зазвичай беру його. В обох випадках головне правило однакове: компонент нічого не шукає глобально через $ без кореня, інакше на сторінці з двома однаковими компонентами тест зачепить перший-ліпший.
Trade-off. Container is convenient when the team already uses @FindBy and wants declarative pages; the downside is factory magic a newcomer cannot see and the requirement of a no-argument constructor. A plain class is simpler to review, debug and compose, so on new projects I usually pick it. Either way the key rule is the same: a component never searches globally with a root-less $, otherwise on a page with two identical components the test grabs whichever comes first.
// варіант 1: Container з @Self, створює фабрика Selenide
public class ProductCard implements Container {
@Self SelenideElement self;
@FindBy(css = ".title") SelenideElement title;
@FindBy(css = "button.add") SelenideElement addButton;
}
List<ProductCard> cards = $$(".product", ProductCard.class);
cards.get(0).title.shouldHave(text("Kettle"));
// варіант 2: звичайний клас із коренем
public class ProductCardView {
private final SelenideElement root;
public ProductCardView(SelenideElement root) {
this.root = root;
}
public static ProductCardView byName(String name) {
return new ProductCardView($$(".product").findBy(text(name)));
}
public ProductCardView addToCart() {
root.$("button.add").click();
return this;
}
public void shouldCost(String price) {
root.$(".price").shouldHave(exactText(price));
}
}
ProductCardView.byName("Kettle").addToCart().shouldCost("49.99");
// option 1: a Container with @Self, created by Selenide's factory
public class ProductCard implements Container {
@Self SelenideElement self;
@FindBy(css = ".title") SelenideElement title;
@FindBy(css = "button.add") SelenideElement addButton;
}
List<ProductCard> cards = $$(".product", ProductCard.class);
cards.get(0).title.shouldHave(text("Kettle"));
// option 2: a plain class with a root
public class ProductCardView {
private final SelenideElement root;
public ProductCardView(SelenideElement root) {
this.root = root;
}
public static ProductCardView byName(String name) {
return new ProductCardView($$(".product").findBy(text(name)));
}
public ProductCardView addToCart() {
root.$("button.add").click();
return this;
}
public void shouldCost(String price) {
root.$(".price").shouldHave(exactText(price));
}
}
ProductCardView.byName("Kettle").addToCart().shouldCost("49.99");
@Self — вкладена анотація інтерфейсу Container, яка каже фабриці покласти в поле кореневий елемент компонента. Поля з @FindBy у контейнері шукаються відносно цього кореня. $$(".product", ProductCard.class) повертає List контейнерів. У другому варіанті root.$(...) будує лінивий ланцюжок від кореня, а findBy(text(name)) вибирає картку за назвою.@Self is a nested annotation of the Container interface that tells the factory to put the component's root element into the field. @FindBy fields inside the container are searched relative to that root. $$(".product", ProductCard.class) returns a List of containers. In the second option, root.$(...) builds a lazy chain from the root, and findBy(text(name)) picks the card by its name.Проблема. Якщо кожен із 300 тестів логіниться через форму, створює товар через адмінку і лише потім перевіряє свою фічу, прогін триває годину, а падіння в чужих кроках маскують справжні баги. UI-тест має проходити через інтерфейс лише ту частину, яку він перевіряє.
The problem. If each of 300 tests logs in through the form, creates a product through the admin panel and only then checks its feature, the run takes an hour and failures in borrowed steps hide the real bugs. A UI test should go through the interface only for the part it actually verifies.
Підхід. Дані створюються через API чи базу до тесту, а сесія підкладається в браузер. Для cookie-сесії треба спершу відкрити будь-яку легку сторінку того самого домену, бо браузер не дозволить поставити cookie для домену, на якому ти ще не був, потім додати cookie і вже тоді відкрити цільову сторінку. Для SPA з токеном у localStorage роблять те саме через Selenide.localStorage().setItem(...). У свіжих 7.x є Selenide.cookies().add(...), у старіших — getWebDriver().manage().addCookie(...).
The approach. Data is created through an API or the database before the test, and the session is planted into the browser. For a cookie session you first open any lightweight page on the same domain, because the browser will not let you set a cookie for a domain you have not visited, then add the cookie, and only then open the target page. For an SPA with a token in localStorage you do the same through Selenide.localStorage().setItem(...). Recent 7.x has Selenide.cookies().add(...); older versions use getWebDriver().manage().addCookie(...).
Трейдоф. Виграш — швидкість і ізоляція: тест падає лише через свою фічу. Ціна — треба підтримувати API-клієнт і знати, як бекенд видає сесію; якщо формат токена зміниться, зламаються всі тести разом. Тому форму логіну все одно покривають кількома окремими UI-тестами, а швидкий логін ховають за одним хелпером або JUnit-розширенням, щоб зміна торкалась одного місця.
Trade-off. The gain is speed and isolation: a test fails only because of its own feature. The cost is maintaining an API client and knowing how the backend issues a session; if the token format changes, every test breaks at once. That is why the login form is still covered by a few dedicated UI tests, while the fast login hides behind one helper or JUnit extension, so a change touches one place.
Підводні камені. Cookie з прапорцями HttpOnly чи SameSite треба ставити з тими ж атрибутами, що й бекенд, інакше сесія мовчки не підхопиться. На Grid браузер і API-клієнт можуть бачити різні адреси стенду. І не залишай згенеровані дані без прибирання: у паралельному прогоні чужий тест може натрапити на твій товар.
Pitfalls. Cookies with HttpOnly or SameSite flags must be set with the same attributes the backend uses, otherwise the session is silently ignored. On a Grid, the browser and the API client may see different stand addresses. And do not leave generated data behind: in a parallel run another test may stumble over your product.
public final class FastLogin {
public static void asUser(String email, String password) {
String sessionId = AuthApi.login(email, password); // наш REST-клієнт
open("/favicon.ico"); // спершу потрапити на домен
Cookie session = new Cookie.Builder("SESSION", sessionId)
.path("/")
.isHttpOnly(true)
.build();
WebDriverRunner.getWebDriver().manage().addCookie(session);
}
public static void asSpaUser(String token) {
open("/favicon.ico");
Selenide.localStorage().setItem("auth_token", token);
}
}
@Test
void userSeesOwnOrders() {
String orderId = OrdersApi.createOrder("anna@example.com");
FastLogin.asUser("anna@example.com", "secret");
open("/orders");
$$(".order").findBy(text(orderId)).shouldBe(visible);
}
public final class FastLogin {
public static void asUser(String email, String password) {
String sessionId = AuthApi.login(email, password); // our REST client
open("/favicon.ico"); // land on the domain first
Cookie session = new Cookie.Builder("SESSION", sessionId)
.path("/")
.isHttpOnly(true)
.build();
WebDriverRunner.getWebDriver().manage().addCookie(session);
}
public static void asSpaUser(String token) {
open("/favicon.ico");
Selenide.localStorage().setItem("auth_token", token);
}
}
@Test
void userSeesOwnOrders() {
String orderId = OrdersApi.createOrder("anna@example.com");
FastLogin.asUser("anna@example.com", "secret");
open("/orders");
$$(".order").findBy(text(orderId)).shouldBe(visible);
}
AuthApi і OrdersApi — умовні API-клієнти проєкту, наприклад на RestAssured. open("/favicon.ico") — дешевий спосіб опинитися на потрібному домені без рендеру важкої сторінки. Cookie.Builder із Selenium дозволяє задати path, HttpOnly та інші атрибути. Selenide.localStorage() повертає обгортку з setItem, getItem і clear.AuthApi and OrdersApi stand for the project's API clients, for example built on RestAssured. open("/favicon.ico") is a cheap way to land on the right domain without rendering a heavy page. Selenium's Cookie.Builder lets you set path, HttpOnly and other attributes. Selenide.localStorage() returns a wrapper with setItem, getItem and clear.Навіщо. Щоб перевірити, як UI поводиться з відповіддю 500, порожнім списком чи повільним платіжним сервісом, не ламаючи справжній бекенд. І щоб перевірити, що фронтенд надсилає правильний запит.
Why. To see how the UI handles a 500, an empty list or a slow payment service without breaking the real backend, and to verify that the front end sends the right request.
Варіант 1: проксі Selenide. Вмикається Configuration.proxyEnabled = true плюс артефакт selenide-proxy. Далі getSelenideProxy().responseMocker().mockText(...) підміняє відповідь, а addRequestFilter і addResponseFilter дозволяють читати чи змінювати трафік. Працює з будь-яким браузером, але браузер має бачити машину з тестами, що на віддаленому Grid часто неможливо.
Option 1: the Selenide proxy. You turn on Configuration.proxyEnabled = true and add the selenide-proxy artifact. Then getSelenideProxy().responseMocker().mockText(...) replaces a response, while addRequestFilter and addResponseFilter let you read or change traffic. It works with any browser, but the browser has to reach the machine running the tests, which is often impossible on a remote Grid.
Варіант 2: CDP. Для Chromium Selenium дає NetworkInterceptor поверх DevTools: маршрут підміняє відповідь прямо в браузері, без проксі в мережі. Швидко й точно, але лише Chrome та Edge і прив’язка до протоколу, що змінюється з кожною версією браузера. BiDi поступово дає аналог для всіх браузерів, та в Java-клієнті Selenium цей API ще рухається, тож я б не будував на ньому фундамент.
Option 2: CDP. For Chromium, Selenium offers NetworkInterceptor on top of DevTools: a route replaces the response inside the browser, with no proxy in the network path. Fast and precise, but only Chrome and Edge, and tied to a protocol that changes with each browser version. BiDi is gradually bringing an equivalent for all browsers, but that API in the Selenium Java client is still moving, so I would not build a foundation on it yet.
Варіант 3: стаб-сервер. WireMock чи MockServer підміняє зовнішній сервіс на рівні бекенду стенду. Тест не залежить від браузера, мок бачать і UI, і API-тести, але потрібна інфраструктура і конфігурація стенду.
Option 3: a stub server. WireMock or MockServer replaces an external service at the level of the stand's backend. The test does not depend on the browser, the mock is visible to both UI and API tests, but it needs infrastructure and stand configuration.
Як обираю. Для зовнішніх інтеграцій (платежі, карти) — стаб-сервер, це найстабільніше. Для точкових UI-сценаріїв помилок у локальному Chrome — CDP. Проксі Selenide — коли потрібні будь-які браузери і мережа це дозволяє. І завжди лишаю частину тестів на справжньому бекенді, бо мок підтверджує лише ту поведінку, яку я сам у нього заклав.
How I choose. For external integrations such as payments or maps, a stub server, because it is the most stable. For focused error scenarios in local Chrome, CDP. The Selenide proxy when any browser is needed and the network allows it. And I always keep some tests on the real backend, because a mock only confirms the behaviour I put into it myself.
// 1. Проксі Selenide (потрібні proxyEnabled і артефакт selenide-proxy)
Configuration.proxyEnabled = true;
open("/");
getSelenideProxy().responseMocker().mockText("empty-orders",
urlEndsWith(GET, "/api/orders"), () -> "[]");
open("/orders");
$(".empty-state").shouldHave(text("No orders yet"));
getSelenideProxy().responseMocker().reset("empty-orders");
// 2. CDP у Chromium через Selenium NetworkInterceptor
try (NetworkInterceptor interceptor = new NetworkInterceptor(
WebDriverRunner.getWebDriver(),
Route.matching(req -> req.getUri().endsWith("/api/orders"))
.to(() -> req -> new HttpResponse()
.setStatus(500)
.setContent(Contents.utf8String("{\"error\":\"boom\"}"))))) {
open("/orders");
$(".error-banner").shouldBe(visible);
}
// 1. The Selenide proxy (needs proxyEnabled and the selenide-proxy artifact)
Configuration.proxyEnabled = true;
open("/");
getSelenideProxy().responseMocker().mockText("empty-orders",
urlEndsWith(GET, "/api/orders"), () -> "[]");
open("/orders");
$(".empty-state").shouldHave(text("No orders yet"));
getSelenideProxy().responseMocker().reset("empty-orders");
// 2. CDP in Chromium through Selenium NetworkInterceptor
try (NetworkInterceptor interceptor = new NetworkInterceptor(
WebDriverRunner.getWebDriver(),
Route.matching(req -> req.getUri().endsWith("/api/orders"))
.to(() -> req -> new HttpResponse()
.setStatus(500)
.setContent(Contents.utf8String("{\"error\":\"boom\"}"))))) {
open("/orders");
$(".error-banner").shouldBe(visible);
}
getSelenideProxy() статично імпортується з WebDriverRunner, urlEndsWith — з RequestMatchers, а GET — з RequestMatcher.HttpMethod. Перший аргумент mockText — ім’я мока, за яким його потім знімають через reset. NetworkInterceptor реалізує AutoCloseable, тому try-with-resources знімає перехоплення після блоку; Route, HttpResponse і Contents — класи пакета org.openqa.selenium.remote.http.getSelenideProxy() is statically imported from WebDriverRunner, urlEndsWith from RequestMatchers, and GET from RequestMatcher.HttpMethod. The first argument of mockText is the mock's name, used later to remove it with reset. NetworkInterceptor implements AutoCloseable, so try-with-resources removes the interception after the block; Route, HttpResponse and Contents come from the org.openqa.selenium.remote.http package.Що це. CDP (Chrome DevTools Protocol) — внутрішній протокол Chromium: мережа, консоль, емуляція пристроїв, продуктивність. WebDriver BiDi — новий стандарт W3C на WebSocket, двонаправлений аналог для всіх браузерів. Selenium 4 підтримує обидва, а Selenide 7 використовує їх під капотом і дає частину можливостей готовими методами.
What it is. CDP (Chrome DevTools Protocol) is Chromium's internal protocol: network, console, device emulation, performance. WebDriver BiDi is the new W3C standard over WebSocket, a bidirectional equivalent for all browsers. Selenium 4 supports both, and Selenide 7 uses them under the hood and exposes some capabilities as ready methods.
Що Selenide робить сам. Режим FileDownloadMode.CDP скачує файли через DevTools, зокрема на Grid і Selenoid. З версії 7.12 Selenide вміє робити скріншоти через DevTools або BiDi і збирати логи браузера через BiDi, а Selenide.emulateDevice(...) вмикає мобільну емуляцію у вже відкритому браузері: для Chromium через CDP, для Firefox через BiDi. Для Firefox Selenide вмикає BiDi за замовчуванням.
What Selenide does itself. FileDownloadMode.CDP downloads files through DevTools, including on Grid and Selenoid. Since 7.12 Selenide can take screenshots through DevTools or BiDi and collect browser logs through BiDi, and Selenide.emulateDevice(...) turns on mobile emulation in an already open browser: through CDP for Chromium and through BiDi for Firefox. For Firefox, Selenide enables BiDi by default.
Що робиш сам. Усе інше — через сирий драйвер: HasDevTools для CDP-команд (емуляція повільної мережі, перехоплення запитів, метрики) і HasBiDi для подій BiDi, наприклад підписки на помилки JavaScript через LogInspector. Для Chrome BiDi вмикається capability webSocketUrl.
What you do yourself. Everything else goes through the raw driver: HasDevTools for CDP commands (slow network emulation, request interception, metrics) and HasBiDi for BiDi events, for example subscribing to JavaScript errors through LogInspector. For Chrome, BiDi is enabled with the webSocketUrl capability.
Трейдоф. CDP потужний, але лише для Chromium і прив’язаний до версії браузера: з кожним оновленням Selenium змінюються згенеровані пакети CDP, і код, що імпортує конкретну версію, ламається. Тому імпортуй devtools.latest або ховай CDP за власним інтерфейсом. BiDi — майбутнє, але набір модулів ще неповний і Selenoid підтримує його не всюди (Selenide навіть має обхід для таких випадків). Моє правило: спершу готовий метод Selenide, потім BiDi, CDP — коли потрібна саме функція Chromium.
Trade-off. CDP is powerful but Chromium-only and tied to the browser version: every Selenium update changes the generated CDP packages, and code that imports a specific version breaks. So import devtools.latest or hide CDP behind your own interface. BiDi is the future, but its set of modules is still incomplete and Selenoid does not support it everywhere (Selenide even has a workaround for such cases). My rule: first a ready Selenide method, then BiDi, and CDP when I need a Chromium-specific feature.
// готовий метод: мобільна емуляція (CDP для Chromium, BiDi для Firefox)
open("/");
Selenide.emulateDevice(Device.IPHONE_14);
$(".burger-menu").shouldBe(visible);
Selenide.resetEmulation();
// BiDi: збираємо помилки JavaScript під час сценарію
// для Chrome потрібна capability webSocketUrl = true
List<String> jsErrors = new CopyOnWriteArrayList<>();
try (LogInspector logs = new LogInspector(WebDriverRunner.getWebDriver())) {
logs.onJavaScriptException(e -> jsErrors.add(e.getText()));
open("/checkout");
$("#pay").click();
$(".receipt").shouldBe(visible);
}
assertThat(jsErrors).isEmpty();
// CDP: повільна мережа, лише локальний Chromium
ChromiumDriver chrome = (ChromiumDriver) WebDriverRunner.getWebDriver();
chrome.executeCdpCommand("Network.enable", Map.of());
chrome.executeCdpCommand("Network.emulateNetworkConditions", Map.of(
"offline", false,
"latency", 400,
"downloadThroughput", 50_000,
"uploadThroughput", 20_000));
open("/catalog");
$(".skeleton").should(disappear);
// a ready method: mobile emulation (CDP for Chromium, BiDi for Firefox)
open("/");
Selenide.emulateDevice(Device.IPHONE_14);
$(".burger-menu").shouldBe(visible);
Selenide.resetEmulation();
// BiDi: collect JavaScript errors during a scenario
// Chrome needs the capability webSocketUrl = true
List<String> jsErrors = new CopyOnWriteArrayList<>();
try (LogInspector logs = new LogInspector(WebDriverRunner.getWebDriver())) {
logs.onJavaScriptException(e -> jsErrors.add(e.getText()));
open("/checkout");
$("#pay").click();
$(".receipt").shouldBe(visible);
}
assertThat(jsErrors).isEmpty();
// CDP: slow network, local Chromium only
ChromiumDriver chrome = (ChromiumDriver) WebDriverRunner.getWebDriver();
chrome.executeCdpCommand("Network.enable", Map.of());
chrome.executeCdpCommand("Network.emulateNetworkConditions", Map.of(
"offline", false,
"latency", 400,
"downloadThroughput", 50_000,
"uploadThroughput", 20_000));
open("/catalog");
$(".skeleton").should(disappear);
Device — record Selenide з готовими профілями на кшталт IPHONE_14 чи PIXEL_7. LogInspector з пакета org.openqa.selenium.bidi.module підписується на події браузера і закривається в try-with-resources. CopyOnWriteArrayList потрібен, бо події приходять з іншого потоку. executeCdpCommand з ChromiumDriver надсилає сиру CDP-команду з назвою і картою параметрів; такий виклик не залежить від згенерованих класів конкретної версії, але приведення типу спрацює лише для локального Chrome чи Edge.Device is a Selenide record with ready profiles such as IPHONE_14 or PIXEL_7. LogInspector from the org.openqa.selenium.bidi.module package subscribes to browser events and is closed in try-with-resources. CopyOnWriteArrayList is needed because events arrive on another thread. executeCdpCommand on ChromiumDriver sends a raw CDP command as a name plus a parameter map; such a call does not depend on the generated classes of a specific version, but the cast only works for a local Chrome or Edge.Selenium. Стандарт W3C, справжні браузери включно з Safari, Grid, Appium для мобільних і величезна екосистема. Але це бібліотека автоматизації, а не тестовий інструмент: очікування, звіти, скріншоти й Page Object команда будує сама. Обираю, коли потрібна максимальна сумісність, екзотичні браузери чи вже є сильний внутрішній фреймворк.
Selenium. The W3C standard, real browsers including Safari, Grid, Appium for mobile and a huge ecosystem. But it is an automation library, not a testing tool: waits, reports, screenshots and Page Objects are built by the team itself. I pick it when maximum compatibility or exotic browsers are needed, or when a strong in-house framework already exists.
Selenide. Той самий Selenium, але з розумними очікуваннями, лінивими елементами, артефактами при падінні й коротким API. Поріг входу низький, код читабельний, інфраструктура (Grid, Selenoid, Appium) лишається та сама. Обмеження: Java і Kotlin, мережа й трасування — через проксі чи CDP, а не вбудовані.
Selenide. The same Selenium, but with smart waiting, lazy elements, artefacts on failure and a compact API. The entry barrier is low, the code is readable, and the infrastructure (Grid, Selenoid, Appium) stays the same. Limits: Java and Kotlin only, and network and tracing come through a proxy or CDP rather than being built in.
Playwright. Власний протокол до патчених збірок Chromium, Firefox і WebKit, автоочікування, ізольовані контексти браузера, вбудоване перехоплення мережі, trace viewer і codegen. Є Java-біндинг, але найкраща екосистема — у TypeScript: паралельність, фікстури й звіти дає саме @playwright/test. Ризики: WebKit — не справжній Safari, мобільні нативні застосунки не покриваються, а інфраструктура відрізняється від Selenium Grid.
Playwright. Its own protocol to patched builds of Chromium, Firefox and WebKit, auto-waiting, isolated browser contexts, built-in network interception, a trace viewer and codegen. There is a Java binding, but the best ecosystem is in TypeScript: parallelism, fixtures and reports come from @playwright/test. Risks: WebKit is not real Safari, native mobile apps are not covered, and the infrastructure differs from Selenium Grid.
Як я обираю. Команда на Java з наявним Grid або Selenoid і великим Selenium-набором — Selenide: мінімальна ціна міграції і швидкий виграш у стабільності. Нова команда, фронтенд на TypeScript, тести поруч із кодом застосунку і потреба в мережевих моках — Playwright. Потрібні справжній Safari, мобільні пристрої чи хмарні ферми на WebDriver — Selenium або Selenide. Вирішальні критерії — мова команди, наявна інфраструктура і те, хто підтримуватиме тести, а не бенчмарки швидкості.
How I choose. A Java team with an existing Grid or Selenoid and a big Selenium suite gets Selenide: the lowest migration cost and a quick win in stability. A new team with a TypeScript front end, tests living next to the application code and a need for network mocks gets Playwright. If real Safari, mobile devices or WebDriver-based cloud farms are required, Selenium or Selenide. The deciding criteria are the team's language, the existing infrastructure and who will maintain the tests, not speed benchmarks.
// Selenium: очікування пишемо самі
new WebDriverWait(driver, Duration.ofSeconds(4))
.until(ExpectedConditions.textToBe(By.cssSelector(".total"), "49.99"));
// Selenide: очікування вбудоване в перевірку
$(".total").shouldHave(exactText("49.99"));
// Playwright for Java: web-first assertion з автоочікуванням
assertThat(page.locator(".total")).hasText("49.99");
// Selenium: we write the wait ourselves
new WebDriverWait(driver, Duration.ofSeconds(4))
.until(ExpectedConditions.textToBe(By.cssSelector(".total"), "49.99"));
// Selenide: waiting is built into the assertion
$(".total").shouldHave(exactText("49.99"));
// Playwright for Java: a web-first assertion with auto-waiting
assertThat(page.locator(".total")).hasText("49.99");
ExpectedConditions.textToBe повертає функцію, яку WebDriverWait.until викликає до успіху чи таймауту. У Selenide те саме робить shouldHave з умовою exactText. У Playwright assertThat імпортується з PlaywrightAssertions, а hasText повторює перевірку локатора до свого таймауту — ідея та сама, що в Selenide, але на іншому протоколі.ExpectedConditions.textToBe returns a function that WebDriverWait.until calls until success or timeout. In Selenide, shouldHave with the exactText condition does the same. In Playwright, assertThat is imported from PlaywrightAssertions, and hasText retries the locator check until its timeout: the same idea as Selenide, on a different protocol.Принцип. Не переписувати все за один спринт. Selenide працює поверх того самого WebDriver, тому старі й нові тести можуть жити в одному прогоні, і міграцію можна вести поступово, починаючи з найнестабільніших тестів.
Principle. Do not rewrite everything in one sprint. Selenide runs on top of the same WebDriver, so old and new tests can live in one run, and the migration can proceed gradually, starting with the flakiest tests.
Кроки. Перше — підключити Selenide і віддати йому створення драйвера: або WebDriverProvider, що повторює стару фабрику з усіма опціями, або тимчасово WebDriverRunner.setWebDriver(driver), щоб старий і новий код ділили одну сесію. Друге — старі хелпери, що приймають WebDriver, отримують його з WebDriverRunner.getWebDriver(). Третє — переписати Page Object: @FindBy з WebElement на SelenideElement, прибрати PageFactory і всі WebDriverWait, Thread.sleep та самописні ретраї. Четверте — замінити асерти над getter-ами на умови: assertEquals(text, el.getText()) стає el.shouldHave(exactText(text)).
Steps. First, add Selenide and hand it driver creation: either a WebDriverProvider that reproduces the old factory with all its options, or temporarily WebDriverRunner.setWebDriver(driver) so old and new code share one session. Second, old helpers that take a WebDriver get it from WebDriverRunner.getWebDriver(). Third, rewrite Page Objects: @FindBy from WebElement to SelenideElement, remove PageFactory and every WebDriverWait, Thread.sleep and home-made retry. Fourth, replace assertions over getters with conditions: assertEquals(text, el.getText()) becomes el.shouldHave(exactText(text)).
Як міряти успіх. До міграції зняти базову лінію: частку flaky-падінь, час прогону, кількість рядків хелперів очікування. Після кожної хвилі порівнювати. Це аргумент для менеджменту і спосіб помітити, якщо переписаний тест став повільнішим через завеликий таймаут.
How to measure success. Take a baseline before the migration: the share of flaky failures, run time and the number of lines in wait helpers. Compare after every wave. It is an argument for management and a way to notice when a rewritten test got slower because of an oversized timeout.
Підводні камені. Змішування неявного очікування Selenium зі смарт-очікуваннями Selenide множить таймаути, тож implicitlyWait треба прибрати одразу. Статичний драйвер у старому коді ламає паралельність, бо Selenide тримає драйвер на потік. Семантика умов відрізняється: text() шукає підрядок без урахування регістру, і дослівний перенос асертів може послабити перевірки. І перейменування в 7.0 (WebElementCondition, Container, *Error) треба врахувати, якщо переходиш зі старої шостої версії.
Pitfalls. Mixing Selenium's implicit wait with Selenide's smart waiting multiplies timeouts, so implicitlyWait must go straight away. A static driver in old code breaks parallelism, because Selenide keeps a driver per thread. Condition semantics differ: text() matches a case-insensitive substring, and a literal port of assertions can weaken checks. And the 7.0 renames (WebElementCondition, Container, *Error) must be accounted for if you come from an old version 6.
// стара фабрика стає WebDriverProvider, Selenide керує життєвим циклом
public class LegacyChromeProvider implements WebDriverProvider {
@Override
public WebDriver createDriver(Capabilities capabilities) {
ChromeOptions options = LegacyOptions.chrome(); // старі налаштування без змін
options.merge(capabilities);
return new ChromeDriver(options);
}
}
// Configuration.browser = LegacyChromeProvider.class.getName();
// було
public class CartPageOld {
@FindBy(css = ".total") WebElement total;
public String total() {
new WebDriverWait(driver, Duration.ofSeconds(10)).until(visibilityOf(total));
return total.getText();
}
}
// assertEquals("49.99", cartPage.total());
// стало
public class CartPage {
private final SelenideElement total = $(".total");
public void shouldHaveTotal(String expected) {
total.shouldHave(exactText(expected));
}
}
// the old factory becomes a WebDriverProvider, Selenide manages the lifecycle
public class LegacyChromeProvider implements WebDriverProvider {
@Override
public WebDriver createDriver(Capabilities capabilities) {
ChromeOptions options = LegacyOptions.chrome(); // old settings unchanged
options.merge(capabilities);
return new ChromeDriver(options);
}
}
// Configuration.browser = LegacyChromeProvider.class.getName();
// before
public class CartPageOld {
@FindBy(css = ".total") WebElement total;
public String total() {
new WebDriverWait(driver, Duration.ofSeconds(10)).until(visibilityOf(total));
return total.getText();
}
}
// assertEquals("49.99", cartPage.total());
// after
public class CartPage {
private final SelenideElement total = $(".total");
public void shouldHaveTotal(String expected) {
total.shouldHave(exactText(expected));
}
}
WebDriverProvider.createDriver(Capabilities) отримує capabilities, які зібрав Selenide, і merge поєднує їх зі старими опціями. Ім’я класу провайдера записується в Configuration.browser. У новому Page Object метод-getter замінено методом-перевіркою: shouldHaveTotal чекає потрібний текст сам, тож WebDriverWait і assertEquals зникають.WebDriverProvider.createDriver(Capabilities) receives the capabilities Selenide assembled, and merge combines them with the old options. The provider's class name goes into Configuration.browser. In the new Page Object the getter is replaced by a checking method: shouldHaveTotal waits for the right text itself, so WebDriverWait and assertEquals disappear.Ситуація. Прогін червоніє через раз, розробники звикли тиснути «перезапустити», і червоний білд уже нікого не лякає. Лагодити по одному тесту безглуздо, поки немає даних і правил.
The situation. Every other run goes red, developers are used to pressing "rerun", and a red build no longer scares anyone. Fixing tests one by one is pointless while there is no data and no rules.
Крок 1: виміряти. Зібрати історію прогонів з Allure чи JUnit XML і порахувати для кожного тесту частку падінь, що зникли при перезапуску. Згрупувати причини за повідомленням Selenide: ElementNotFound вказує на локатор чи повільне завантаження, ElementShould з фактичним значенням — на гонку даних, помилки драйвера й сесії — на інфраструктуру.
Step 1: measure. Collect run history from Allure or JUnit XML and compute, for each test, the share of failures that vanished on rerun. Group causes by the Selenide message: ElementNotFound points at a locator or slow loading, ElementShould with an actual value at a data race, and driver or session errors at infrastructure.
Крок 2: ізолювати. Тести з частотою падінь вище порогу переносять у карантин: вони запускаються, але не блокують злиття, і кожен має задачу з власником і терміном. Автоматичний retry допустимий лише як тимчасовий захід і з позначкою в звіті, інакше він ховає справжні баги застосунку.
Step 2: isolate. Tests whose failure rate is above a threshold go into quarantine: they still run but do not block merges, and each has a ticket with an owner and a deadline. Automatic retry is acceptable only as a temporary measure and must be flagged in the report, otherwise it hides real application bugs.
Крок 3: усунути класи причин. Спільні тестові дані замінити на унікальні, згенеровані через API. Вимкнути анімації на тестовому стенді. Прибрати sleep, getter-асерти і глобальне підвищення timeout. Додати стабільні атрибути data-testid разом із фронтендом. Зафіксувати розмір вікна і версію браузера.
Step 3: remove classes of causes. Replace shared test data with unique data generated through the API. Turn off animations on the test stand. Remove sleep, getter-based assertions and global increases of timeout. Add stable data-testid attributes together with the front-end team. Pin the window size and the browser version.
Крок 4: закріпити. Правила в код-рев’ю і статичні перевірки (заборона Thread.sleep і implicitlyWait у тестовому коді), метрика flaky-частки на дашборді і правило «новий тест проганяється 20 разів перед злиттям». Стабільність — властивість процесу, а не героїзму одного автоматизатора.
Step 4: lock it in. Code review rules and static checks (no Thread.sleep or implicitlyWait in test code), a flaky-rate metric on a dashboard, and a rule that a new test runs 20 times before merge. Stability is a property of the process, not the heroism of one automation engineer.
// тест у карантині: має задачу і власника, не блокує злиття
@Tag("quarantine") // QA-1234, власник: orders team, до 15.10
@Test
void refundAppearsInHistory() { /* ... */ }
// перед злиттям новий тест має 20 разів поспіль пройти
@RepeatedTest(20)
void couponIsAppliedOnce() {
open("/cart");
$("#coupon").setValue("SAVE10").pressEnter();
$(".discount").shouldHave(exactText("-10%"));
}
// Maven: основний прогін без карантину
// mvn test -Dgroups='!quarantine'
// окремий job: mvn test -Dgroups=quarantine
// a quarantined test: it has a ticket and an owner, it does not block merges
@Tag("quarantine") // QA-1234, owner: orders team, due 15.10
@Test
void refundAppearsInHistory() { /* ... */ }
// before merge a new test must pass 20 times in a row
@RepeatedTest(20)
void couponIsAppliedOnce() {
open("/cart");
$("#coupon").setValue("SAVE10").pressEnter();
$(".discount").shouldHave(exactText("-10%"));
}
// Maven: the main run without quarantine
// mvn test -Dgroups='!quarantine'
// a separate job: mvn test -Dgroups=quarantine
@Tag з JUnit 5 позначає групу тестів, яку Maven Surefire фільтрує параметром -Dgroups; вираз !quarantine означає «усе, крім карантину». @RepeatedTest(20) запускає той самий тест 20 разів, і одне падіння вже сигнал, що тест нестабільний. Коментар біля тегу — мінімальна домовленість: номер задачі, власник і термін.@Tag from JUnit 5 marks a group of tests that Maven Surefire filters with -Dgroups; the expression !quarantine means "everything except quarantine". @RepeatedTest(20) runs the same test 20 times, and a single failure is already a signal of flakiness. The comment next to the tag is the minimum agreement: a ticket number, an owner and a deadline.Шари. Я розділяю фреймворк на чотири рівні. Конфігурація: один клас, що читає selenide.properties і системні властивості, плюс профілі середовищ. Драйвер: WebDriverProvider чи налаштування Configuration.remote для Selenoid або Grid, без статичних драйверів у коді. Модель UI: сторінки й компоненти з кореневим елементом. Тести: лише сценарії і перевірки бізнес-правил, без селекторів.
Layers. I split the framework into four levels. Configuration: one class reading selenide.properties and system properties, plus environment profiles. Driver: a WebDriverProvider or Configuration.remote settings for Selenoid or Grid, with no static drivers in the code. UI model: pages and components with a root element. Tests: only scenarios and business-rule checks, no selectors.
Інфраструктурні сервіси. API-клієнти для створення даних і швидкого логіну, генератори унікальних тестових даних, JUnit-розширення, яке підключає Allure-лістенер, чистить стан і за потреби закриває браузер. Спільна бібліотека публікується як окремий артефакт з версіями, а команди підключають її залежністю, а не копіюють код.
Infrastructure services. API clients for creating data and fast login, generators of unique test data, and a JUnit extension that registers the Allure listener, cleans state and closes the browser when needed. The shared library is published as a separate versioned artifact, and teams consume it as a dependency instead of copying code.
Домовленості. Локатори на data-testid, узгоджені з фронтендом. Жодного sleep, getter-асертів і глобального збільшення таймауту. Кожен тест незалежний і може йти паралельно. Назви тестів описують поведінку. Звіт Allure з кроками, скріншотами й page source доступний з кожного CI-прогону за посиланням.
Conventions. Locators on data-testid, agreed with the front-end team. No sleep, no getter-based assertions, no global timeout increases. Every test is independent and can run in parallel. Test names describe behaviour. An Allure report with steps, screenshots and page source is reachable by link from every CI run.
Трейдофи. Спільний фреймворк прискорює старт нових команд, але стає вузьким місцем, якщо його змінює одна людина, тому потрібні власники, changelog і семантичні версії. Надто товста абстракція (власні обгортки над кожним методом Selenide) вбиває головну перевагу — простоту API, і новачкам доводиться вчити «наш діалект». Я тримаю обгортки мінімальними: розширення, конфігурація, дані й компоненти, а в тестах лишається звичайний Selenide.
Trade-offs. A shared framework speeds up new teams but becomes a bottleneck if one person changes it, so it needs owners, a changelog and semantic versions. Too thick an abstraction (custom wrappers over every Selenide method) kills the main advantage, the simplicity of the API, and newcomers have to learn "our dialect". I keep wrappers minimal: extensions, configuration, data and components, and tests use plain Selenide.
public class UiTestExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback {
@Override
public void beforeAll(ExtensionContext context) {
TestConfig.apply(); // baseUrl, remote, browserSize із профілю середовища
}
@Override
public void beforeEach(ExtensionContext context) {
// лістенери SelenideLogger живуть у ThreadLocal, тому реєструємо в потоці тесту
if (!SelenideLogger.hasListener("AllureSelenide")) {
SelenideLogger.addListener("AllureSelenide",
new AllureSelenide().screenshots(true).savePageSource(true));
}
}
@Override
public void afterEach(ExtensionContext context) {
if (WebDriverRunner.hasWebDriverStarted()) {
Selenide.cookies().clear();
Selenide.localStorage().clear();
}
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(UiTestExtension.class)
public @interface UiTest { }
@UiTest
class CheckoutTest {
@Test
void guestCanPayByCard() { /* лише сценарій і перевірки */ }
}
public class UiTestExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback {
@Override
public void beforeAll(ExtensionContext context) {
TestConfig.apply(); // baseUrl, remote, browserSize from the environment profile
}
@Override
public void beforeEach(ExtensionContext context) {
// SelenideLogger listeners live in a ThreadLocal, so register them on the test thread
if (!SelenideLogger.hasListener("AllureSelenide")) {
SelenideLogger.addListener("AllureSelenide",
new AllureSelenide().screenshots(true).savePageSource(true));
}
}
@Override
public void afterEach(ExtensionContext context) {
if (WebDriverRunner.hasWebDriverStarted()) {
Selenide.cookies().clear();
Selenide.localStorage().clear();
}
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(UiTestExtension.class)
public @interface UiTest { }
@UiTest
class CheckoutTest {
@Test
void guestCanPayByCard() { /* only the scenario and its checks */ }
}
UiTestExtension реалізує колбеки JUnit 5: beforeAll налаштовує конфігурацію, beforeEach реєструє Allure-лістенер у SelenideLogger у потоці конкретного тесту (лістенери зберігаються в ThreadLocal, тож у паралельному прогоні реєстрація в beforeAll загубила б кроки інших потоків), а afterEach чистить стан браузера, якщо той уже запущений. Власна анотація @UiTest з мета-анотацією @ExtendWith дозволяє підключити все одним рядком. TestConfig — умовний клас проєкту, що читає профіль середовища.UiTestExtension implements JUnit 5 callbacks: beforeAll sets up configuration, beforeEach registers the Allure listener in SelenideLogger on the test's own thread (listeners are stored in a ThreadLocal, so registering in beforeAll would lose other threads' steps in a parallel run), and afterEach cleans browser state if it has already started. The custom @UiTest annotation with the @ExtendWith meta-annotation wires everything in one line. TestConfig stands for a project class that reads the environment profile.Очікування. Thread.sleep чи Selenide.sleep замість умови; WebDriverWait поверх Selenide, що дублює вбудоване очікування; глобальне підвищення Configuration.timeout до 30 секунд «щоб не падало», яке робить кожне справжнє падіння повільним.
Waiting. Thread.sleep or Selenide.sleep instead of a condition; WebDriverWait on top of Selenide, duplicating the built-in wait; a global increase of Configuration.timeout to 30 seconds "so it does not fail", which makes every real failure slow.
Перевірки. Асерти над getter-ами: assertEquals("5", $(".count").getText()) або assertTrue($(".ok").isDisplayed()) не чекають і дають flaky-падіння. Надто слабкі умови: text("1") пройде і для «10», і для «21», хоча автор мав на увазі точне значення. Перевірки видимості без перевірки змісту. Тест без жодної перевірки, який лише клікає.
Assertions. Assertions over getters: assertEquals("5", $(".count").getText()) or assertTrue($(".ok").isDisplayed()) do not wait and produce flaky failures. Conditions that are too weak: text("1") passes for "10" and for "21", although the author meant an exact value. Visibility checks without checking content. A test with no assertion at all that only clicks.
Локатори й структура. Довгі XPath за позицією на кшталт //div[3]/div[2]/span, CSS-класи з хешами CSS-модулів, пошук від кореня документа всередині компонента. Селектори прямо в тестах замість Page Object. Сирий WebElement, збережений у змінну.
Locators and structure. Long positional XPath such as //div[3]/div[2]/span, CSS classes with CSS-module hashes, searching from the document root inside a component. Selectors directly in tests instead of Page Objects. A raw WebElement kept in a variable.
Дані й ізоляція. Тести, що залежать від порядку запуску чи від даних іншого тесту; хардкодні логіни, спільні на весь прогін; відсутність прибирання. Статичний стан і мутація Configuration посеред тесту, що в паралельному прогоні змінює поведінку сусідів.
Data and isolation. Tests that depend on run order or on another test's data; hard-coded logins shared by the whole run; no cleanup. Static state and mutating Configuration in the middle of a test, which changes the neighbours' behaviour in a parallel run.
Як подаю зауваження. Не лише «так не можна», а з конкретною заміною і причиною: shouldHave(exactText("5")) замість getter-а, бо так тест дочекається оновлення лічильника. Повторювані зауваження перетворюю на правило лінтера чи ArchUnit-тест, щоб рев’ю не ловило одне й те саме щотижня.
How I phrase feedback. Not just "do not do this" but with a concrete replacement and a reason: shouldHave(exactText("5")) instead of a getter, because then the test waits for the counter to update. Recurring comments turn into a linter rule or an ArchUnit test, so review does not catch the same thing every week.
// погано: getter без очікування
assertEquals("5", $(".cart-count").getText());
// добре
$(".cart-count").shouldHave(exactText("5"));
// погано: пауза навмання
Selenide.sleep(3000);
$("#submit").click();
// добре: чекаємо на стан, а не на час
$(".spinner").should(disappear);
$("#submit").click();
// погано: позиційний XPath і хеш CSS-модуля
$x("//div[3]/div[2]/button").click();
$(".Button_primary__x7Qz2").click();
// добре: стабільний тестовий атрибут
$(byTestId("checkout-submit")).click();
// погано: зміна глобальної конфігурації посеред тесту
Configuration.timeout = 30_000;
// добре: довший таймаут лише для однієї повільної перевірки
$(".report").shouldBe(visible, Duration.ofSeconds(30));
// bad: a getter without waiting
assertEquals("5", $(".cart-count").getText());
// good
$(".cart-count").shouldHave(exactText("5"));
// bad: a random pause
Selenide.sleep(3000);
$("#submit").click();
// good: wait for a state, not for time
$(".spinner").should(disappear);
$("#submit").click();
// bad: a positional XPath and a CSS-module hash
$x("//div[3]/div[2]/button").click();
$(".Button_primary__x7Qz2").click();
// good: a stable test attribute
$(byTestId("checkout-submit")).click();
// bad: changing global configuration in the middle of a test
Configuration.timeout = 30_000;
// good: a longer timeout only for one slow check
$(".report").shouldBe(visible, Duration.ofSeconds(30));
should(disappear) чекає, доки спінер зникне або його не стане в DOM. byTestId — селектор із Selectors (з 7.12), що шукає за атрибутом data-test-id; назву атрибута можна змінити властивістю selenide.test-id.attribute, наприклад на data-testid. shouldBe(visible, Duration.ofSeconds(30)) збільшує таймаут лише для цього виклику, не чіпаючи глобальне значення.should(disappear) waits until the spinner is hidden or gone from the DOM. byTestId is a selector from Selectors (since 7.12) that searches by the data-test-id attribute; the attribute name can be changed with the selenide.test-id.attribute property, for example to data-testid. shouldBe(visible, Duration.ofSeconds(30)) raises the timeout for this call only, leaving the global value alone.