← Valtive QA Quiz quiz.valtive.io/notes/selenide valtive.io
📖 Про цей гайд📖 About this guide
Для когоWho it's for
Для Java-автоматизаторів, які готуються до співбесіди на QA Automation або SDET і пишуть UI-тести на Selenide 7 з JUnit 5 чи TestNG. Картки пояснюють не лише синтаксис, а й чому Selenide поводиться саме так: чому перевірка сама чекає, чим text() відрізняється від exactText(), як скачати файл у CI, як ганяти тести паралельно на Selenoid і як мігрувати старий Selenium-набір. Кожне зі 107 питань квізу Selenide покрите щонайменше однією карткою, а дрібні питання про сусідні методи зібрані в одну картку.
For Java automation engineers preparing for a QA Automation or SDET interview who write UI tests with Selenide 7 and JUnit 5 or TestNG. The cards explain not only the syntax but why Selenide behaves the way it does: why an assertion waits by itself, how text() differs from exactText(), how to download a file in CI, how to run tests in parallel on Selenoid and how to migrate an old Selenium suite. All 107 questions of the Selenide quiz are covered by at least one card, and small questions about neighbouring methods are merged into one card.
Як користуватисьHow to use it
Фільтруй картки за рівнем або розділом. Відповідь іде за схемою «Що це → Коли → Приклад → Підводні камені», а код під нею написаний для Selenide 7.x і JUnit 5 зі статичними імпортами — його можна вставити у свій проєкт і запустити. Senior-картки наприкінці розділів — це сценарії й трейдофи, а не пошук параметра конфігу. Наостанок закрий текст і перекажи відповідь своїми словами за блоком 🗣 Як читати вголос: так ти тренуєш живу розмову з інтерв'юером, а не впізнавання правильного варіанта.
Filter cards by level or by section. Each answer follows "What it is → When → Example → Pitfalls", and the code under it targets Selenide 7.x and JUnit 5 with static imports, so you can paste it into your project and run it. Senior cards at the end of sections are scenarios and trade-offs rather than config lookups. Finally, hide the text and retell the answer in your own words using the 🗣 How to say it out loud block: that trains a live conversation with an interviewer rather than recognising the right option.
Три якорі — з них випливає більшість відповідейThree anchors — most answers follow from them
ЯКІР 1 · Перевірка сама чекаєANCHOR 1 · An assertion waits by itself
Кожен should-метод і кожна дія повторюють перевірку до Configuration.timeout (4 секунди за замовчуванням). Тому в Selenide-коді немає WebDriverWait і sleep, а таймаут підбирають під реальну поведінку застосунку, а не під «щоб не падало».
Every should method and every action retries its check until Configuration.timeout (4 seconds by default). That is why Selenide code has no WebDriverWait and no sleep, and the timeout is tuned to real application behaviour rather than to "make it pass".
ЯКІР 2 · Елемент — це пошук, а не посиланняANCHOR 2 · An element is a search, not a reference
$("#login") нічого не шукає, доки ти не викличеш дію чи перевірку, і шукає заново при кожному виклику. Звідси відсутність StaleElementReferenceException, дешеві поля в Page Object і колекції, які завжди відображають поточний DOM.
$("#login") finds nothing until you call an action or an assertion, and it searches again on every call. That is why there is no StaleElementReferenceException, why Page Object fields are cheap and why collections always reflect the current DOM.
ЯКІР 3 · Падіння має пояснювати себеANCHOR 3 · A failure must explain itself
Selenide сам робить скріншот і зберігає page source, а повідомлення про помилку показує локатор, умову й фактичне значення. Додай as(), Allure і артефакти CI — і більшість падінь розбираються без повторного запуску.
Selenide takes a screenshot and saves the page source on its own, and the error message shows the locator, the condition and the actual value. Add as(), Allure and CI artifacts, and most failures can be analysed without a rerun.
73
питаньquestions
3
рівніlevels
—
voice-блокиvoice blocks
107/107
питань квізу покритоquiz questions covered
📚 10 РОЗДІЛІВ📚 10 SECTIONS
—
Junior
—
Middle
—
Senior
▶ 1 7
1 Що таке Selenide і що він додає поверх Selenium WebDriver? What is Selenide, and what does it add on top of Selenium WebDriver?
Основи Basics Junior ▶

Що це. 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 і в Selenide
The same scenario in Selenium and in Selenide
// 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"));
🔧 Розбір синтаксису: У Selenium очікування живе окремо від пошуку: WebDriverWait і ExpectedConditions треба писати там, де ти здогадався, що елемент може запізнитися. У Selenide $ повертає лінивий SelenideElement, а setValue, click і shouldHave самі чекають до Configuration.timeout. exactText — умова з класу Condition, яку зазвичай імпортують статично.
🔧 Syntax breakdown: In Selenium, waiting lives separately from lookup: you add 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.
💡 Простими словами: Selenium — це мотор, Selenide — автомобіль з коробкою-автоматом. Їде той самий мотор, але передачі перемикати не треба.
💡 In plain words: Selenium is the engine and Selenide is a car with an automatic gearbox. The same engine does the driving, you just stop shifting gears.
🗣 Як читати вголос Для мене Selenide — це тонка обгортка над WebDriver, яка прибирає три головні болі: очікування, застарілі елементи й керування браузером. Я пишу $ з селектором і одразу дію або перевірку, а бібліотека сама повторює спробу до таймауту. При падінні я отримую скріншот, HTML і зрозуміле повідомлення, тож не треба писати свій фреймворк-обгортку. Але я пам’ятаю, що під капотом той самий Selenium, тож Grid, драйвери й сирий WebDriver нікуди не зникають.
🗣 How to say it out loud To me Selenide is a thin layer over WebDriver that removes three headaches: waiting, stale elements and browser management. I write $ with a selector followed by an action or an assertion, and the library keeps retrying until the timeout. When something fails I get a screenshot, the HTML and a readable message, so I do not have to build my own wrapper framework. But I keep in mind that it is still Selenium underneath, so Grid, drivers and the raw WebDriver are all still there.
2 Як підключити Selenide до проєкту і написати перший тест на JUnit 5? How do you add Selenide to a project and write a first JUnit 5 test?
Основи Basics Junior ▶

Що потрібно. Основний артефакт — 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.

Залежність Maven і мінімальний тест
The Maven dependency and a minimal test
// 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() — ланцюжок: кожна дія повертає той самий елемент.
🔧 Syntax breakdown: The artifact coordinates are 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.
💡 Простими словами: Одна залежність, два статичні імпорти і open() — цього досить, щоб перший тест запрацював.
💡 In plain words: One dependency, two static imports and open() are enough to get a first test running.
🗣 Як читати вголос Я додаю одну залежність com.codeborne:selenide у тестовому скоупі і не оголошую Selenium окремо, щоб не зловити конфлікт версій. Сьома версія потребує сімнадцятої Java. Далі пишу звичайний клас JUnit 5 зі статичними імпортами Selenide і Condition, викликаю open з адресою, а браузер піднімається сам. Драйвер я ніде не створюю і не закриваю вручну, бо цим займається бібліотека.
🗣 How to say it out loud I add a single com.codeborne:selenide dependency in test scope and I do not declare Selenium separately, so I do not end up with a version conflict. Version seven needs Java seventeen. Then I write a plain JUnit 5 class with static imports of Selenide and Condition, call open with a URL, and the browser starts on its own. I never create or quit the driver by hand, because the library takes care of that.
3 Як відкривати сторінки й навігувати в Selenide: open, baseUrl, refresh, back? How do you open pages and navigate in Selenide: open, baseUrl, refresh, back?
Основи Basics Junior ▶

Що це. Навігацію дають статичні методи класу 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.

Відносні шляхи, перезавантаження та історія
Relative paths, reload and history
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.
🔧 Syntax breakdown: 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.
💡 Простими словами: У тестах пиши лише шлях, а адресу стенду тримай в одному місці конфігурації.
💡 In plain words: Write only the path in tests and keep the stand address in one place in the configuration.
🗣 Як читати вголос Я відкриваю сторінки через open, і майже завжди з відносним шляхом, бо адреса стенду лежить у baseUrl і перемикається системною властивістю в CI. Для навігації є refresh, back і forward. Refresh я люблю як окрему перевірку: після перезавантаження кошик чи фільтри мають лишитися на місці. Чекати після навігації окремо не потрібно, бо наступна перевірка сама дочекається свого елемента.
🗣 How to say it out loud I open pages with open, and almost always with a relative path, because the stand address sits in baseUrl and CI switches it with a system property. For navigation there are refresh, back and forward. I like refresh as a check in its own right: after a reload the cart or the filters must still be there. There is no need for a separate wait after navigation, since the next assertion waits for its own element.
4 Що таке SelenideElement і чому пошук елемента в Selenide лінивий? What is a SelenideElement, and why is element lookup in Selenide lazy?
Основи Basics Junior ▶

Що це. 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.

Оголошення без пошуку і пошук при кожній дії
A declaration without lookup, and a lookup on every action
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(...)) знову шукає вузол і порівнює текст, повторюючи спробу до таймауту.
🔧 Syntax breakdown: Fields of type 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.
💡 Простими словами: SelenideElement — це не сам елемент, а записана адреса, за якою його щоразу шукають.
💡 In plain words: A SelenideElement is not the element itself but a saved address used to look it up every time.
🗣 Як читати вголос SelenideElement я пояснюю як проксі над WebElement, яке пам’ятає лише селектор. Коли я пишу $ з CSS, у браузері нічого не відбувається, а пошук стається в момент кліку чи перевірки і повторюється щоразу. Завдяки цьому елементи спокійно живуть полями Page Object і переживають перемальовування React. Мінус теж є: помилку в селекторі я побачу лише на першій дії й після таймауту, тому повідомлення про падіння завжди читаю уважно.
🗣 How to say it out loud I explain SelenideElement as a proxy over WebElement that only remembers a selector. When I write $ with a CSS selector, nothing happens in the browser; the lookup happens at the moment of a click or an assertion and is repeated every time. Thanks to that, elements can live as Page Object fields and survive React re-renders. There is a downside too: I only see a broken selector on the first action and after the timeout, so I always read the failure message carefully.
5 Як Selenide захищає від StaleElementReferenceException і коли вона все ж може виникнути? How does Selenide protect you from StaleElementReferenceException, and when can it still happen?
Основи Basics Middle ▶

Що це. 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.

Безпечний ланцюжок і небезпечне збереження сирого елемента
A safe chain versus keeping a raw element
// безпечно: кожна перевірка шукає рядок і статус заново
$$("#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-об’єкт без очікувань і повторів.
🔧 Syntax breakdown: 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.
💡 Простими словами: Поки працюєш через $ і should, stale-помилок немає. Щойно збережеш сирий WebElement, захист закінчується.
💡 In plain words: As long as you go through $ and should, there are no stale errors. Once you keep a raw WebElement, the protection ends.
🗣 Як читати вголос Stale-помилка виникає, коли тримаєш посилання на вузол, якого вже немає в DOM. Selenide її гасить, бо зберігає селектор, а не вузол: при stale він заново шукає елемент і повторює дію до таймауту, причому для вкладених ланцюжків теж. Коли на проєкті я все ж бачу цю помилку, першим ділом шукаю місце, де хтось дістав сирий WebElement через toWebElement чи JavaScript і зберіг його в змінну. Друге, що перевіряю, — чи не став селектор після перемальовування знаходити інший елемент.
🗣 How to say it out loud A stale error happens when you hold a reference to a node that is no longer in the DOM. Selenide suppresses it because it keeps the selector rather than the node: on a stale error it looks the element up again and repeats the action until the timeout, nested chains included. When I still see this error on a project, the first thing I look for is a place where someone took a raw WebElement via toWebElement or JavaScript and kept it in a variable. The second thing I check is whether the selector started matching a different element after a re-render.
6 Хто відкриває й закриває браузер у Selenide і навіщо closeWebDriver, @AfterAll і holdBrowserOpen? Who opens and closes the browser in Selenide, and what are closeWebDriver, @AfterAll and holdBrowserOpen for?
Основи Basics Middle ▶

Як це влаштовано. Браузер стартує ліниво, при першому 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.

Три стратегії життєвого циклу браузера
Three browser lifecycle strategies
// 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().
🔧 Syntax breakdown: 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.
💡 Простими словами: За замовчуванням браузер один на потік і закривається сам. closeWebDriver — лише коли свідомо хочеш почати з чистого аркуша.
💡 In plain words: By default there is one browser per thread and it closes by itself. Use closeWebDriver only when you deliberately want a clean slate.
🗣 Як читати вголос У Selenide браузер відкривається при першому open і живе один на потік, а наприкінці прогону бібліотека закриває його сама. closeWebDriver я викликаю свідомо: у AfterAll, щоб швидше звільнити слот на Grid, або через BrowserPerTestStrategyExtension, коли тестам потрібна повна ізоляція. Якщо ж браузер спільний, я чищу cookies і localStorage між тестами. А holdBrowserOpen я вже не використовую, бо він deprecated і лишав драйвер висіти, тож для налагодження ставлю брейкпоінт.
🗣 How to say it out loud In Selenide the browser opens on the first open call and lives one per thread, and at the end of the run the library closes it itself. I call closeWebDriver on purpose: in AfterAll to free a Grid slot sooner, or through BrowserPerTestStrategyExtension when tests need full isolation. If the browser is shared, I clear cookies and localStorage between tests. I no longer use holdBrowserOpen, because it is deprecated and left the driver hanging, so for debugging I set a breakpoint instead.
7 Як дістати сирий WebDriver із Selenide і коли це виправдано? How do you get the raw WebDriver from Selenide, and when is that justified?
Основи Basics Middle ▶

Що це. Клас 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.

Сирий драйвер для DevTools і власний драйвер через setWebDriver
The raw driver for DevTools and your own driver via setWebDriver
// дістати драйвер поточного потоку
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().
🔧 Syntax breakdown: 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 не вміє, і повертайся до $ одразу після цього.
💡 In plain words: The raw driver is an emergency exit. Use it for what Selenide cannot do, and go back to $ right afterwards.
🗣 Як читати вголос Сирий драйвер я беру через WebDriverRunner.getWebDriver, коли Selenide чогось не обгортає: CDP чи BiDi, специфічні Actions, бібліотека на кшталт axe, яка приймає WebDriver. Пам’ятаю, що такий код уже не чекає і не захищений від stale. Зворотний метод setWebDriver використовую рідко: його треба викликати до першого open, і тоді закривати драйвер доведеться самому. Якщо потрібно просто кастомно створити браузер, я пишу WebDriverProvider.
🗣 How to say it out loud I take the raw driver with WebDriverRunner.getWebDriver when Selenide does not wrap something: CDP or BiDi, special Actions, or a library like axe that takes a WebDriver. I remember that such code no longer waits and is not protected from stale elements. I rarely use the reverse method, setWebDriver: it has to be called before the first open, and then closing the driver becomes my job. If I just need a custom browser setup, I write a WebDriverProvider.
▶ 2 6
8 Чим відрізняються $, $$ і $x у Selenide? What is the difference between $, $$ and $x in Selenide?
Локатори Locators Junior ▶

Що це. Це три статичні методи класу 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.

Один елемент, колекція і XPath
One element, a collection and XPath
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.
🔧 Syntax breakdown: $ 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.
💡 Простими словами: Один долар — один елемент, два долари — список, x — той самий пошук мовою XPath.
💡 In plain words: One dollar means one element, two dollars mean a list, and x means the same search written in XPath.
🗣 Як читати вголос Я запам’ятовую це просто: один долар дає мені один SelenideElement, два долари — колекцію ElementsCollection, а суфікс x означає, що селектор написаний на XPath. За замовчуванням я пишу CSS, бо його легше читати, а XPath лишаю для рідкісних випадків. Ще я стежу, щоб селектор для одного елемента був унікальним, бо інакше Selenide тихо візьме перший збіг, і тест перевірятиме зовсім інший вузол.
🗣 How to say it out loud I remember it simply: one dollar gives me one SelenideElement, two dollars give me an ElementsCollection, and the x suffix means the selector is XPath. By default I write CSS because it is easier to read, and I keep XPath for rare cases. I also make sure a single-element selector is unique, otherwise Selenide quietly takes the first match and the test ends up checking a completely different node.
9 Які селектори Selenide дає понад CSS і XPath? Which selectors does Selenide offer beyond CSS and XPath?
Локатори Locators Junior ▶

Що це. Клас 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.

Текст, посилання, тестовий атрибут і мітка поля
Text, links, a test attribute and a field label
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"), якщо назву атрибута не перевизначено.
🔧 Syntax breakdown: The static import of 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.
💡 Простими словами: byText — повний текст, withText — шматок тексту, byTestId — домовленість із розробниками, яка переживе редизайн.
💡 In plain words: byText means the whole text, withText means a piece of it, and byTestId is an agreement with developers that survives a redesign.
🗣 Як читати вголос Крім CSS і XPath, я користуюся класом Selectors. Для кнопок і меню беру byText, коли потрібен повний збіг, або withText для підрядка, і пам’ятаю, що вони чутливі до регістру. Для посилань підходить і стандартний By.linkText, і його часткова версія. Але найбільше я люблю тестові атрибути: у свіжих версіях є byTestId, і з розробниками я домовляюся, щоб ключові елементи їх мали, бо тоді зміна тексту чи верстки не ламає тести.
🗣 How to say it out loud Besides CSS and XPath I use the Selectors class. For buttons and menus I take byText when I need a full match, or withText for a substring, and I remember that both are case-sensitive. For links the standard By.linkText works, and so does its partial version. But my favourite is test attributes: recent versions have byTestId, and I agree with developers that key elements get such attributes, because then a copy or layout change does not break the tests.
10 Як шукати елементи всередині іншого елемента в Selenide? How do you search for elements inside another element in Selenide?
Локатори Locators Junior ▶

Що це. У 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.

Пошук у межах блоку
Searching within a block
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 .// означає «будь-який нащадок поточного вузла», а // без крапки — «будь-який вузол документа».
🔧 Syntax breakdown: 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".
💡 Простими словами: Спершу знайди контейнер, потім шукай у ньому. Так однакові поля в різних блоках не переплутаються.
💡 In plain words: Find the container first, then search inside it. That way identical fields in different blocks never get mixed up.
🗣 Як читати вголос Для вкладеного пошуку я викликаю $ чи find прямо на елементі, а для кількох нащадків — $$ або findAll, це синоніми. Ланцюжок мені подобається більше за один довгий CSS, бо він повторює структуру сторінки і в помилці видно, яка ланка не знайшлася. Особливо це рятує, коли на сторінці два однакові блоки, наприклад адреси доставки й оплати. Єдине, про що нагадую новачкам, — XPath усередині ланцюжка треба починати з крапки.
🗣 How to say it out loud For nested search I call $ or find directly on the element, and for several descendants I use $$ or findAll, which are synonyms. I prefer a chain to one long CSS selector, because it follows the page structure and the error shows which link was not found. It is a lifesaver when a page has two identical blocks, like shipping and billing addresses. The one thing I remind newcomers about is that XPath inside a chain must start with a dot.
11 Як рухатися DOM відносно елемента: parent, closest, ancestor, sibling і preceding? How do you move around the DOM relative to an element: parent, closest, ancestor, sibling and preceding?
Локатори Locators Middle ▶

Що це. 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.

Кнопка в рядку, знайденому за текстом, і значення поруч із підписом
A button in a row found by text, and a value next to its caption
// рядок таблиці, де є потрібний 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 — наступний за ним.
🔧 Syntax breakdown: Each method returns a new lazy 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.
💡 Простими словами: Знайди те, що унікальне, а потім піднімись або зроби крок убік до того, що тобі насправді потрібно.
💡 In plain words: Find the thing that is unique, then climb up or step sideways to the thing you actually need.
🗣 Як читати вголос Відносну навігацію я використовую, коли унікальний якір є лише в одному місці, а дія потрібна поруч. Класичний приклад — знайти клітинку з email, піднятися через closest до рядка і натиснути кнопку видалення саме в ньому. Для кроку вбік є sibling і preceding з індексом від нуля, а parent дає безпосереднього батька. Ланцюжки з кількох parent я не пишу, бо вони ламаються від першої нової обгортки в розмітці.
🗣 How to say it out loud I use relative navigation when the unique anchor exists in only one place and the action is needed next to it. The classic example is finding the cell with an email, climbing to the row with closest and clicking the delete button in that row. For a sideways step there are sibling and preceding with a zero-based index, and parent gives the direct parent. I avoid chains of several parent calls, because they break on the first new wrapper in the markup.
12 Як знайти елемент усередині Shadow DOM у Selenide? How do you find an element inside Shadow DOM with Selenide?
Локатори Locators Middle ▶

Що це. 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.

Явний шлях через хости і глибокий пошук
An explicit path through hosts and a deep search
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.
🔧 Syntax breakdown: The first argument of 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.
💡 Простими словами: Shadow DOM — кімната із зачиненими дверима. shadowCss називає кожні двері по черзі, shadowDeepCss обходить увесь будинок.
💡 In plain words: Shadow DOM is a room behind closed doors. shadowCss names each door in turn, shadowDeepCss walks through the whole house.
🗣 Як читати вголос Коли елемент сидить у веб-компоненті, звичайний CSS до нього не дістає. Я беру shadowCss і передаю спочатку селектор цілі, а потім ланцюжок хостів від зовнішнього до внутрішнього. Якщо вкладеність глибока чи нестабільна, використовую shadowDeepCss, розуміючи, що він повільніший і може зачепити схожий елемент в іншому компоненті. Закритий shadow root автоматизувати не вийде взагалі, і тоді я обговорюю з командою тестову збірку або компонентні тести.
🗣 How to say it out loud When an element lives inside a web component, plain CSS cannot reach it. I use shadowCss and pass the target selector first, then the chain of hosts from outer to inner. If the nesting is deep or unstable, I switch to shadowDeepCss, knowing it is slower and may catch a similar element in another component. A closed shadow root cannot be automated at all, and then I discuss a test build or component-level tests with the team.
13 Навіщо давати елементу ім’я через as() і як це змінює звіти? Why give an element a name with as(), and how does that change reports?
Локатори Locators Middle ▶

Що це. 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.

Alias у Page Object і як виглядає помилка
An alias in a Page Object and what the error looks like
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. Формат повідомлення в коментарі наведено для ілюстрації, точні рядки залежать від версії.
🔧 Syntax breakdown: 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.
💡 Простими словами: as() — це табличка з назвою на дверях. Корисна, коли номер кімнати нічого не каже, і шкідлива, коли табличку забули поміняти.
💡 In plain words: as() is a name plate on a door. It helps when the room number says nothing and hurts when nobody updated the plate.
🗣 Як читати вголос Метод as я використовую, щоб у звіті падіння й у кроках Allure стояла зрозуміла назва на кшталт «кнопка оплати», а не згенерований id. Працює і для окремих елементів, і для колекцій. Але роблю це вибірково: якщо селектор уже читається, alias лише дублює його, а застаріла назва може збрехати в звіті. Тому я називаю роль елемента, а не його текст, і тримаю ці назви в Page Object поруч із селектором.
🗣 How to say it out loud I use as so that failure reports and Allure steps show a readable name like pay button rather than a generated id. It works for single elements and for collections. But I do it selectively: if the selector already reads well, an alias just duplicates it, and a stale name can lie in the report. So I name the role of the element rather than its text, and I keep those names in the Page Object right next to the selector.
▶ 3 11
14 Як працюють should, shouldBe, shouldHave і shouldNot у Selenide? How do should, shouldBe, shouldHave and shouldNot work in Selenide?
Перевірки Assertions Junior ▶

Що це. Це методи перевірки на 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.

Синоніми перевірок і небезпечний boolean
Assertion synonyms and a dangerous boolean
$("#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 перевіряє його один раз.
🔧 Syntax breakdown: Every 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.
💡 Простими словами: Be чи Have — питання того, як речення звучить англійською. Механіка однакова: перевірити і чекати.
💡 In plain words: Be or Have is a question of how the sentence sounds in English. The mechanics are the same: check and wait.
🗣 Як читати вголос Я пояснюю, що shouldBe, shouldHave і просто should — синоніми, різниця лише в тому, як рядок читається англійською. Кожна з них повторює перевірку до таймауту і, якщо не дочекалася, кидає UIAssertionError зі скріншотом. Заперечні версії чекають протилежного стану. Головне, від чого я застерігаю, — методи is, has і exists: вони повертають boolean миттєво, і обгорнути їх у assertTrue означає отримати нестабільний тест.
🗣 How to say it out loud I explain that shouldBe, shouldHave and plain should are synonyms, the only difference being how the line reads in English. Each one retries the check up to the timeout and, if it never succeeds, throws a UIAssertionError with a screenshot. The negative versions wait for the opposite state. What I warn about most are the is, has and exists methods: they return a boolean instantly, and wrapping them in assertTrue gives you a flaky test.
15 Чим відрізняються умови visible, exist і hidden? How do the visible, exist and hidden conditions differ?
Перевірки Assertions Junior ▶

Що це. 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.

Три стани одного повідомлення
Three states of one message
$("#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), тобто це ті самі перевірки з іншою назвою в звіті.
🔧 Syntax breakdown: All five conditions are static fields of the 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.
💡 Простими словами: exist — «є в розмітці», visible — «є і показаний», hidden — «не показаний, а чи є в розмітці, байдуже».
💡 In plain words: exist means "in the markup", visible means "there and shown", hidden means "not shown, whether or not it is in the markup".
🗣 Як читати вголос Я розрізняю їх так: exist дивиться лише на DOM, visible ще й питає браузер, чи відображається елемент, а hidden задоволений і прихованим елементом, і відсутнім. Для всього, що бачить користувач, я пишу visible, exist лишаю для службових вузлів на кшталт прихованого токена, а hidden використовую, щоб дочекатися зникнення спінера. І пам’ятаю, що перекритий шаром елемент для WebDriver усе одно видимий.
🗣 How to say it out loud I tell them apart like this: exist looks only at the DOM, visible also asks the browser whether the element is displayed, and hidden is happy with both a hidden element and a missing one. For anything the user sees I write visible, I keep exist for service nodes like a hidden token, and I use hidden to wait for a spinner to go away. I also remember that an element covered by another layer still counts as visible for WebDriver.
16 Як перевірити, що елемент зник або що його взагалі немає в DOM? How do you check that an element has disappeared, or that it is not in the DOM at all?
Перевірки Assertions Middle ▶

Два різні питання. «Користувач цього не бачить» і «цього вузла немає в розмітці» — різні твердження, і в 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.

Невидимість проти відсутності
Invisibility versus absence
// спінер: досить, що користувач його не бачить
$(".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 перед нею — якір, який гарантує, що сторінка вже намальована і відсутність панелі не випадкова.
🔧 Syntax breakdown: 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.
💡 Простими словами: hidden — «не видно», shouldNot(exist) — «немає». Для безпеки потрібне друге.
💡 In plain words: hidden means "not seen", shouldNot(exist) means "not there". Security checks need the second one.
🗣 Як читати вголос Я завжди уточнюю, що саме треба довести. Якщо користувач просто не має бачити спінер, пишу shouldBe hidden або shouldNotBe visible, і кожна з цих перевірок пройде і для прихованого, і для видаленого елемента. Якщо ж важливо, що вузла немає в розмітці, наприклад адмінської кнопки у звичайного користувача, я пишу shouldNot exist. І перед такою перевіркою дочікуюся якоря, щоб сторінка точно завантажилася, інакше відсутність нічого не доводить.
🗣 How to say it out loud I always clarify what exactly needs proving. If the user just should not see a spinner, I write shouldBe hidden or shouldNotBe visible, and either passes for a hidden element and for a removed one. If it matters that the node is not in the markup at all, say an admin button for a regular user, I write shouldNot exist. Before such a check I wait for an anchor so the page has definitely loaded, otherwise the absence proves nothing.
17 Чим відрізняються умови text, exactText, ownText і matchText? How do the text, exactText, ownText and matchText conditions differ?
Перевірки Assertions Junior ▶

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.

Одна розмітка, різні текстові умови
One piece of markup, different text conditions
// <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 враховують регістр.
🔧 Syntax breakdown: All the conditions are static methods of 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 шукає шматок, exactText порівнює все, own відкидає дітей, match читає регулярку. Регістр за замовчуванням ігнорується скрізь.
💡 In plain words: text looks for a piece, exactText compares everything, own drops the children, match reads a regex. Case is ignored everywhere by default.
🗣 Як читати вголос Найчастіша помилка, яку я бачу, — text замість exactText. Text шукає підрядок без урахування регістру, тому перевірка лічильника на одиницю тихо пройде на дванадцяти. ExactText порівнює весь текст, але регістр теж ігнорує, для суворого порівняння є версія CaseSensitive. OwnText відкидає текст дочірніх тегів, а matchText перевіряє регулярний вираз, що зручно для номерів замовлень. Ще згадую, що в сьомій версії є перемикач textCheck, а у восьмій повний збіг стане стандартом.
🗣 How to say it out loud The most common mistake I see is text instead of exactText. Text looks for a case-insensitive substring, so checking that a counter equals one quietly passes on twelve. ExactText compares the whole text but also ignores case; for a strict comparison there is the CaseSensitive version. OwnText drops the text of child tags, and matchText checks a regular expression, which is handy for order numbers. I also mention that version seven has the textCheck switch, and in version eight a full match becomes the default.
18 Як прочитати текст, value, атрибут чи CSS-властивість елемента і чому getter не замінює перевірку? How do you read an element's text, value, attribute or CSS property, and why does a getter not replace an assertion?
Перевірки Assertions Junior ▶

Що є. 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.

Getter для даних, умова для перевірки
A getter for data, a condition for checking
// погано: одна спроба, без очікування потрібного значення
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-ом гарантує, що номер уже з’явився, а не порожній.
🔧 Syntax breakdown: 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.
💡 Простими словами: Getter фотографує, умова стежить. Для перевірки потрібне стеження, для даних — фото, зроблене в правильний момент.
💡 In plain words: A getter takes a photo, a condition keeps watching. Checks need the watching; data needs a photo taken at the right moment.
🗣 Як читати вголос Прочитати значення я можу через text, val, attr чи getCssValue, але для перевірок ними не користуюся. Getter чекає лише появи елемента, а значення бере в ту мілісекунду, коли прочитав, тож assertEquals на ньому стає нестабільним. Умова shouldHave, навпаки, повторює спробу і в разі падіння показує фактичне значення зі скріншотом. Getter я залишаю для даних, які потрібні далі в тесті, наприклад номера замовлення, і читаю його лише після того, як умова підтвердила стабільний стан.
🗣 How to say it out loud I can read values with text, val, attr or getCssValue, but I do not use them for assertions. A getter only waits for the element to appear and takes the value at the millisecond it reads it, so an assertEquals on top of it becomes flaky. A shouldHave condition, on the other hand, retries and shows the actual value with a screenshot on failure. I keep getters for data needed later in the test, like an order number, and I read it only after a condition has confirmed a stable state.
19 Як перевіряти атрибути елемента: attribute, href, value, cssClass і cssValue? How do you check element attributes: attribute, href, value, cssClass and cssValue?
Перевірки Assertions Middle ▶

Загальний атрибут. 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.

Атрибути, посилання, значення поля і стилі
Attributes, links, field values and styles
$("#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 записують так, як його повертає браузер після обчислення стилів.
🔧 Syntax breakdown: All the conditions are static methods of 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.
💡 Простими словами: Для класу — cssClass, для посилання — href, для поля — value чи exactValue, для всього іншого — attribute.
💡 In plain words: For a class use cssClass, for a link use href, for a field use value or exactValue, and for everything else use attribute.
🗣 Як читати вголос Для атрибутів у мене є кілька правил. Загальний випадок покриває attribute: з одним аргументом він перевіряє наявність, з двома — значення, а attributeMatching — регулярку. Так я перевіряю і атрибут-підказку в порожньому полі, бо окремої умови для неї немає. Для посилань беру href, бо Selenium віддає абсолютну адресу. Для полів пам’ятаю, що value шукає підрядок, а exactValue — повний збіг. Класи перевіряю тільки через cssClass, бо attribute з class порівнює весь рядок класів.
🗣 How to say it out loud I have a few rules for attributes. The general case is covered by attribute: with one argument it checks presence, with two it checks the value, and attributeMatching takes a regex. That is also how I check the hint attribute of an empty field, because there is no dedicated condition for it. For links I use href, since Selenium hands back an absolute URL. For fields I remember that value looks for a substring and exactValue for a full match. Classes I check only through cssClass, because attribute with class compares the whole class string.
20 Як перевірити стан поля: enabled, disabled, checked, selected, focused і readonly? How do you check a field's state: enabled, disabled, checked, selected, focused and readonly?
Перевірки Assertions Junior ▶

Що є. 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.

Стани полів у формі реєстрації
Field states in a sign-up form
$("#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] вибирає конкретний перемикач із групи за двома атрибутами. Закоментований рядок показує форму, від якої варто відмовитися.
🔧 Syntax breakdown: All the states are static fields of 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.
💡 Простими словами: Для галочки і перемикача — checked, для пункту списку — selected, для поля, яке можна редагувати, — editable.
💡 In plain words: For a tick box or a radio use checked, for a list option use selected, and for a field that can be edited use editable.
🗣 Як читати вголос Стан полів я перевіряю готовими умовами: enabled і disabled, checked для галочок і перемикачів, focused після помилки валідації, readonly та editable для полів. Для radio є три записи: checked, selected і перевірка атрибута checked. Перші два дають однаковий результат, бо обидва питають isSelected, але checked читається природніше. Атрибут я не використовую, бо React може змінити стан, не чіпаючи розмітку. А для кастомних перемикачів дивлюся на прихований input, а не на намальований div.
🗣 How to say it out loud I check field state with ready-made conditions: enabled and disabled, checked for tick boxes and radios, focused after a validation error, readonly and editable for fields. For a radio there are three ways to write it: checked, selected and an attribute check for checked. The first two give the same result, since both ask isSelected, but checked reads more naturally. I avoid the attribute, because React can change the state without touching the markup. And for custom toggles I look at the hidden input rather than the drawn div.
21 Що насправді перевіряють умови interactable і clickable? What do the interactable and clickable conditions actually check?
Перевірки Assertions Middle ▶

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 і кнопка, що вмикається після валідації
A transparent input and a button enabled after validation
// <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() не обов’язковий, але робить причину падіння очевиднішою.
🔧 Syntax breakdown: 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.
💡 Простими словами: interactable — «з цим можна працювати, навіть якщо воно прозоре», clickable — «і воно ще й увімкнене». Про перекриття обидві умови нічого не знають.
💡 In plain words: interactable means "you can work with it even if it is transparent", clickable adds "and it is enabled". Neither knows anything about overlap.
🗣 Як читати вголос Interactable часто плутають з «видимий і нічим не перекритий», але в Selenide це інше: елемент або видимий, або має нульову прозорість, як прихований input для завантаження файлу. Clickable — це interactable плюс enabled, і саме на нього click чекає перед кліком. Перекриття банером чи модалкою жодна з цих умов не перевіряє: воно проявиться лише на самому кліку як перехоплений клік, і Selenide повторюватиме спробу до таймауту.
🗣 How to say it out loud People often confuse interactable with visible and not covered, but in Selenide it means something else: the element is either visible or fully transparent, like a hidden input for a file upload. Clickable is interactable plus enabled, and that is what click waits for before clicking. Neither condition checks for a banner or a modal covering the element: that only shows up on the click itself as an intercepted click, and Selenide keeps retrying until the timeout.
22 Як комбінувати умови: and, or, not, allOf, anyOf і because? How do you combine conditions: and, or, not, allOf, anyOf and because?
Перевірки Assertions Middle ▶

Кілька умов підряд. Найпростіший спосіб — передати їх через кому: 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.

Кома, іменоване «і», «або» та пояснення
A comma, a named AND, an OR and an explanation
$("#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(...) повертає нову умову з поясненням, яке з’явиться в тексті помилки поруч з очікуваним і фактичним значенням.
🔧 Syntax breakdown: 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.
💡 Простими словами: Кома — для кількох вимог, and — щоб дати їм спільне ім’я, because — щоб пояснити, навіщо ця вимога. А or — лише коли два результати справді коректні.
💡 In plain words: Commas are for several requirements, and gives them a shared name, because explains why a requirement exists. Use or only when two outcomes really are both correct.
🗣 Як читати вголос Зазвичай мені вистачає кількох умов через кому в одному shouldBe. Якщо комбінація повторюється, я збираю її через and з назвою і кладу в Page Object, щоб у звіті було «активна вкладка», а не список технічних умов. Or і anyOf використовую рідко, лише для справжнього недетермінізму на кшталт A/B-тесту, бо в інших випадках «або» ховає баг. А because люблю для неочевидних правил: пояснення потрапляє прямо в текст помилки, і колега одразу розуміє контекст.
🗣 How to say it out loud Usually a few comma-separated conditions in one shouldBe are enough for me. If a combination repeats, I build it with a named and and put it into the Page Object, so the report says active tab rather than a list of technical conditions. I rarely use or and anyOf, only for genuine nondeterminism such as an A/B test, because otherwise an or hides a bug. And I like because for non-obvious rules: the explanation lands right in the error text, and a colleague immediately gets the context.
23 Як перевірити URL і заголовок сторінки з очікуванням? How do you check the page URL and title with waiting?
Перевірки Assertions Middle ▶

Що це. Для перевірок на рівні браузера, а не елемента, 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).

Редирект, заголовок, нова вкладка і cookie
A redirect, a title, a new tab and a cookie
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() лише за іменем, але не за сигнатурою, тож обидва можна використовувати поруч.
🔧 Syntax breakdown: 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.
💡 Простими словами: Для адреси й заголовка теж потрібне очікування: webdriver().shouldHave — це shouldHave для всієї вкладки.
💡 In plain words: The address and the title need waiting too: webdriver().shouldHave is shouldHave for the whole tab.
🗣 Як читати вголос Для URL і заголовка я використовую webdriver і умови з WebDriverConditions, бо редирект чи зміна адреси в SPA стаються не миттєво, а тут є той самий механізм очікування, що й для елементів. Для шляхів обираю urlContaining чи urlStartingWith, бо повний url ламається від будь-якого трекінгового параметра. Getter-и title і url залишаю для логування. А ще numberOfWindows допомагає дочекатися нової вкладки перед перемиканням.
🗣 How to say it out loud For the URL and the title I use webdriver with conditions from WebDriverConditions, because a redirect or an SPA address change does not happen instantly, and this gives me the same waiting mechanism as for elements. For paths I choose urlContaining or urlStartingWith, since a full url breaks on any tracking parameter. I keep the title and url getters for logging. And numberOfWindows helps me wait for a new tab before switching to it.
24 Як написати власну умову в Selenide і коли вона краща за getter з assertEquals? How do you write a custom condition in Selenide, and when is it better than a getter with assertEquals?
Перевірки Assertions Senior ▶

Що це. Кожна умова в 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.

Умова «ціна в гривнях» і швидкий варіант через match
A "price in hryvnias" condition and a quick variant via match
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, тут вона лише ілюструє синтаксис.
🔧 Syntax breakdown: 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.
💡 Простими словами: Власна умова — це спосіб навчити shouldHave твоєї предметної мови, не втрачаючи очікувань і звітів.
💡 In plain words: A custom condition teaches shouldHave the language of your domain without losing waiting and reports.
🗣 Як читати вголос Власну умову я пишу, коли доменна перевірка повторюється в багатьох тестах, наприклад формат ціни чи сума рядка кошика. Наслідую WebElementCondition, реалізую check і обов’язково повертаю в CheckResult фактичне значення, щоб помилка пояснювала себе. Для разових випадків вистачає Condition.match з лямбдою. Головна перевага над getter-ом з assertEquals — я безкоштовно отримую очікування, скріншот і крок у звіті. А обмеження в тому, що check викликається багато разів, тож він має бути дешевим і без побічних ефектів.
🗣 How to say it out loud I write a custom condition when a domain check repeats across many tests, such as a price format or a cart row total. I extend WebElementCondition, implement check, and always return the actual value in the CheckResult so the error explains itself. For one-off cases Condition.match with a lambda is enough. The main advantage over a getter with assertEquals is that I get waiting, a screenshot and a report step for free. The limitation is that check runs many times, so it has to be cheap and free of side effects.
▶ 4 6
25 Як працюють розумні очікування Selenide і чому explicit wait у тестах зайвий? How do Selenide smart waits work, and why is an explicit wait unnecessary in tests?
Очікування Waits Junior ▶

Що це. Кожен метод 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.

Очікування вбудоване в дію і перевірку
Waiting is built into the action and the check
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 показано як антиприклад: він працює, але лише додає другий, незалежний таймаут поверх вбудованого.
🔧 Syntax breakdown: 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.
💡 Простими словами: У Selenide кожна перевірка — це маленький WebDriverWait, який уже написали за тебе.
💡 In plain words: In Selenide every check is a small WebDriverWait that someone has already written for you.
🗣 Як читати вголос Я пояснюю так: у Selenide кожна дія й кожна should-перевірка — це цикл із повторами. Бібліотека шукає елемент, перевіряє умову, якщо ні — чекає двісті мілісекунд і пробує знову, і так до чотирьох секунд за замовчуванням. Клік до того ж сам чекає, поки кнопку можна натиснути. Тому я не пишу WebDriverWait і не вмикаю implicit wait, бо це лише подвоює таймаути. А ось getter без повторів — це вже моя відповідальність, тож для перевірок я беру умови.
🗣 How to say it out loud The way I put it: in Selenide every action and every should check is a retry loop. The library finds the element, checks the condition, and if it fails it waits two hundred milliseconds and tries again, up to four seconds by default. A click also waits on its own until the button can be pressed. That is why I do not write WebDriverWait and do not enable implicit waits, they only double the timeouts. A getter, on the other hand, does not retry, so for assertions I always reach for conditions.
26 Що регулюють Configuration.timeout і pollingInterval і як їх змінити? What do Configuration.timeout and pollingInterval control, and how do you change them?
Очікування Waits Junior ▶

Що це. 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.

Три способи задати таймаут і інтервал опитування
Three ways to set the timeout and polling interval
// 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.
🔧 Syntax breakdown: 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.
💡 Простими словами: timeout — скільки чекати загалом, pollingInterval — як часто заглядати. Перше змінюють частіше, друге майже ніколи.
💡 In plain words: timeout is how long to wait overall, pollingInterval is how often to peek. You change the first now and then and the second almost never.
🗣 Як читати вголос Timeout — це скільки Selenide максимально повторює перевірку, за замовчуванням чотири секунди. PollingInterval — пауза між спробами, двісті мілісекунд. Я задаю їх у selenide.properties або через системну властивість у CI, а не розкидаю по коду. Глобальний таймаут підіймаю лише під реальну повільність стенду, бо великий таймаут ховає проблеми з продуктивністю і робить кожне падіння довшим. І окремо пам’ятаю про pageLoadTimeout, який стосується лише завантаження сторінки.
🗣 How to say it out loud Timeout is the longest Selenide keeps retrying a check, four seconds by default. PollingInterval is the pause between attempts, two hundred milliseconds. I set them in selenide.properties or through a system property in CI rather than scattering them around the code. I only raise the global timeout to match the real slowness of a stand, because a large timeout hides performance problems and makes every failure slower. And I keep pageLoadTimeout separate in my head, since it only covers page loading.
27 Як задати окремий таймаут для однієї перевірки чи дії, не змінюючи глобальний? How do you set a separate timeout for a single check or action without changing the global one?
Очікування Waits Middle ▶

Що це. Методи 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.

Повільний звіт і повільна оплата з власними лімітами
A slow report and a slow payment with their own limits
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, який повертає опції кліку з власним лімітом.
🔧 Syntax breakdown: The second 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.
💡 Простими словами: Локальний таймаут — це позначка «тут довго, і це нормально». Якщо таких позначок забагато, щось не так зі стендом.
💡 In plain words: A local timeout is a note saying this is slow and that is fine. When there are too many such notes, something is wrong with the stand.
🗣 Як читати вголос Для однієї повільної операції я передаю Duration другим параметром у shouldBe чи shouldHave. Це діє лише на цей виклик, а глобальні чотири секунди лишаються. Для кліку є ClickOptions.withTimeout. Типовий випадок у мене — генерація звіту чи оплата в банківській пісочниці. Але якщо бачу локальні таймаути в кожному тесті, я йду розбиратися зі стендом чи глобальним значенням, а не множу винятки, бо великий таймаут маскує гонки.
🗣 How to say it out loud For one slow operation I pass a Duration as the second argument to shouldBe or shouldHave. It only affects that call, and the global four seconds stay as they are. For clicks there is ClickOptions.withTimeout. My typical cases are report generation or a payment in a bank sandbox. But if I see local timeouts in every test, I go and look at the stand or the global value instead of piling up exceptions, because a large timeout masks races.
28 Чому Selenide.sleep і Thread.sleep шкодять тестам і чим їх замінити? Why do Selenide.sleep and Thread.sleep hurt tests, and what should replace them?
Очікування Waits Junior ▶

Що це. 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.

Пауза навмання і очікування конкретного стану
A blind pause versus waiting for a specific state
// погано: 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 порівнює весь текст без урахування регістру й зайвих пробілів.
🔧 Syntax breakdown: 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.
💡 Простими словами: Чекай на подію, а не на годинник: подія точно скаже, коли можна йти далі.
💡 In plain words: Wait for an event, not for the clock: the event tells you exactly when to move on.
🗣 Як читати вголос Sleep у Selenide — це просто гарантована пауза, без жодної перевірки. Вона або задовга і сповільнює прогін, або закоротка і робить тест нестабільним на повільному агенті. Тому замість неї я чекаю на видимий результат: зникнення спінера, потрібний текст статусу, кількість рядків. Такий рядок водночас і чекає, і перевіряє. Паузу я лишаю тільки там, де треба довести, що чогось не сталося за певний час, і обов’язково пишу коментар чому.
🗣 How to say it out loud Sleep in Selenide is simply a guaranteed pause with no check at all. It is either too long and slows the run down, or too short and makes the test flaky on a slow agent. So instead I wait for a visible outcome: the spinner going away, the right status text, the number of rows. Such a line both waits and asserts. I keep a pause only where I have to prove that something did not happen within a certain time, and I always add a comment explaining why.
29 Як дочекатися зникнення лоадера, завершення AJAX-запиту чи анімації в Selenide? How do you wait for a loader to vanish, an AJAX request to finish or an animation to end in Selenide?
Очікування Waits Middle ▶

Лоадер. Найпростіше — чекати, доки індикатор зникне: $(".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.

Лоадер, результат запиту й анімація модального вікна
A loader, a request result and a modal animation
$("#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 стиль, що обнуляє переходи й анімації для всіх елементів.
🔧 Syntax breakdown: 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.
💡 Простими словами: Чекай на те, що побачить користувач, а не на те, що робить мережа чи CSS.
💡 In plain words: Wait for what the user will see, not for what the network or CSS is doing.
🗣 Як читати вголос Лоадер я чекаю через should disappear, але пам’ятаю про пастку: якщо спінер з’являється із запізненням, перевірка може проскочити раніше. Тому головне очікування в мене — на результат, наприклад потрібну кількість карток або службовий атрибут, який виставляє фронтенд. Мережу напряму не відстежую, бо користувач бачить дані, а не запити. Для анімацій у сьомій версії є умова animated, і я пишу shouldNotBe animated перед кліком у модальному вікні. А в тестовому оточенні взагалі прошу вимикати переходи.
🗣 How to say it out loud I wait for a loader with should disappear, but I remember the trap: if the spinner appears with a delay, the check can slip through too early. So my main wait is for the result, for example the right number of cards or a service attribute the front end sets. I do not track the network directly, because the user sees data, not requests. For animations version seven has the animated condition, and I write shouldNotBe animated before clicking inside a modal. And in the test environment I ask for transitions to be switched off entirely.
30 Як діагностувати flaky-тест у Selenide: таймаут, polling, анімації чи гонка з перемальовуванням? How do you diagnose a flaky Selenide test: timeout, polling, animations or a race with re-rendering?
Очікування Waits Senior ▶

Контекст. Тест проходить дев’ять разів із десяти, а в 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.

Як читати падіння і як відтворити нестабільність
Reading a failure and reproducing flakiness
// типове повідомлення 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(...)) повторює перевірку до таймауту.
🔧 Syntax breakdown: The comment at the top shows the lines of an 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.
💡 Простими словами: Спершу дізнайся, чому тест мерехтить, і лише потім вирішуй, що міняти. Таймаут — це лише одна з чотирьох можливих відповідей.
💡 In plain words: Find out why the test flickers first, and only then decide what to change. The timeout is just one of four possible answers.
🗣 Як читати вголос Коли тест мерехтить, я не починаю з підняття таймауту. Спершу читаю повідомлення Selenide: там є локатор, фактичне значення в момент останньої спроби, скріншот і збережена сторінка. За ними розкладаю причину на чотири кошики: повільне середовище, анімація чи оверлей, гонка з перемальовуванням і нестабільні дані. Далі відтворюю падіння через RepeatedTest у тих самих умовах, що й CI: headless, той самий розмір вікна, повільна мережа. Ретраї вмикаю тільки разом із метрикою і розбором кожного випадку.
🗣 How to say it out loud When a test flickers, I do not start by raising the timeout. First I read the Selenide message: it has the locator, the actual value at the last attempt, a screenshot and the saved page. From those I sort the cause into four buckets: a slow environment, an animation or overlay, a race with re-rendering, and unstable data. Then I reproduce it with RepeatedTest under the same conditions as CI: headless, the same window size, a throttled network. I only enable retries together with a metric and a review of every case.
▶ 5 4
31 Як працювати з ElementsCollection: first, last, get і розмір колекції? How do you work with an ElementsCollection: first, last, get and the collection size?
Колекції Collections Junior ▶

Що це. $$(".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.

Перший, останній, за індексом і кількість
First, last, by index and the count
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") — пошук усередині третього елемента, увесь ланцюжок лишається лінивим.
🔧 Syntax breakdown: 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.
💡 Простими словами: Кількість перевіряй через shouldHave(size(n)), а size() залиш для логів.
💡 In plain words: Check the count with shouldHave(size(n)) and keep size() for logging.
🗣 Як читати вголос Подвійний долар дає мені ElementsCollection, і вона така ж лінива, як окремий елемент. Для доступу беру first, last або get з індексом від нуля, і отримую звичайний SelenideElement з очікуваннями. Кількість перевіряю умовою size з CollectionCondition, бо вона чекає, доки список довантажиться. Getter size без аргументів у перевірках не використовую: він повертає число тієї миті, і саме на ньому ламаються тести з асинхронними списками.
🗣 How to say it out loud Double dollar gives me an ElementsCollection, and it is just as lazy as a single element. To reach items I use first, last or get with a zero-based index, and I get a normal SelenideElement with waiting. I check the count with the size condition from CollectionCondition, because it waits for the list to finish loading. I never use the no-argument size getter in assertions: it returns the number at that instant, and that is exactly where tests with async lists break.
32 Як фільтрувати колекцію в Selenide: filterBy, excludeWith і findBy? How do you filter a collection in Selenide: filterBy, excludeWith and findBy?
Колекції Collections Middle ▶

Що це. Колекцію можна звузити умовою, не пишучи складного 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.

Фільтр, виключення і пошук першого збігу
Filter, exclude and find the first match
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 — ті самі, що для окремого елемента.
🔧 Syntax breakdown: 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.
💡 Простими словами: filterBy — сито, excludeWith — сито навпаки, findBy — рука, що дістає перше знайдене.
💡 In plain words: filterBy is a sieve, excludeWith is a sieve turned inside out, and findBy is a hand that grabs the first match.
🗣 Як читати вголос Для колекцій я використовую три методи: filterBy лишає елементи за умовою, excludeWith відкидає, а findBy повертає перший збіг як окремий елемент. Усі вони ліниві, тож після кліку фільтр перераховується сам. Зручно, коли критерій — стан чи текст, а не структура верстки. Але я пам’ятаю, що фільтрація йде на боці тесту, тому для величезних списків звужую селектор. А у findBy беру exactText, щоб не зачепити схожий рядок.
🗣 How to say it out loud For collections I use three methods: filterBy keeps elements by a condition, excludeWith drops them, and findBy returns the first match as a single element. They are all lazy, so after a click the filter is recomputed automatically. It is handy when the criterion is state or text rather than markup structure. But I remember that filtering runs on the test side, so for huge lists I narrow the selector instead. And in findBy I use exactText so I do not hit a similar row.
33 Які є умови для колекцій і чим texts відрізняється від exactTexts, textsInAnyOrder та itemWithText? What collection conditions exist, and how does texts differ from exactTexts, textsInAnyOrder and itemWithText?
Колекції Collections Middle ▶

Розмір. 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.

Розмір, тексти в порядку і без, один елемент і предикат
Size, ordered and unordered texts, a single item and a predicate
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).
🔧 Syntax breakdown: The static import of 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).
💡 Простими словами: texts — «кожен містить своє по порядку», exactTexts — «кожен дорівнює своєму», itemWithText — «хоча б один дорівнює».
💡 In plain words: texts means each one contains its own in order, exactTexts means each one equals its own, itemWithText means at least one equals it.
🗣 Як читати вголос Умови колекцій я ділю на дві групи. Розмірні: size, sizeGreaterThan, і для непорожньої колекції я пишу sizeGreaterThan нуль, а для порожньої є empty. Текстові: texts перевіряє кількість і порядок, і за замовчуванням кожен елемент лише містить свій рядок; exactTexts вимагає повного збігу, textsInAnyOrder ігнорує порядок, а itemWithText шукає хоча б один елемент з точно таким текстом. Getter texts повертає список без очікування, тому для перевірок я беру умову.
🗣 How to say it out loud I split collection conditions into two groups. Size ones: size, sizeGreaterThan, and for a non-empty collection I write sizeGreaterThan zero, while for an empty one there is empty. Text ones: texts checks the count and the order, and by default each element only has to contain its string; exactTexts needs a full match, textsInAnyOrder ignores order, and itemWithText looks for at least one element with exactly that text. The texts getter returns a list without waiting, so for assertions I use the condition.
34 Як знайти рядок таблиці за текстом і виконати дію всередині нього? How do you find a table row by its text and perform an action inside it?
Колекції Collections Middle ▶

Що це. Типова задача: у таблиці користувачів знайти рядок з «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.

Дія в рядку, перевірка стовпця й атрибутів
An action in a row, a column check and attributes
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 приймає назву атрибута і значення в тому ж порядку, що й елементи.
🔧 Syntax breakdown: 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.
💡 Простими словами: Спершу знайди рядок за унікальним вмістом, а вже потім шукай кнопку всередині нього.
💡 In plain words: First find the row by unique content, and only then look for the button inside it.
🗣 Як читати вголос Для таблиць у мене один шаблон: знайти рядок через findBy за унікальним значенням, найкраще за email чи ідентифікатором, і вже всередині нього шукати кнопку чи клітинку. Тоді тест не ламається, коли в таблиці додаються записи. Цілий стовпець перевіряю колекцією клітинок з exactTexts, а атрибути — умовою attributes. Окремо думаю про пагінацію й віртуальний скрол, бо в DOM є лише видимі рядки. Після видалення перевіряю, що в колекції немає тексту, а не звертаюся до старого рядка.
🗣 How to say it out loud For tables I have one pattern: find the row with findBy by a unique value, ideally an email or an id, and only then look for a button or cell inside it. That way the test does not break when records are added. I check a whole column with a collection of cells and exactTexts, and attributes with the attributes condition. I also think about pagination and virtual scrolling separately, since only visible rows exist in the DOM. After a delete I check that the text is gone from the collection instead of touching the old row.
▶ 6 9
35 Як у Selenide виконати click, hover, doubleClick і contextClick? How do you perform click, hover, doubleClick and contextClick in Selenide?
Дії Actions Junior ▶

Що це. Усі чотири дії — методи самого 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)).

Чотири дії мишею в одному сценарії
Four mouse actions in one scenario
$(".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-рядка не потребують екранування.
🔧 Syntax breakdown: Each line is a chain of find, wait, act. 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.
💡 Простими словами: Усі дії мишею викликаються прямо на елементі й самі чекають, доки на нього можна натиснути.
💡 In plain words: Every mouse action is called directly on the element and waits by itself until the element can be clicked.
🗣 Як читати вголос У Selenide я не будую Actions для простих жестів. Клік, наведення, подвійний клік і правий клік — це методи click, hover, doubleClick і contextClick на самому елементі. Перед дією бібліотека чекає, поки елемент стане доступним для кліку, тож окремі очікування не потрібні. Єдине, про що я пам’ятаю, — наведення живе до наступного руху миші, тому пункт підменю клікаю одразу після hover.
🗣 How to say it out loud In Selenide I do not build Actions for simple gestures. Click, hover, double click and right click are the methods click, hover, doubleClick and contextClick on the element itself. Before acting, the library waits for the element to become clickable, so I need no separate waits. The one thing I keep in mind is that a hover lives only until the next pointer move, so I click the submenu item right after the hover.
36 Які опції має click у Selenide і коли виправданий клік через JavaScript? What options does click have in Selenide, and when is a JavaScript click justified?
Дії Actions Middle ▶

Що це. 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.

Клік зі зсувом, з клавішею і через JavaScript
A click with an offset, with a key held, and via JavaScript
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 створюють його копію з новим параметром. Зсув рахується від центру елемента в пікселях, від’ємні значення зсувають ліворуч і вгору.
🔧 Syntax breakdown: The static factories 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.
💡 Простими словами: Якщо нативний клік падає через перекриття, це повідомлення про баг, а не привід перейти на JavaScript.
💡 In plain words: If a native click fails because something covers the element, that is a bug report, not a reason to switch to JavaScript.
🗣 Як читати вголос У click можна передати ClickOptions: зсув від центру для слайдерів, затиснуті клавіші для виділення діапазону, JavaScript-метод і force, який пропускає перевірку видимості. JS-клік я вважаю крайнім заходом. Він натискає навіть кнопку, яку перекрив банер чи модалка, тобто ховає справжній дефект. Тому коли бачу ElementClickInterceptedException, я шукаю, що саме лежить зверху, а глобальний clickViaJs на проєкті взагалі не вмикаю.
🗣 How to say it out loud You can pass ClickOptions to click: an offset from the centre for sliders, held keys for range selection, the JavaScript method, and force, which skips the visibility check. I treat a JS click as a last resort. It presses even a button covered by a banner or a modal, so it hides a real defect. When I see ElementClickInterceptedException, I look for what is lying on top, and I never turn on the global clickViaJs on a project.
37 Чим відрізняються setValue, val, type, append і sendKeys у Selenide? How do setValue, val, type, append and sendKeys differ in Selenide?
Дії Actions Junior ▶

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.

П’ять способів ввести текст
Five ways to enter text
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(...)) повертає першу підказку, що містить потрібний текст.
🔧 Syntax breakdown: 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.
💡 Простими словами: За замовчуванням бери setValue. type — для полів, які реагують на кожну літеру, append — щоб дописати.
💡 In plain words: Use setValue by default. Use type for fields that react to every letter, and append to add to existing text.
🗣 Як читати вголос Для звичайних полів я пишу setValue: воно очищає поле, вводить текст і генерує change, а val — просто синонім. Коли поле живе від кожного натискання, як автопідказка, беру type: він друкує по символу з паузою і сам очищає поле. Append потрібен, щоб дописати текст, а голий sendKeys лишаю для стрілок і спецклавіш. JS-режим вмикаю рідко, бо він може обійти валідацію на натисканнях, а паролі завжди позначаю як sensitive, щоб не світити їх у звітах.
🗣 How to say it out loud For ordinary fields I write setValue: it clears the field, types the text and fires change, and val is just a synonym. When a field reacts to every keystroke, like an autosuggest, I use type: it prints one character at a time with a pause and clears the field itself. Append adds text to the end, and I keep plain sendKeys for arrows and special keys. I rarely use JS mode because it can skip keystroke validation, and I always mark passwords as sensitive so they do not leak into reports.
38 Як очистити поле вводу в Selenide і чому clear() іноді не спрацьовує в React чи Vue? How do you clear an input in Selenide, and why does clear() sometimes fail in React or Vue?
Дії Actions Junior ▶

Варіанти. Перший — $("#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.

Три способи очищення і перевірка результату
Three ways to clear, plus checking the result
// 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(...), яка шукає підрядок.
🔧 Syntax breakdown: The 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.
💡 Простими словами: Після очищення перевір, що поле справді порожнє, і що форма це помітила.
💡 In plain words: After clearing, check that the field really is empty and that the form noticed.
🗣 Як читати вголос Поле я очищаю через clear або через setValue з порожнім рядком. Selenide-версія clear уже генерує change і blur, але в React буває, що компонент усе одно тримає старе значення в стані. Тоді я підключаю модуль clear-with-shortcut, і clear починає виділяти текст і тиснути Backspace, як живий користувач. І завжди після очищення перевіряю значення поля й зникнення помилки валідації, а не вірю самій дії.
🗣 How to say it out loud I clear a field with clear or with setValue and an empty string. The Selenide version of clear already fires change and blur, but in React a component sometimes still keeps the old value in its state. Then I add the clear-with-shortcut module, and clear starts selecting the text and pressing Backspace like a real user. After clearing I always check the field value and that the validation error disappeared, instead of trusting the action alone.
39 Як натискати клавіші та комбінації в Selenide: pressEnter, press, Keys.chord? How do you press keys and key combinations in Selenide: pressEnter, press, Keys.chord?
Дії Actions Junior ▶

Що є. Для частих клавіш є готові методи: 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.

Окремі клавіші, комбінації та послідовності
Single keys, combinations and sequences
$("#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().
🔧 Syntax breakdown: 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.
💡 Простими словами: Для Enter, Tab і Escape є готові методи, для решти — press або Keys.chord.
💡 In plain words: Enter, Tab and Escape have ready methods; for everything else use press or Keys.chord.
🗣 Як читати вголос Для частих клавіш я використовую pressEnter, pressTab і pressEscape, бо вони ланцюгові й читаються як речення. Інші клавіші натискаю через press, а комбінації збираю через Keys.chord, наприклад Ctrl і A. Коли треба затиснути модифікатор на кілька натискань, беру actions із keyDown і keyUp. На крос-платформних агентах пам’ятаю, що на Mac замість Control потрібен Command, інакше тест тихо нічого не виділить.
🗣 How to say it out loud For common keys I use pressEnter, pressTab and pressEscape, because they chain and read like a sentence. Other keys go through press, and I build combinations with Keys.chord, for example Ctrl plus A. When a modifier has to stay down across several keystrokes, I use actions with keyDown and keyUp. On cross-platform agents I remember that a Mac needs Command instead of Control, otherwise the test quietly selects nothing.
40 Як працювати з select, checkbox і radio у Selenide? How do you work with select, checkbox and radio in Selenide?
Дії Actions Junior ▶

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.

Нативний select, прапорці й перемикачі
A native select, checkboxes and radio buttons
$("#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.
🔧 Syntax breakdown: 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.
💡 Простими словами: selectOption і selectRadio працюють лише з нативними елементами. Для красивих кастомних списків — два кліки.
💡 In plain words: selectOption and selectRadio only work with native elements. Fancy custom lists need two clicks.
🗣 Як читати вголос Для нативного select я вибираю опцію за текстом через selectOption, за value через selectOptionByValue або за індексом, а перевіряю через getSelectedOption. Прапорці ставлю через setSelected, бо він ідемпотентний, на відміну від кліку, а в групі radio використовую selectRadio зі значенням. Якщо ж дропдаун намальований на div, ці методи не допоможуть, і я відкриваю список кліком і клікаю пункт. Стилізовані чекбокси з прихованим input клікаю через label.
🗣 How to say it out loud For a native select I choose an option by text with selectOption, by value with selectOptionByValue or by index, and I check it through getSelectedOption. I tick checkboxes with setSelected because it is idempotent, unlike a click, and in a radio group I use selectRadio with the value. If a dropdown is drawn with divs, these methods do not help, so I open the list with a click and click the item. Styled checkboxes with a hidden input I click through their label.
41 Як прокрутити сторінку до елемента і клікнути кнопку, що поза viewport? How do you scroll to an element and click a button that is outside the viewport?
Дії Actions Middle ▶

Що відбувається без скролу. Нативний 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.

Скрол під липкою шапкою і в контейнері
Scrolling under a sticky header and inside a container
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. Усі методи скролу повертають елемент, тож клік чи перевірка йдуть одразу в ланцюжку.
🔧 Syntax breakdown: 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.
💡 Простими словами: Клік прокручує сам. Якщо елемент усе одно перекритий, прокрути його в центр, а не до верху.
💡 In plain words: A click scrolls by itself. If the element is still covered, scroll it to the centre rather than to the top.
🗣 Як читати вголос Нативний клік уже прокручує елемент у видиму зону, тому для більшості кнопок скрол не потрібен. Коли він таки потрібен, я пам’ятаю, що scrollTo ставить елемент до верху вікна, де його часто перекриває липка шапка. Тому я беру scrollIntoCenter або scrollIntoView з опцією center. Для таблиць із власним скролбаром scrollTo не підходить, бо рухає лише вікно, а scrollIntoView прокручує і контейнер.
🗣 How to say it out loud A native click already scrolls the element into view, so most buttons need no scrolling. When it is needed, I remember that scrollTo puts the element at the top of the window, where a sticky header often covers it. So I use scrollIntoCenter or scrollIntoView with the center option. For tables with their own scrollbar scrollTo does not work, because it only moves the window, while scrollIntoView scrolls the container as well.
42 Як виконати drag and drop у Selenide і чому він іноді не спрацьовує? How do you perform drag and drop in Selenide, and why does it sometimes not work?
Дії Actions Middle ▶

Що це. У 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.

HTML5-перетягування, режим Actions і ручний жест
HTML5 dragging, Actions mode and a manual gesture
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().
🔧 Syntax breakdown: 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().
💡 Простими словами: Спершу дізнайся, на яких подіях побудоване перетягування на сторінці, і обери відповідний режим.
💡 In plain words: First find out which events the page uses for dragging, then pick the matching mode.
🗣 Як читати вголос У сьомій версії drag and drop — це один метод dragAndDrop з опціями, а ціль задається через to з селектором або елементом. За замовчуванням він працює через JavaScript і генерує HTML5-події drag, що підходить для нативного перетягування. Якщо бібліотека слухає події миші, я перемикаюся на usingSeleniumActions, а для капризних бібліотек пишу actions з рухом у кілька кроків. Після перетягування обов’язково перевіряю новий порядок чи вміст, а не просто відсутність помилки.
🗣 How to say it out loud In version seven drag and drop is a single dragAndDrop method with options, and the target is given via to with a selector or an element. By default it works through JavaScript and fires HTML5 drag events, which suits native dragging. If a library listens to mouse events, I switch to usingSeleniumActions, and for picky libraries I write actions with movement in several steps. After dragging I always check the new order or content, not just the absence of an error.
43 Як виконати JavaScript у Selenide і коли це доречно в тесті? How do you run JavaScript in Selenide, and when is it appropriate in a test?
Дії Actions Middle ▶

Що це. Статичний 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.

Читання стану, асинхронний скрипт і очікування результату
Reading state, an async script and waiting for a result
// прочитати значення з 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;")));
🔧 Розбір синтаксису: Тип результату визначає змінна ліворуч: WebDriver повертає числа як Long або Double, рядки як String, об’єкти як Map. У скрипті обов’язковий return, інакше повернеться null. Selenide.Wait() повертає SelenideWait з поточним таймаутом, а until повторює лямбду, доки вона не дасть true.
🔧 Syntax breakdown: The result type comes from the variable on the left: WebDriver returns numbers as 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.
💡 Простими словами: JavaScript у тесті — для читання й підготовки, а не для дій замість користувача.
💡 In plain words: JavaScript in a test is for reading and preparing, not for acting instead of the user.
🗣 Як читати вголос JavaScript я запускаю через executeJavaScript, передаю елементи як arguments і отримую результат потрібного типу. Для fetch та інших промісів є асинхронна версія з callback. Доречним я вважаю читання стану, якого немає в DOM, наприклад localStorage чи dataLayer аналітики, і підготовку середовища. А от кліки й введення через JS у тестах не пишу, бо тест тоді пройде там, де користувач застрягне. І пам’ятаю, що скрипт сам не чекає, тож читання стану загортаю в очікування.
🗣 How to say it out loud I run JavaScript with executeJavaScript, pass elements as arguments and get a result of the needed type. For fetch and other promises there is an async version with a callback. I think it is appropriate for reading state that is not in the DOM, like localStorage or the analytics dataLayer, and for preparing the environment. I do not write clicks or typing through JS in tests, because the test then passes where a user would get stuck. And I remember that a script does not wait, so I wrap state reads in a wait.
▶ 7 6
44 Як працювати з iframe у Selenide і як повернутися до основної сторінки? How do you work with an iframe in Selenide, and how do you get back to the main page?
Контексти й файли Contexts and files Middle ▶

Що це. 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.

Платіжний фрейм у Page Object з гарантованим поверненням
A payment frame in a Page Object with a guaranteed way back
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 гарантує повернення в корінь навіть тоді, коли перевірка всередині фрейму впала.
🔧 Syntax breakdown: 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.
💡 Простими словами: Зайшов у фрейм — вийди з нього в тому ж методі.
💡 In plain words: If you entered a frame, leave it in the same method.
🗣 Як читати вголос Фрейм — це окремий документ, тому спершу я перемикаюся через switchTo().frame за іменем, id або елементом. У Selenide це перемикання ще й чекає появи фрейму. Для вкладених фреймів використовую innerFrame зі списком імен. Головне правило — повернутися через defaultContent, і я роблю це у finally всередині методу Page Object, бо інакше наступні кроки шукатимуть елементи сторінки у фреймі й падатимуть з дивними таймаутами.
🗣 How to say it out loud A frame is a separate document, so first I switch with switchTo().frame by name, id or element. In Selenide that switch also waits for the frame to appear. For nested frames I use innerFrame with a list of names. The main rule is to come back with defaultContent, and I do it in finally inside a Page Object method, otherwise the following steps look for page elements inside the frame and fail with odd timeouts.
45 Як перемикатися між вкладками й вікнами браузера в Selenide? How do you switch between browser tabs and windows in Selenide?
Контексти й файли Contexts and files Middle ▶

Що це. Кожна вкладка чи вікно для 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.

Нова вкладка, перевірка і повернення за handle
A new tab, a check and a return by handle
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().
🔧 Syntax breakdown: 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().
💡 Простими словами: Запам’ятай handle головної вкладки на початку, і повернутися завжди буде просто.
💡 In plain words: Remember the main tab handle at the start, and getting back will always be easy.
🗣 Як читати вголос Нова вкладка не забирає фокус драйвера, тому після кліку я перемикаюся через switchTo().window за індексом або заголовком, а Selenide сам чекає, поки вкладка з’явиться. Перед цим перевіряю кількість вікон умовою numberOfWindows. На початку зберігаю handle головної вкладки, бо порядок індексів у браузерах не гарантований. Після closeWindow одразу перемикаюся назад, інакше наступна команда впаде з NoSuchWindowException.
🗣 How to say it out loud A new tab does not take the driver focus, so after the click I switch with switchTo().window by index or title, and Selenide waits for the tab to appear. Before that I check the window count with the numberOfWindows condition. At the start I save the main tab handle, because index order across browsers is not guaranteed. After closeWindow I switch back immediately, otherwise the next command fails with NoSuchWindowException.
46 Як обробляти JavaScript alert, confirm і prompt у Selenide? How do you handle JavaScript alert, confirm and prompt in Selenide?
Контексти й файли Contexts and files Middle ▶

Що це. Нативні діалоги браузера блокують сторінку: поки 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.

Підтвердити, скасувати, ввести текст і прочитати діалог
Accept, cancel, type into a dialog and read it
$("#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.
🔧 Syntax breakdown: 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.
💡 Простими словами: Нативний діалог закривають confirm, dismiss чи prompt, а модалку на div — звичайним кліком.
💡 In plain words: A native dialog is closed with confirm, dismiss or prompt; a div modal is closed with an ordinary click.
🗣 Як читати вголос Для нативних діалогів у Selenide є три методи: confirm натискає OK, dismiss — Cancel, prompt вводить текст і підтверджує. Кожен чекає появи діалогу й повертає його текст, а версії з рядком ще й перевіряють, що текст саме той. Якщо мені потрібен сирий Alert, я беру switchTo().alert, який теж чекає. Для кнопки видалення завжди пишу два тести: із підтвердженням і зі скасуванням. І розрізняю нативні діалоги та модалки на div, бо друге — просто елементи сторінки.
🗣 How to say it out loud For native dialogs Selenide has three methods: confirm presses OK, dismiss presses Cancel, and prompt types text and accepts. Each waits for the dialog and returns its text, and the versions taking a string also check that the text is right. If I need a raw Alert, I use switchTo().alert, which waits as well. For a delete button I always write two tests, one accepting and one cancelling. And I tell native dialogs apart from div modals, since the latter are just page elements.
47 Як завантажити файл на сервер через input type=file у Selenide? How do you upload a file through input type=file in Selenide?
Контексти й файли Contexts and files Junior ▶

Що це. Системний діалог вибору файлу 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.

Завантаження з ресурсів, з диска і кількох файлів
Uploading from resources, from disk and several files at once
// файл лежить у 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.
🔧 Syntax breakdown: 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.
💡 Простими словами: Файл передається в input, а не через системне вікно. Шукай input, навіть якщо він прихований.
💡 In plain words: The file goes into the input, not through the system window. Look for the input, even if it is hidden.
🗣 Як читати вголос Системний діалог вибору файлу тест не бачить, тому я передаю файл прямо в input type=file. Для тестових даних беру uploadFromClasspath з ресурсів проєкту, а для згенерованих файлів — uploadFile з File. Прихований input не заважає, а на Grid файл сам передається на ноду, бо Selenide ставить LocalFileDetector. Після завантаження я завжди перевіряю результат на сторінці, а також пишу негативні тести на розмір і розширення.
🗣 How to say it out loud A test cannot see the system file picker, so I pass the file directly into input type=file. For test data I use uploadFromClasspath with project resources, and for generated files uploadFile with a File. A hidden input is not a problem, and on Grid the file travels to the node by itself because Selenide sets a LocalFileDetector. After uploading I always check the result on the page, and I also write negative tests for size and extension.
48 Як скачати файл у Selenide-тесті і перевірити його вміст? How do you download a file in a Selenide test and check its content?
Контексти й файли Contexts and files Middle ▶

Що це. $("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.

Скачати CSV і перевірити вміст
Download a CSV and check its content
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, бо файл уже на диску і чекати нічого не треба.
🔧 Syntax breakdown: 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.
💡 Простими словами: download() дає тобі File. Справжній тест починається, коли ти відкриваєш його і читаєш вміст.
💡 In plain words: download() gives you a File. The real test starts when you open it and read the content.
🗣 Як читати вголос Скачування я роблю методом download на посиланні чи кнопці: він чекає файл і повертає File. Через DownloadOptions можу змінити режим для одного виклику, відфільтрувати файл за розширенням чи ім’ям і дати більше часу на великий експорт. Файли лягають у build/downloads. Але я не зупиняюсь на факті скачування: читаю CSV рядками або PDF через бібліотеку і перевіряю заголовок, кількість записів і кодування, бо баги зазвичай саме там.
🗣 How to say it out loud I download with the download method on a link or button: it waits for the file and returns a File. Through DownloadOptions I can change the mode for one call, filter the file by extension or name, and allow more time for a large export. Files land in build/downloads. But I do not stop at the fact of the download: I read the CSV line by line or the PDF through a library and check the header, the record count and the encoding, because that is usually where the bugs are.
49 Як обрати режим скачування файлів у Selenide: HTTPGET, FOLDER, PROXY чи CDP? How do you choose a file download mode in Selenide: HTTPGET, FOLDER, PROXY or CDP?
Контексти й файли Contexts and files Senior ▶

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.

Порівняння режимів і перемикання для одного виклику
Comparing the modes and switching for one call
Режим    | Як отримує файл                  | Елементи         | Віддалений браузер
---------+----------------------------------+------------------+----------------------------
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));
🔧 Розбір синтаксису: Таблиця зводить чотири значення enum FileDownloadMode за трьома питаннями: як файл потрапляє до тесту, з якими елементами це працює і що потрібно для Grid. Рядок selenide.fileDownload задає режим глобально через файл властивостей, а DownloadOptions.using(...) перевизначає його лише для одного download.
🔧 Syntax breakdown: The table sums up the four values of the 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.
💡 Простими словами: HTTPGET — для посилань, FOLDER чи CDP — для кнопок і форм, PROXY — коли проксі вже є і мережа дозволяє.
💡 In plain words: HTTPGET for links, FOLDER or CDP for buttons and forms, PROXY when you already have the proxy and the network allows it.
🗣 Як читати вголос Режим скачування я обираю від того, як сайт віддає файли. Якщо це прості посилання, лишаю HTTPGET: він швидкий і працює скрізь, бо просто робить GET з cookies. Коли файл з’являється після форми чи JS-кнопки, для Chrome беру CDP, а для кількох браузерів — FOLDER, і на Grid підключаю модуль selenide-grid або selenide-selenoid, бо папка живе в контейнері. PROXY розглядаю, лише коли проксі вже потрібен і браузер бачить машину з тестами. І за потреби перемикаю режим для одного виклику, не ламаючи глобальну конфігурацію.
🗣 How to say it out loud I choose the download mode based on how the site serves files. If they are plain links, I keep HTTPGET: it is fast and works everywhere, since it just sends a GET with cookies. When the file appears after a form or a JS button, I use CDP for Chrome and FOLDER for several browsers, and on Grid I add the selenide-grid or selenide-selenoid module, because the folder lives in the container. I consider PROXY only when the proxy is already needed and the browser can reach the test machine. And if needed I switch the mode for one call without breaking the global configuration.
▶ 8 8
50 Які параметри Configuration налаштовують найчастіше і що вони означають? Which Configuration settings are tuned most often, and what do they mean?
Конфігурація Configuration Junior ▶

Що це. 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.

Типовий базовий клас із конфігурацією
A typical base class with configuration
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 викликає його один раз на клас.
🔧 Syntax breakdown: All fields are plain 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 — це пульт керування всім прогоном. Налаштуй його один раз до першого open() і не крути ручки посеред тестів.
💡 In plain words: Configuration is the remote control for the whole run. Set it once before the first open() and do not turn the knobs in the middle of tests.
🗣 Як читати вголос Я тримаю налаштування в одному базовому класі або у файлі властивостей і задаю їх до першого open. Найчастіше це браузер, headless, розмір вікна, baseUrl і таймаут. Розмір вікна в CI фіксую завжди, бо інакше мобільна верстка ховає елементи. Ще пам’ятаю про pageLoadStrategy: eager пришвидшує тести на важких сторінках, але тоді картинки й сторонні скрипти можуть ще вантажитись. І головне — поля статичні, тож міняти їх посеред паралельного прогону не можна.
🗣 How to say it out loud I keep settings in one base class or in a properties file and set them before the first open. Most often that is the browser, headless mode, window size, baseUrl and the timeout. I always pin the window size in CI, because otherwise the mobile layout hides elements. I also keep pageLoadStrategy in mind: eager speeds up tests on heavy pages, but images and third-party scripts may still be loading. And the key point is that the fields are static, so I never change them in the middle of a parallel run.
51 Як передати ChromeOptions і власні capabilities у Selenide? How do you pass ChromeOptions and custom capabilities to Selenide?
Конфігурація Configuration Middle ▶

Що це. Поле 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.

Опції Chrome з налаштуваннями профілю і Selenoid
Chrome options with profile preferences and Selenoid settings
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.
🔧 Syntax breakdown: 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, задавай через поле. У capabilities клади лише те, чого Selenide не знає.
💡 In plain words: Anything that has its own Configuration field goes through that field. Put into capabilities only what Selenide does not know about.
🗣 Як читати вголос Опції браузера я передаю через Configuration.browserCapabilities: це MutableCapabilities, тому туди лягає звичайний ChromeOptions з аргументами, prefs і вендорськими ключами для Selenoid. Selenide зливає це зі своїми налаштуваннями, тож розмір вікна чи headless я задаю через його поля, а не аргументами, щоб не було конфлікту. Якщо набір крос-браузерний, опції збираю в залежності від обраного браузера, бо ChromeOptions у Firefox нічого не дадуть.
🗣 How to say it out loud I pass browser options through Configuration.browserCapabilities: it is a MutableCapabilities, so a regular ChromeOptions with arguments, prefs and vendor keys for Selenoid fits right in. Selenide merges that with its own settings, so I set the window size or headless mode through its fields rather than as arguments, to avoid clashes. If the suite is cross-browser, I build options depending on the chosen browser, because ChromeOptions do nothing for Firefox.
52 Як задавати конфігурацію Selenide без перекомпіляції: selenide.properties і -Dselenide? How do you configure Selenide without recompiling: selenide.properties and -Dselenide?
Конфігурація Configuration Middle ▶

Що це. Кожне поле 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.

Файл властивостей і прокидання -D у Gradle
A properties file and forwarding -D in Gradle
# 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() })
# }
🔧 Розбір синтаксису: Файл — звичайний Java .properties: пари ключ=значення, коментарі через #. Ключ дорівнює імені поля Configuration з префіксом selenide.. Фрагмент для Gradle Kotlin DSL фільтрує системні властивості процесу Gradle за префіксом і копіює їх у JVM, де виконуються тести.
🔧 Syntax breakdown: The file is a regular Java .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.
💡 Простими словами: Файл — для значень за замовчуванням, -D — для середовища, код — лише для того, що ніколи не змінюється.
💡 In plain words: The file holds defaults, -D holds the environment, and code holds only what never changes.
🗣 Як читати вголос Налаштування я розкладаю на три рівні. У selenide.properties лежать локальні значення за замовчуванням, CI перевизначає потрібне через -Dselenide точка щось, а в коді я присвоюю лише те, що не повинно мінятися між середовищами. Пам’ятаю пастку: присвоєння в коді перемагає -D, тож захардкожений браузер зламає крос-браузерну джобу. А в Gradle системні властивості треба явно прокинути в задачу test, бо тести йдуть в окремому JVM.
🗣 How to say it out loud I split settings into three levels. selenide.properties holds local defaults, CI overrides what it needs with -Dselenide dot something, and in code I only assign what must not change between environments. I remember the trap: an assignment in code beats -D, so a hardcoded browser breaks a cross-browser job. And in Gradle system properties have to be forwarded to the test task explicitly, because tests run in a separate JVM.
53 Як запустити Selenide-тести на Selenium Grid або Selenoid? How do you run Selenide tests on Selenium Grid or Selenoid?
Конфігурація Configuration Middle ▶

Що це. Досить задати 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.

Перемикання на Selenoid з відео і VNC
Switching to Selenoid with video and VNC
@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 їх просто проігнорує.
🔧 Syntax breakdown: 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.
💡 Простими словами: Один рядок Configuration.remote переносить увесь набір з ноутбука на ферму браузерів.
💡 In plain words: A single Configuration.remote line moves the whole suite from a laptop to a browser farm.
🗣 Як читати вголос Щоб запустити тести на Grid чи Selenoid, я задаю Configuration.remote з адресою хаба, найчастіше системною властивістю з CI, а тести не чіпаю. Selenide створює RemoteWebDriver з тими самими налаштуваннями і сам ставить LocalFileDetector, тож завантаження файлів працює. Для Selenoid вмикаю VNC і відео через selenoid:options, щоб дивитися падіння. Для скачування у режимі FOLDER підключаю модуль selenide-selenoid чи selenide-grid, а кількість потоків узгоджую з кількістю слотів.
🗣 How to say it out loud To run tests on Grid or Selenoid I set Configuration.remote to the hub address, usually through a system property from CI, and leave the tests alone. Selenide creates a RemoteWebDriver with the same settings and sets a LocalFileDetector by itself, so file uploads keep working. For Selenoid I enable VNC and video via selenoid:options to watch failures. For FOLDER downloads I add the selenide-selenoid or selenide-grid module, and I keep the thread count in line with the number of slots.
54 Як підключити власний WebDriverProvider і чим він кращий за setWebDriver? How do you plug in your own WebDriverProvider, and why is it better than setWebDriver?
Конфігурація Configuration Middle ▶

Що це. 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.

Провайдер для хмарного браузера з токеном
A provider for a cloud browser with a token
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 тут умовний, у реального постачальника він свій.
🔧 Syntax breakdown: 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.
💡 Простими словами: Провайдер вирішує, як народжується браузер, а Selenide і далі вирішує, коли він народжується і помирає.
💡 In plain words: The provider decides how the browser is born, while Selenide still decides when it is born and when it dies.
🗣 Як читати вголос Коли стандартних опцій замало, я реалізую WebDriverProvider з методом createDriver і записую ім’я класу в Configuration.browser. Усередині мерджу capabilities від Selenide у свої опції, щоб не загубити розмір вікна, і додаю те, що потрібно, наприклад токен хмарного провайдера. Перевага над setWebDriver у тому, що драйвер і далі живе по потоку, закривається і перестворюється самим Selenide. Але якщо мені потрібні вбудований проксі чи тека завантажень, я дивлюся в бік DriverFactory.
🗣 How to say it out loud When standard options are not enough, I implement WebDriverProvider with a createDriver method and put the class name into Configuration.browser. Inside, I merge Selenide's capabilities into my options so I do not lose the window size, and I add what I need, such as a cloud provider token. The advantage over setWebDriver is that the driver still lives per thread and is closed and recreated by Selenide itself. But if I need the embedded proxy or the downloads folder, I look at DriverFactory instead.
55 Навіщо вбудований проксі Selenide і що вмикає Configuration.proxyEnabled? What is the embedded Selenide proxy for, and what does Configuration.proxyEnabled turn on?
Конфігурація Configuration Middle ▶

Що це. При 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.

Запис запитів і підміна відповіді через проксі
Recording requests and mocking a response through the proxy
// потрібна залежність 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");
🔧 Розбір синтаксису: Фільтр запиту — лямбда з трьома параметрами BrowserUp: запит Netty, його вміст і метадані з URL. Повернення null пропускає запит далі, а повернена відповідь зупинила б його. urlEndsWith(GET, ...) — матчер із com.codeborne.selenide.proxy.RequestMatchers, а GET імпортують з RequestMatcher.HttpMethod. Ім’я мока потрібне, щоб потім зняти саме його через reset.
🔧 Syntax breakdown: A request filter is a lambda with three BrowserUp parameters: the Netty request, its contents and metadata including the URL. Returning 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.
💡 Простими словами: Проксі ставить тест між браузером і сервером: усе, що летить мережею, можна побачити або підмінити.
💡 In plain words: The proxy puts the test between the browser and the server: anything travelling over the network can be seen or replaced.
🗣 Як читати вголос Вбудований проксі вмикається прапорцем proxyEnabled і в сьомій версії потребує артефакту selenide-proxy. Я використовую його для трьох речей: скачування файлів, які віддає форма, а не посилання, запису запитів браузера і точкових моків на кшталт порожнього списку чи помилки сервера. За замовчуванням він вимкнений, бо браузер на Grid має бачити машину з тестами, і це не завжди так. Тому на віддалених фермах я спершу перевіряю мережу або обираю інший підхід.
🗣 How to say it out loud The embedded proxy is switched on with the proxyEnabled flag and in version seven needs the selenide-proxy artifact. I use it for three things: downloading files served by a form rather than a link, recording browser requests, and targeted mocks such as an empty list or a server error. It is off by default because a browser on Grid has to see the test machine, and that is not always the case. So on remote farms I check the network first or choose another approach.
56 Як запускати Selenide-тести паралельно і що треба знати про потокобезпечність? How do you run Selenide tests in parallel, and what do you need to know about thread safety?
Конфігурація Configuration Senior ▶

Модель. Статичні $ і 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.

Паралельність у JUnit 5 і другий браузер у тесті
Parallelism in JUnit 5 and a second browser in a test
// 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() — умовний клієнт тестових даних.
🔧 Syntax breakdown: The four keys enable parallelism, make the mode concurrent and pin four threads. 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.
💡 Простими словами: Браузер у кожного потоку свій, а от конфігурація і тестові дані — спільні, якщо ти не подбав про інше.
💡 In plain words: Each thread gets its own browser, but configuration and test data are shared unless you take care otherwise.
🗣 Як читати вголос Паралельність у Selenide вмикається в раннері, бо бібліотека вже тримає окремий драйвер на потік. У JUnit 5 я задаю чотири ключі в junit-platform.properties, у TestNG — parallel і thread-count. Далі стежу за трьома речами: Configuration статична, тому посеред тестів її не змінюю; слухачі на кшталт Allure реєструю на кожен потік; тестові дані кожен тест створює собі сам. Якщо в сценарії два користувачі, піднімаю другий SelenideDriver з копією конфігурації. І кількість потоків підбираю під слоти Grid, перевіряючи реальний час прогону.
🗣 How to say it out loud Parallelism in Selenide is switched on in the runner, because the library already keeps a separate driver per thread. In JUnit 5 I set four keys in junit-platform.properties, in TestNG parallel and thread-count. Then I watch three things: Configuration is static, so I never change it mid-test; listeners such as Allure are registered per thread; and every test creates its own test data. If a scenario has two users, I start a second SelenideDriver with a copy of the configuration. And I size the thread count to the Grid slots, checking the real run time.
57 Як Selenide працює з мобільними застосунками через Appium? How does Selenide work with mobile apps through Appium?
Конфігурація Configuration Middle ▶

Що це. Для мобільних тестів є окремий модуль 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.

Провайдер Android і перший мобільний тест
An Android provider and a first mobile test
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 повертає елемент з тими самими очікуваннями, що й у вебі.
🔧 Syntax breakdown: 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-стиль, але замість open() — launchApp(), а замість CSS — accessibility id.
💡 In plain words: The same Selenide style, but launchApp() instead of open() and accessibility ids instead of CSS.
🗣 Як читати вголос Для мобільних застосунків я підключаю модуль selenide-appium. Драйвер створюю сам у WebDriverProvider, наприклад AndroidDriver з опціями UiAutomator2, вказую провайдер у Configuration.browser і запускаю застосунок через SelenideAppium.launchApp. Далі пишу звичні $ і shouldHave, тільки з AppiumBy, і найкраще з accessibility id, бо XPath на мобілці повільний і крихкий. Таймаут для емулятора зазвичай піднімаю, а свайпи й тапи беру з самого модуля.
🗣 How to say it out loud For mobile apps I add the selenide-appium module. I create the driver myself in a WebDriverProvider, for example an AndroidDriver with UiAutomator2 options, point Configuration.browser at the provider and start the app with SelenideAppium.launchApp. Then I write the usual $ and shouldHave, just with AppiumBy, preferably accessibility ids, because XPath on mobile is slow and brittle. I usually raise the timeout for the emulator, and I take swipes and taps from the module itself.
▶ 9 6
58 Які артефакти Selenide зберігає при падінні тесту і де їх шукати? Which artifacts does Selenide save when a test fails, and where do you find them?
Звіти Reports Junior ▶

Що це. Коли падає перевірка чи дія 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.

Налаштування артефактів і ручні знімки
Artifact settings and manual snapshots
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, якщо знімок не вдалося зробити через технічну проблему. Ім’я автоматичного знімка складається з часової позначки й лічильника.
🔧 Syntax breakdown: 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 приходить із доказами: картинкою того, що бачив браузер, і HTML того, що було в DOM.
💡 In plain words: A Selenide failure arrives with evidence: a picture of what the browser saw and the HTML of what was in the DOM.
🗣 Як читати вголос При падінні Selenide сам зберігає скріншот і HTML сторінки і дописує шляхи до них у повідомлення про помилку. Лежать вони в reportsFolder, за замовчуванням build/reports/tests, тож у Maven я перенаправляю теку в target. У CI задаю reportsUrl, щоб у лозі були посилання, які відкриваються одним кліком. Для важливих, але успішних кроків іноді роблю знімок вручну, а скріншоти ніколи не вимикаю, бо без HTML і картинки флакі-падіння майже не розібрати.
🗣 How to say it out loud On failure Selenide saves a screenshot and the page HTML by itself and appends their paths to the error message. They go to reportsFolder, build/reports/tests by default, so in Maven I redirect that folder into target. In CI I set reportsUrl so the log contains links that open in one click. For important but passing steps I sometimes take a snapshot by hand, and I never turn screenshots off, because without the HTML and the picture flaky failures are almost impossible to analyse.
59 Як отримати скріншот при будь-якому падінні тесту, а не лише при падінні перевірки Selenide? How do you get a screenshot on any test failure, not only when a Selenide assertion fails?
Звіти Reports Middle ▶

Проблема. Автоматичний скріншот 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 і TestNG
A screenshot for any failure in JUnit 5 and TestNG
// 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 має таку саму коротку назву.
🔧 Syntax breakdown: @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.
💡 Простими словами: Selenide фотографує свої помилки сам, а ScreenShooter — будь-які.
💡 In plain words: Selenide photographs its own failures by itself; ScreenShooter photographs any failure.
🗣 Як читати вголос Вбудований скріншот спрацьовує лише на падінні дії чи should, а тест може впасти й на assertEquals чи винятку в коді. Тому в JUnit 5 я вішаю ScreenShooterExtension, у TestNG — слухача ScreenShooter, а в старих проєктах на JUnit 4 — однойменне правило. Поруч люблю TextReportExtension: у лозі видно всі кроки Selenide з часом. Режим знімків для успішних тестів вмикаю лише тимчасово, бо він роздуває артефакти, і пам’ятаю, що другий SelenideDriver ці розширення не фотографують.
🗣 How to say it out loud The built-in screenshot fires only when an action or a should call fails, but a test can also fail on assertEquals or an exception in the code. So in JUnit 5 I attach ScreenShooterExtension, in TestNG the ScreenShooter listener, and in old JUnit 4 projects the rule of the same name. Next to it I like TextReportExtension: the log shows every Selenide step with its timing. I switch on snapshots for passing tests only temporarily, since it bloats artifacts, and I remember that these extensions do not capture a second SelenideDriver.
60 Як підключити Selenide до Allure і що робить AllureSelenide? How do you connect Selenide to Allure, and what does AllureSelenide do?
Звіти Reports Middle ▶

Що це. Інтеграцію дає артефакт 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.

Реєстрація слухача Allure в базовому класі
Registering the Allure listener in a base class
// залежності: 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.
🔧 Syntax breakdown: The first argument of 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.
💡 Простими словами: AllureSelenide перекладає кожен рядок Selenide-тесту на крок звіту, а скріншот чіпляє до того кроку, де все зламалось.
💡 In plain words: AllureSelenide turns every line of a Selenide test into a report step and pins the screenshot to the step where things broke.
🗣 Як читати вголос Для Allure я додаю allure-selenide і реєструю AllureSelenide як слухача SelenideLogger, а не як JUnit-розширення. Після цього кожна дія й перевірка стає кроком звіту, а на впалому кроці з’являються скріншот і, якщо треба, HTML. Реєструю слухача в BeforeEach базового класу, бо слухачі прив’язані до потоку і в паралельному прогоні інакше губляться кроки. Разом з as() на важливих елементах звіт читається як нормальний сценарій, який зрозуміє навіть менеджер.
🗣 How to say it out loud For Allure I add allure-selenide and register AllureSelenide as a SelenideLogger listener, not as a JUnit extension. After that every action and assertion becomes a report step, and the failed step gets a screenshot and, if needed, the HTML. I register the listener in BeforeEach of a base class, because listeners are bound to a thread and in a parallel run steps would otherwise go missing. Together with as() on key elements, the report reads like a proper scenario that even a manager can follow.
61 Як увімкнути soft assertions у Selenide для JUnit 5 і TestNG? How do you enable soft assertions in Selenide for JUnit 5 and TestNG?
Звіти Reports Middle ▶

Що це. 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.

Soft-режим для одного класу в JUnit 5
Soft mode for one class in JUnit 5
@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 захищає інші класи, але в паралельному прогоні статичне поле все одно спільне.
🔧 Syntax breakdown: 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.
💡 Простими словами: Soft-режим — це чекліст: тест пройде всі пункти й наприкінці покаже, які з них не збіглися.
💡 In plain words: Soft mode is a checklist: the test walks through every item and at the end shows which ones did not match.
🗣 Як читати вголос Щоб увімкнути м’які перевірки, мені потрібні дві речі одночасно: assertionMode SOFT і інтеграція з раннером, яка викине помилки наприкінці. У JUnit 5 це SoftAssertsExtension, у TestNG — слухач SoftAsserts з модуля selenide-testng. Пам’ятаю, що м’якими стають лише should-методи, а клік по відсутній кнопці так само зупинить тест. І оскільки режим задається статичним полем, я вмикаю його свідомо для конкретного набору, а не для всього проєкту.
🗣 How to say it out loud To enable soft checks I need two things at once: assertionMode SOFT and a runner integration that throws the errors at the end. In JUnit 5 that is SoftAssertsExtension, in TestNG the SoftAsserts listener from the selenide-testng module. I remember that only should methods become soft, and clicking a missing button still stops the test. And since the mode is a static field, I switch it on deliberately for a specific suite rather than for the whole project.
62 Soft assertions чи fail-fast: коли що обирати в UI-тестах? Soft assertions or fail-fast: which do you choose in UI tests, and when?
Звіти Reports Senior ▶

Суть вибору. 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.

Fail-fast передумови і м’який блок незалежних перевірок
Fail-fast preconditions and a soft block of independent checks
@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-режимом.
🔧 Syntax breakdown: AssertJ 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.
💡 Простими словами: Якщо друга перевірка має сенс лише після першої, це fail-fast. Якщо їх можна переставити місцями, це кандидат на soft.
💡 In plain words: If the second check only makes sense after the first, that is fail-fast. If you can swap them around, it is a candidate for soft.
🗣 Як читати вголос Я вибираю режим за залежністю між перевірками. Якщо кроки йдуть ланцюжком, як логін, кошик і оплата, лишаю fail-fast, бо інакше одна причина дає каскад із десятка падінь і ще й чекає таймаут на кожному. Якщо ж я перевіряю кілька незалежних фактів на одному екрані, soft економить цикли перезапуску. Глобально soft не вмикаю ніколи: роблю це точково для класу, а для даних з API беру AssertJ assertSoftly, який не чіпає режим Selenide.
🗣 How to say it out loud I choose the mode by how the checks depend on each other. If steps form a chain, like login, cart and payment, I keep fail-fast, because otherwise one cause turns into a cascade of a dozen failures, each waiting out its timeout. If I am checking several independent facts on one screen, soft saves rerun cycles. I never switch soft on globally: I do it for a specific class, and for API data I use AssertJ assertSoftly, which leaves the Selenide mode alone.
63 Як налаштувати запуск Selenide-тестів у CI, щоб падіння можна було розібрати без повторного запуску? How do you set up Selenide tests in CI so that a failure can be analysed without a rerun?
Звіти Reports Senior ▶

Мета. Кожне падіння в 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.

Джоба GitHub Actions з Selenoid, Allure і артефактами
A GitHub Actions job with Selenoid, Allure and artifacts
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 з образами браузерів; тут він опущений.
🔧 Syntax breakdown: The 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.
💡 Простими словами: Добрий CI відповідає на питання «чому впало» ще до того, як ти відкрив IDE.
💡 In plain words: Good CI answers "why did it fail" before you even open the IDE.
🗣 Як читати вголос Я будую CI так, щоб падіння приходило з доказами. Оточення стабільне: headless або Selenoid з фіксованою версією, явний розмір вікна, таймаути через властивості. Теку звітів публікую як артефакт завжди, навіть при падінні, і задаю reportsUrl, щоб посилання в лозі відкривались одразу. Allure з історією показує, чи тест флакі давно, а відео вмикаю вибірково для тестів під розслідуванням. Перезапуск дозволяю один, і тести, що пройшли з другої спроби, окремо підсвічую, щоб вони не ховались.
🗣 How to say it out loud I build CI so that a failure arrives with evidence. The environment is stable: headless or Selenoid with a pinned version, an explicit window size, timeouts via properties. I always publish the reports folder as an artifact, even on failure, and set reportsUrl so the links in the log open straight away. Allure with history shows whether a test has been flaky for long, and I enable video selectively for tests under investigation. I allow a single rerun, and I flag tests that passed on the second attempt so they do not hide.
▶ 10 10
64 Як писати Page Object на Selenide і що змінюється порівняно з PageFactory у Selenium? How do you write a Page Object with Selenide, and what changes compared with Selenium PageFactory?
Архітектура Architecture Middle ▶

Що це. 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.

Два стилі Page Object і тест, що ними користується
Two Page Object styles and a test that uses them
// стиль 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-ланцюжок у тесті.
🔧 Syntax breakdown: 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.
💡 Простими словами: Page Object відповідає на питання «що користувач може зробити на цій сторінці», а тест — «що ми хочемо довести».
💡 In plain words: A Page Object answers "what can the user do on this page", and the test answers "what do we want to prove".
🗣 Як читати вголос У Selenide я пишу Page Object без PageFactory: поля одразу ініціалізую через $, бо вони ліниві, і драйвер у конструктор не передаю. Якщо команда звикла до FindBy, використовую page або open з класом, і фабрика Selenide підставить ліниві елементи. Методи в мене повертають наступну сторінку, щоб тест читався ланцюжком. Getter-и для асертів я не віддаю назовні, а роблю перевірку умовою Selenide всередині методу, щоб не втратити очікування.
🗣 How to say it out loud In Selenide I write Page Objects without PageFactory: I initialise fields right away with $, since they are lazy, and I do not pass a driver into the constructor. If the team is used to FindBy, I use page or open with a class, and the Selenide factory injects lazy elements. My methods return the next page so the test reads as a chain. I do not expose getters for assertions; I do the check with a Selenide condition inside the method so waiting is not lost.
65 Як описувати повторювані компоненти сторінки: Container з @Self чи звичайний клас із кореневим елементом? How do you model repeating page components: a Container with @Self or a plain class with a root element?
Архітектура Architecture Senior ▶

Контекст. На реальній сторінці повторюються не сторінки, а компоненти: картка товару, рядок таблиці, 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.

Один компонент двома способами
One component written two ways
// варіант 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)) вибирає картку за назвою.
🔧 Syntax breakdown: @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.
💡 Простими словами: Компонент завжди шукає від свого кореня. Як саме цей корінь передати — анотацією чи конструктором — справа смаку команди.
💡 In plain words: A component always searches from its own root. Whether that root arrives through an annotation or a constructor is a matter of team taste.
🗣 Як читати вголос Повторювані шматки сторінки я виношу в компоненти, які шукають усе від свого кореня. У Selenide для цього є інтерфейс Container з анотацією Self, і фабрика сама створить список таких компонентів; старий ElementsContainer у сьомій версії вже видалили. Проте в нових проєктах я частіше пишу звичайний клас із конструктором, що приймає кореневий SelenideElement: там немає рефлексії, його легко дебажити і передати корінь, знайдений за текстом. Принцип один: жодного глобального пошуку зсередини компонента.
🗣 How to say it out loud I extract repeating parts of a page into components that search everything from their own root. Selenide offers the Container interface with the Self annotation for this, and the factory builds a list of such components for me; the old ElementsContainer was removed in version seven. On new projects, though, I more often write a plain class whose constructor takes the root SelenideElement: there is no reflection, it is easy to debug, and I can pass a root found by text. The principle is the same: no global lookups from inside a component.
66 Як готувати стан для UI-тесту без UI: логін через API, cookies і localStorage? How do you prepare state for a UI test without the UI: login via API, cookies and localStorage?
Архітектура Architecture Senior ▶

Проблема. Якщо кожен із 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.

Швидкий логін: сесія з API, cookie в браузер
Fast login: a session from the API, a cookie into the browser
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.
🔧 Syntax breakdown: 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 проходь лише те, що перевіряєш. Решту стану підкладай збоку.
💡 In plain words: Go through the UI only for what you are testing. Plant the rest of the state from the side.
🗣 Як читати вголос Я не логінюся через форму в кожному тесті. Дані створюю через API, сесію отримую теж через API, потім відкриваю легку сторінку на домені, кладу cookie або токен у localStorage і вже тоді йду на сторінку, яку перевіряю. Прогін стає в рази швидшим, а падіння показує саме зламану фічу. Логін-форму при цьому покриваю кількома окремими тестами, а швидкий логін тримаю в одному хелпері, щоб зміна формату сесії правилася в одному місці.
🗣 How to say it out loud I do not log in through the form in every test. I create data through the API, I get the session through the API too, then I open a lightweight page on the domain, put a cookie or a token into localStorage, and only then go to the page under test. The run becomes several times faster, and a failure points at the broken feature itself. I still cover the login form with a few dedicated tests, and I keep the fast login in one helper, so a change in the session format is fixed in one place.
67 Як мокати або перехоплювати мережеві запити в Selenide-тестах: проксі, CDP чи окремий стаб-сервер? How do you mock or intercept network requests in Selenide tests: the proxy, CDP or a separate stub server?
Архітектура Architecture Senior ▶

Навіщо. Щоб перевірити, як 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.

Та сама підміна відповіді через проксі Selenide і через CDP
The same response stub through the Selenide proxy and through CDP
// 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.
🔧 Syntax breakdown: 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.
💡 Простими словами: Мок перевіряє, як UI реагує на відповідь. Чи правильна сама відповідь бекенду — питання до інших тестів.
💡 In plain words: A mock checks how the UI reacts to a response. Whether the backend response itself is right is a question for other tests.
🗣 Як читати вголос Для підміни мережі в мене три інструменти. Вбудований проксі Selenide працює з будь-яким браузером і вміє мокати відповіді та читати трафік, але браузер має достукатися до машини з тестами, що на Grid часто не так. CDP через NetworkInterceptor точний і швидкий, але лише для Chromium. А зовнішні інтеграції на кшталт платежів я мокаю стаб-сервером на рівні стенду, бо це найстабільніше. Частину сценаріїв завжди лишаю на справжньому бекенді, щоб моки не створили ілюзію зеленого.
🗣 How to say it out loud I have three tools for faking the network. The built-in Selenide proxy works with any browser and can mock responses and read traffic, but the browser has to reach the test machine, which on a Grid is often not the case. CDP through NetworkInterceptor is precise and fast, but only for Chromium. External integrations such as payments I mock with a stub server at the stand level, because that is the most stable option. I always keep some scenarios on the real backend so mocks do not create an illusion of green.
68 Що дають CDP і WebDriver BiDi у Selenide 7 і як ними користуватися з тестів? What do CDP and WebDriver BiDi give you in Selenide 7, and how do you use them from tests?
Архітектура Architecture Senior ▶

Що це. 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.

Готовий метод Selenide, BiDi-підписка на помилки JS і CDP-емуляція повільної мережі
A ready Selenide method, a BiDi subscription to JS errors and CDP slow-network emulation
// готовий метод: мобільна емуляція (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.
🔧 Syntax breakdown: 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.
💡 Простими словами: Спершу пошукай готовий метод Selenide. BiDi — для крос-браузерних подій, CDP — для суто хромівських трюків.
💡 In plain words: Look for a ready Selenide method first. BiDi is for cross-browser events, CDP for Chrome-only tricks.
🗣 Як читати вголос CDP — це внутрішній протокол Chromium, а BiDi — новий стандарт W3C, що працює в усіх браузерах і вміє сам надсилати події. Selenide сьомої версії вже використовує їх під капотом: скачування через CDP, скріншоти й логи через BiDi, emulateDevice для мобільної емуляції. Коли мені потрібно більше, я беру сирий драйвер: через HasBiDi підписуюсь на помилки JavaScript, через HasDevTools емулюю повільну мережу. CDP-код я ховаю за своїм класом, бо він ламається з оновленням версії Chrome.
🗣 How to say it out loud CDP is Chromium's internal protocol, and BiDi is the new W3C standard that works in every browser and can push events on its own. Selenide seven already uses them under the hood: downloads through CDP, screenshots and logs through BiDi, emulateDevice for mobile emulation. When I need more, I take the raw driver: through HasBiDi I subscribe to JavaScript errors, through HasDevTools I emulate a slow network. I hide CDP code behind my own class, because it breaks whenever the Chrome version changes.
69 Selenide, чистий Selenium чи Playwright: що обрати для нового проєкту і чому? Selenide, plain Selenium or Playwright: which would you pick for a new project, and why?
Архітектура Architecture Senior ▶

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.

Одна перевірка у трьох інструментах
One check in three tools
// 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");
🔧 Розбір синтаксису: У Selenium ExpectedConditions.textToBe повертає функцію, яку WebDriverWait.until викликає до успіху чи таймауту. У Selenide те саме робить shouldHave з умовою exactText. У Playwright assertThat імпортується з PlaywrightAssertions, а hasText повторює перевірку локатора до свого таймауту — ідея та сама, що в Selenide, але на іншому протоколі.
🔧 Syntax breakdown: In Selenium, 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.
💡 Простими словами: Інструмент обирають під команду й інфраструктуру, а не під найгучнішу статтю цього року.
💡 In plain words: You pick the tool for the team and the infrastructure, not for this year's loudest article.
🗣 Як читати вголос Я порівнюю їх так. Selenium — стандарт і максимальна сумісність, але фреймворк навколо доведеться будувати самому. Selenide — той самий Selenium з готовими очікуваннями, артефактами й коротким API, ідеальний для Java-команди з наявним Grid. Playwright дає автоочікування, моки мережі й trace viewer з коробки, але найсильніший у TypeScript і не покриває справжній Safari та нативні мобільні застосунки. Остаточно вирішують мова команди, інфраструктура і те, хто підтримуватиме тести.
🗣 How to say it out loud This is how I compare them. Selenium is the standard with maximum compatibility, but you build the framework around it yourself. Selenide is the same Selenium with ready waits, artefacts and a compact API, ideal for a Java team with an existing Grid. Playwright gives auto-waiting, network mocks and a trace viewer out of the box, but it is strongest in TypeScript and does not cover real Safari or native mobile apps. In the end, the team's language, the infrastructure and who maintains the tests decide it.
70 Як мігрувати великий набір тестів із чистого Selenium на Selenide? How do you migrate a large test suite from plain Selenium to Selenide?
Архітектура Architecture Senior ▶

Принцип. Не переписувати все за один спринт. 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.

Проміжний стан: стара фабрика драйвера під Selenide і переписаний Page Object
The intermediate state: the old driver factory behind Selenide and a rewritten Page Object
// стара фабрика стає 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 зникають.
🔧 Syntax breakdown: 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.
💡 Простими словами: Мігруй хвилями: спершу драйвер, потім найнестабільніші сторінки, і щоразу міряй результат.
💡 In plain words: Migrate in waves: the driver first, then the flakiest pages, and measure the result every time.
🗣 Як читати вголос Я не переписую все одразу. Спершу віддаю Selenide створення драйвера через WebDriverProvider, що повторює стару фабрику, а старі хелпери беруть драйвер із WebDriverRunner, тож обидва світи живуть в одному прогоні. Далі хвилями переписую Page Object, починаючи з найнестабільніших тестів: прибираю WebDriverWait, sleep і самописні ретраї, асерти над getText замінюю умовами. Implicit wait вимикаю одразу. І до та після кожної хвилі міряю частку flaky-падінь і час прогону.
🗣 How to say it out loud I do not rewrite everything at once. First I hand driver creation to Selenide through a WebDriverProvider that reproduces the old factory, and old helpers take the driver from WebDriverRunner, so both worlds live in the same run. Then I rewrite Page Objects in waves, starting with the flakiest tests: I remove WebDriverWait, sleep and home-made retries, and replace assertions on getText with conditions. I switch off implicit wait right away. And before and after each wave I measure the flaky rate and the run time.
71 Як стабілізувати нестабільний Selenide-набір на рівні команди, а не окремого тесту? How do you stabilise a flaky Selenide suite at the team level rather than test by test?
Архітектура Architecture Senior ▶

Ситуація. Прогін червоніє через раз, розробники звикли тиснути «перезапустити», і червоний білд уже нікого не лякає. Лагодити по одному тесту безглуздо, поки немає даних і правил.

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.

Карантин через тег і прогін нового тесту двадцять разів
Quarantine through a tag and running a new test twenty times
// тест у карантині: має задачу і власника, не блокує злиття
@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 разів, і одне падіння вже сигнал, що тест нестабільний. Коментар біля тегу — мінімальна домовленість: номер задачі, власник і термін.
🔧 Syntax breakdown: @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.
💡 Простими словами: Спершу цифри, потім карантин, потім усунення причин цілими класами — і правила, щоб вони не повернулися.
💡 In plain words: Numbers first, then quarantine, then removing causes as whole classes, and rules so they do not come back.
🗣 Як читати вголос На рівні команди я починаю з даних: збираю історію прогонів і рахую для кожного тесту частку падінь, що зникли при перезапуску, а причини групую за повідомленнями Selenide. Найгірші тести йдуть у карантин з власником і терміном, щоб основний білд знову щось означав. Потім усуваю цілі класи причин: спільні дані, анімації, sleep, асерти над getter-ами, глобально роздутий таймаут. І закріплюю результат правилами рев’ю, метрикою на дашборді та двадцятьма прогонами нового тесту перед злиттям.
🗣 How to say it out loud At team level I start with data: I collect run history and compute, per test, the share of failures that disappeared on rerun, grouping causes by Selenide messages. The worst tests go into quarantine with an owner and a deadline, so the main build means something again. Then I remove whole classes of causes: shared data, animations, sleep, assertions on getters, a globally inflated timeout. And I lock the result in with review rules, a metric on a dashboard and twenty runs of every new test before merge.
72 Як спроєктувати UI-фреймворк на Selenide з нуля для кількох команд? How would you design a Selenide-based UI framework from scratch for several teams?
Архітектура Architecture Senior ▶

Шари. Я розділяю фреймворк на чотири рівні. Конфігурація: один клас, що читає 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.

Одне JUnit-розширення, що збирає інфраструктуру для всіх тестів
One JUnit extension that wires the infrastructure for all tests
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 — умовний клас проєкту, що читає профіль середовища.
🔧 Syntax breakdown: 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.
💡 Простими словами: Добрий фреймворк на Selenide тонкий: він прибирає інфраструктуру з тестів, але не ховає сам Selenide.
💡 In plain words: A good Selenide framework is thin: it takes infrastructure out of tests without hiding Selenide itself.
🗣 Як читати вголос Я будую фреймворк шарами: конфігурація з профілями середовищ, створення драйвера через провайдер або remote, модель UI зі сторінок і компонентів, і тести, де лишаються тільки сценарії. Інфраструктуру збираю в одне JUnit-розширення з власною анотацією: Allure, очищення стану, швидкий логін через API. Бібліотеку публікую як версійований артефакт з власниками й changelog. І свідомо не обгортаю кожен метод Selenide, бо тоді новачкам доведеться вчити наш діалект замість відомого API.
🗣 How to say it out loud I build the framework in layers: configuration with environment profiles, driver creation through a provider or remote, a UI model of pages and components, and tests that contain only scenarios. I gather the infrastructure into one JUnit extension with a custom annotation: Allure, state cleanup, fast login via the API. I publish the library as a versioned artifact with owners and a changelog. And I deliberately do not wrap every Selenide method, because then newcomers would have to learn our dialect instead of a well-known API.
73 Які антипатерни ти шукаєш на код-рев’ю Selenide-тестів? Which anti-patterns do you look for when reviewing Selenide tests?
Архітектура Architecture Senior ▶

Очікування. 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.

Типові знахідки рев’ю і їхні заміни
Typical review findings and their replacements
// погано: 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)) збільшує таймаут лише для цього виклику, не чіпаючи глобальне значення.
🔧 Syntax breakdown: Each bad/good pair shows how one habit is replaced. 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.
💡 Простими словами: Якщо одне зауваження повторюється втретє, воно має стати правилом лінтера, а не коментарем.
💡 In plain words: If the same review comment appears a third time, it should become a linter rule rather than a comment.
🗣 Як читати вголос На рев’ю Selenide-тестів я передусім шукаю все, що ламає очікування: sleep, WebDriverWait поверх Selenide, асерти над getText чи isDisplayed і роздутий глобальний таймаут. Далі дивлюся на силу перевірок: text з коротким рядком часто пропускає помилки, де потрібен exactText. Потім локатори: позиційний XPath, хеші CSS-модулів, селектори просто в тестах. І ізоляцію: залежність від порядку, спільні логіни, зміни Configuration посеред тесту. Зауваження завжди даю з готовою заміною, а повторювані перетворюю на правила лінтера.
🗣 How to say it out loud When reviewing Selenide tests I first look for anything that breaks waiting: sleep, WebDriverWait on top of Selenide, assertions on getText or isDisplayed, and an inflated global timeout. Next I look at how strong the checks are: text with a short string often lets bugs through where exactText is needed. Then locators: positional XPath, CSS-module hashes, selectors right in tests. And isolation: order dependence, shared logins, changes to Configuration mid-test. I always give feedback with a ready replacement and turn recurring ones into linter rules.