← Valtive QA Quiz quiz.valtive.io/notes/selenium valtive.io
📖 Про цей гайд📖 About this guide
Для когоWho it's for
Для автоматизаторів на Java, які готуються до співбесіди на позицію QA Automation з Selenium 4, від junior до senior. Тут не визначення з підручника, а робочі відповіді: як команда доходить до браузера, як писати очікування без Thread.sleep, як жити з фреймами, вікнами і Shadow DOM, як масштабувати прогін на Grid і що робити з нестабільними тестами. Кожне з 50 питань квізу Selenium покрите щонайменше однією карткою.
For Java automation engineers preparing for a QA Automation interview with Selenium 4, from junior to senior. These are working answers rather than textbook definitions: how a command reaches the browser, how to write waits without Thread.sleep, how to handle frames, windows and Shadow DOM, how to scale a run on Grid and what to do with flaky tests. Each of the 50 questions in the Selenium quiz is covered by at least one card.
Як користуватисьHow to use it
Фільтруй питання за рівнем або розділом. Відкрий картку, прочитай відповідь, а код із неї запусти у своєму проєкті на JUnit 5 — більшість прикладів компілюється майже без змін. Потім закрий текст і перекажи відповідь уголос за блоком 🗣 Як читати вголос: на співбесіді оцінюють, як ти пояснюєш рішення своїми словами, а не те, чи впізнаєш ти правильний варіант.
Filter the questions by level or by section. Open a card, read the answer and run its code in your own JUnit 5 project — most examples compile with little or no change. Then hide the text and retell the answer out loud using the 🗣 How to say it out loud block: an interview judges how you explain a decision in your own words, not whether you recognise the right option.
Три якорі — з них випливає більшість відповідейThree anchors — most answers follow from them
ЯКІР 1 · Команда = HTTP-запитANCHOR 1 · A command is an HTTP request
Кожен виклик WebDriver — це запит W3C до драйвера. Звідси ціна команд, роль Grid як проксі, сесії, що можуть витікати, і BiDi як канал, яким браузер сам надсилає події.
Every WebDriver call is a W3C request to a driver. That explains the cost of commands, Grid acting as a proxy, sessions that can leak, and BiDi as the channel the browser uses to push events.
ЯКІР 2 · Чекай на стан, а не на часANCHOR 2 · Wait for state, not for time
Крім завантаження сторінки, Selenium не чекає на стан твого UI. Явне очікування на конкретну умову замість sleep і implicit wait — головний інструмент проти flaky-тестів і StaleElementReferenceException.
Apart from page load, Selenium does not wait for the state of your UI. An explicit wait on a specific condition, instead of sleep or implicit wait, is the main weapon against flaky tests and StaleElementReferenceException.
ЯКІР 3 · Одна сесія — один потікANCHOR 3 · One session per thread
Паралельний прогін тримається на ізоляції: окремий WebDriver у ThreadLocal, власні тестові дані, гарантований quit. Grid лише роздає слоти — порядок у тестах наводиш ти.
A parallel run rests on isolation: a separate WebDriver in a ThreadLocal, its own test data and a guaranteed quit. Grid only hands out slots; keeping the tests in order is your job.
51
питаньquestions
3
рівніlevels
—
voice-блокиvoice blocks
50/50
питань квізу покритоquiz questions covered
📚 9 РОЗДІЛІВ📚 9 SECTIONS
—
Junior
—
Middle
—
Senior
▶ 1 7
1 Що таке Selenium WebDriver і як команда з Java-коду доходить до браузера? What is Selenium WebDriver, and how does a command from Java code reach the browser?
АрхітектураArchitecture Junior ▶

Що це. WebDriver — це бібліотека клієнта (Java, Python, C# тощо) плюс стандартизований протокол W3C WebDriver, через який тест керує справжнім браузером. Між кодом і браузером стоїть окремий процес-драйвер: chromedriver, geckodriver, msedgedriver або safaridriver. Кожен виклик на кшталт driver.findElement(...) клієнт серіалізує в JSON і відправляє драйверу як HTTP-запит, а драйвер уже нативними засобами браузера виконує дію і повертає JSON-відповідь.

What it is. WebDriver is a client library (Java, Python, C# and others) plus the standard W3C WebDriver protocol that a test uses to drive a real browser. A separate driver process sits between your code and the browser: chromedriver, geckodriver, msedgedriver or safaridriver. Every call such as driver.findElement(...) is serialised to JSON and sent to the driver as an HTTP request, and the driver performs the action through the browser's native automation hooks and returns a JSON response.

Навіщо. Розуміння схеми «одна команда = один HTTP-запит» пояснює більшість практичних речей: чому кожен getText() коштує мережевого кола, чому Grid — це просто проксі для тих самих запитів, чому сесія має ідентифікатор і чому витік сесій забиває ноди. У Selenium 4 старий JSON Wire Protocol прибрали, тож усі браузери говорять одним W3C-діалектом.

Why it matters. Knowing that one command equals one HTTP request explains a lot of day-to-day behaviour: every getText() costs a network round trip, Grid is simply a proxy for those same requests, a session has an id, and leaked sessions block nodes. Selenium 4 dropped the old JSON Wire Protocol, so every browser now speaks the same W3C dialect.

Приклад. driver.findElement(By.id("login")) перетворюється на POST /session/{sessionId}/element з тілом {"using": "css selector", "value": "#login"} — Java-клієнт сам переписує By.id на CSS, бо W3C знає лише п’ять стратегій пошуку. У відповідь приходить посилання на елемент, яке далі йде в POST .../element/{id}/click.

Example. driver.findElement(By.id("login")) becomes POST /session/{sessionId}/element with the body {"using": "css selector", "value": "#login"}. The Java client rewrites By.id into CSS itself, because W3C defines only five location strategies. The response carries an element reference, which is then used in POST .../element/{id}/click.

Підводні камені. Класичний протокол односпрямований: тест питає — браузер відповідає, а сам браузер нічого не повідомляє. Тому логи консолі чи мережеві події доводилось витягувати обхідними шляхами. Це закриває WebDriver BiDi — двонаправлений протокол поверх WebSocket, де браузер сам надсилає події. У Selenium 4 він вмикається капабіліті webSocketUrl і поступово замінює Chrome-специфічний CDP.

Pitfalls. The classic protocol is one-way: the test asks and the browser answers, but the browser never reports anything on its own. That is why console logs and network events used to need workarounds. WebDriver BiDi fixes this with a bidirectional protocol over WebSocket where the browser pushes events. In Selenium 4 it is switched on with the webSocketUrl capability and is gradually replacing the Chrome-only CDP.

Уточнення. Часто питають, чи Selenium — це тестовий фреймворк. Ні: це інструмент автоматизації браузера, а запуск тестів, асерти і звіти дають JUnit 5 або TestNG разом із бібліотеками на кшталт AssertJ.

Follow-up. Interviewers often ask whether Selenium is a test framework. It is not: it automates the browser, while running tests, assertions and reports come from JUnit 5 or TestNG together with libraries like AssertJ.

Одна команда і те, що реально йде по мережі
One command and what actually goes over the wire
WebDriver driver = new ChromeDriver();       // старт chromedriver + POST /session
driver.get("https://shop.example.com/login"); // POST /session/{id}/url

WebElement login = driver.findElement(By.id("login"));
// POST /session/{id}/element
// {"using": "css selector", "value": "#login"}

login.click();                                 // POST /session/{id}/element/{elementId}/click
driver.quit();                                 // DELETE /session/{id}
WebDriver driver = new ChromeDriver();       // starts chromedriver + POST /session
driver.get("https://shop.example.com/login"); // POST /session/{id}/url

WebElement login = driver.findElement(By.id("login"));
// POST /session/{id}/element
// {"using": "css selector", "value": "#login"}

login.click();                                 // POST /session/{id}/element/{elementId}/click
driver.quit();                                 // DELETE /session/{id}
🔧 Розбір синтаксису: new ChromeDriver() запускає локальний процес драйвера і відкриває сесію запитом POST /session. WebElement — не копія DOM-вузла, а лише посилання з ідентифікатором, який драйвер видав. Коментарі праворуч показують HTTP-метод і шлях W3C, у який перетворюється кожен рядок; {id} — ідентифікатор сесії, {elementId} — ідентифікатор елемента.
🔧 Syntax breakdown: new ChromeDriver() starts a local driver process and opens a session with POST /session. A WebElement is not a copy of the DOM node, only a reference carrying an id the driver handed out. The comments on the right show the W3C HTTP method and path each line turns into; {id} is the session id and {elementId} is the element id.
💡 Простими словами: Тест — це клієнт, драйвер — сервер, браузер — виконавець. Кожен рядок коду — окремий запит до сервера.
💡 In plain words: The test is a client, the driver is a server and the browser does the work. Every line of code is a separate request to that server.
🗣 Як читати вголос Я пояснюю WebDriver як клієнт-серверну схему. Мій Java-код — клієнт, chromedriver чи geckodriver — сервер, який говорить протоколом W3C. Коли я викликаю findElement або click, бібліотека шле HTTP-запит з JSON, драйвер виконує дію в браузері і повертає відповідь. Звідси практичні висновки: кожна команда має ціну, Grid лише пересилає ці запити, а незакрита сесія займає слот. У Selenium 4 додався BiDi на WebSocket, і тепер браузер може сам надсилати мені події, наприклад логи консолі.
🗣 How to say it out loud I describe WebDriver as a client and server setup. My Java code is the client, chromedriver or geckodriver is the server that speaks the W3C protocol. When I call findElement or click, the library sends an HTTP request with a JSON body, the driver does the action in the browser and replies. That has practical consequences: every command has a cost, Grid only forwards those requests, and a session I never close keeps a slot busy. Selenium 4 added BiDi over WebSocket, so the browser can now push events to me, console logs for instance.
2 Що робить Selenium Manager і коли драйвер браузера все ж доводиться налаштовувати вручну? What does Selenium Manager do, and when do you still have to set up the browser driver yourself?
АрхітектураArchitecture Junior ▶

Що це. Selenium Manager — утиліта на Rust, яку вбудовано в Selenium починаючи з 4.6. Коли ти пишеш new ChromeDriver() і драйвера немає ні в PATH, ні в системній властивості webdriver.chrome.driver, клієнт сам викликає Selenium Manager. Той визначає версію встановленого браузера, завантажує сумісний chromedriver і кладе його в кеш ~/.cache/selenium. Новіші версії вміють ще й завантажити сам браузер Chrome for Testing, якщо його немає або якщо в опціях указано browserVersion.

What it is. Selenium Manager is a Rust utility bundled with Selenium since 4.6. When you write new ChromeDriver() and there is no driver on PATH or in the webdriver.chrome.driver system property, the client calls Selenium Manager on its own. It detects the installed browser version, downloads a matching chromedriver and stores it in the ~/.cache/selenium cache. Newer releases can also download the Chrome for Testing browser itself when it is missing or when the options set a browserVersion.

Навіщо. Раніше команди тримали драйвери в репозиторії або підключали сторонню бібліотеку WebDriverManager, і після автооновлення Chrome зранку падав увесь прогін. Тепер типовий локальний запуск і простий CI обходяться без жодного налаштування драйверів.

Why it matters. Teams used to keep drivers in the repository or pull in the third-party WebDriverManager library, and an overnight Chrome auto-update would break the whole morning run. Now a typical local run and a simple CI job need no driver setup at all.

Приклад. Новачок клонує проєкт, запускає тест — і все працює без кроку «завантаж chromedriver». А щоб зафіксувати версію для відтворюваності, достатньо options.setBrowserVersion("126"): менеджер підтягне саме цю збірку браузера і відповідний драйвер.

Example. A new joiner clones the project, runs a test, and it just works with no "download chromedriver" step. To pin a version for reproducible runs, options.setBrowserVersion("126") is enough: the manager fetches that exact browser build and the driver that goes with it.

Підводні камені. Менеджеру потрібен доступ в інтернет. У закритій корпоративній мережі без проксі-налаштувань він не зможе нічого завантажити, і тоді драйвер кладуть у образ агента заздалегідь та вказують шлях явно. Якщо драйвер уже є в PATH, Selenium візьме саме його, навіть застарілий, — звідси загадкові помилки несумісності версій. На Grid менеджер працює на ноді, а не в тестовому процесі, а в офіційних Docker-образах драйвер і браузер уже зібрані парою.

Pitfalls. The manager needs internet access. In a locked-down corporate network without proxy settings it cannot download anything, so the driver is baked into the agent image in advance and its path is set explicitly. If a driver is already on PATH, Selenium uses that one even when it is outdated, which is where puzzling version mismatch errors come from. On Grid the manager runs on the node rather than in the test process, and the official Docker images already ship a matched browser and driver pair.

Уточнення. Можуть спитати, чи потрібен ще WebDriverManager. Для нових проєктів на Selenium 4.6+ зазвичай ні; він лишається в старих кодових базах, і його прибирають під час міграції.

Follow-up. You may be asked whether WebDriverManager is still needed. For new projects on Selenium 4.6+ it usually is not; it survives in older code bases and gets removed during a migration.

Автоматичний драйвер, фіксована версія і явний шлях
Automatic driver, pinned version and an explicit path
// 1. Нічого не налаштовуємо: Selenium Manager знайде або завантажить chromedriver
WebDriver driver = new ChromeDriver();

// 2. Фіксуємо версію браузера для відтворюваних прогонів
ChromeOptions options = new ChromeOptions();
options.setBrowserVersion("126");
WebDriver pinned = new ChromeDriver(options);

// 3. Офлайн-агент: драйвер лежить в образі, шлях задаємо явно
ChromeDriverService service = new ChromeDriverService.Builder()
        .usingDriverExecutable(new File("/opt/drivers/chromedriver"))
        .build();
WebDriver offline = new ChromeDriver(service, new ChromeOptions());
// 1. No setup: Selenium Manager finds or downloads chromedriver
WebDriver driver = new ChromeDriver();

// 2. Pin the browser version for reproducible runs
ChromeOptions options = new ChromeOptions();
options.setBrowserVersion("126");
WebDriver pinned = new ChromeDriver(options);

// 3. Offline agent: the driver is in the image, the path is set explicitly
ChromeDriverService service = new ChromeDriverService.Builder()
        .usingDriverExecutable(new File("/opt/drivers/chromedriver"))
        .build();
WebDriver offline = new ChromeDriver(service, new ChromeOptions());
🔧 Розбір синтаксису: Конструктор ChromeDriver() без аргументів запускає пошук драйвера: спершу явний шлях, потім PATH, і лише тоді Selenium Manager. setBrowserVersion — стандартне W3C-капабіліті browserVersion. ChromeDriverService.Builder().usingDriverExecutable(...) вимикає автоматичний пошук і запускає саме вказаний файл.
🔧 Syntax breakdown: The no-argument ChromeDriver() constructor starts the driver lookup: an explicit path first, then PATH, and only then Selenium Manager. setBrowserVersion sets the standard W3C browserVersion capability. ChromeDriverService.Builder().usingDriverExecutable(...) switches the automatic lookup off and launches exactly the given file.
💡 Простими словами: Selenium Manager — це вбудований «завантажувач драйверів». Він рятує від ранкових падінь після оновлення Chrome, але хоче інтернет.
💡 In plain words: Selenium Manager is a built-in driver downloader. It saves you from the morning failures after a Chrome update, but it wants internet access.
🗣 Як читати вголос З версії 4.6 я просто пишу new ChromeDriver, і все. Якщо драйвера немає, Selenium Manager сам дивиться, яка версія Chrome стоїть, і качає відповідний chromedriver у кеш. Може навіть завантажити браузер потрібної версії, якщо я її зафіксую через setBrowserVersion. Ручне налаштування мені потрібне у двох випадках: закрита мережа без доступу назовні, де драйвер лежить в образі агента, і коли в PATH лежить старий драйвер, який перехоплює пошук. WebDriverManager у нових проєктах я вже не підключаю.
🗣 How to say it out loud Since version 4.6 I just write new ChromeDriver and that is it. If no driver is found, Selenium Manager checks which Chrome is installed and downloads the matching chromedriver into its cache. It can even fetch a specific browser build when I pin one with setBrowserVersion. I still set things up by hand in two cases: a closed network with no outbound access, where the driver lives in the agent image, and an old driver sitting on PATH that wins the lookup. I no longer add WebDriverManager to new projects.
3 Як налаштувати браузер через ChromeOptions і FirefoxOptions і чим опції відрізняються від capabilities? How do you configure a browser with ChromeOptions and FirefoxOptions, and how do options relate to capabilities?
АрхітектураArchitecture Junior ▶

Що це. Капабіліті — це JSON-набір побажань до сесії, який клієнт надсилає в POST /session. Частина ключів стандартна для W3C: browserName, browserVersion, platformName, pageLoadStrategy, acceptInsecureCerts, unhandledPromptBehavior. Решта — вендорські, з префіксом: goog:chromeOptions, moz:firefoxOptions. Класи ChromeOptions, FirefoxOptions, EdgeOptions — типобезпечні обгортки, які самі збирають цей JSON.

What it is. Capabilities are a JSON set of session requirements the client sends in POST /session. Some keys are standard W3C: browserName, browserVersion, platformName, pageLoadStrategy, acceptInsecureCerts, unhandledPromptBehavior. The rest are vendor keys with a prefix, such as goog:chromeOptions and moz:firefoxOptions. The ChromeOptions, FirefoxOptions and EdgeOptions classes are type-safe wrappers that build that JSON for you.

Навіщо. Через опції задають усе, що відрізняє тестовий браузер від звичайного: безголовий режим, розмір вікна, мову, теку завантажень, дозволи, самопідписані сертифікати стенду. На Grid ці ж капабіліті визначають, на яку ноду потрапить сесія.

Why it matters. Options hold everything that makes a test browser different from an ordinary one: headless mode, window size, language, download folder, permissions, the self-signed certificate of a test environment. On Grid the same capabilities decide which node the session lands on.

Приклад. Для CI-прогону в Chrome додаємо --headless=new і --window-size=1920,1080, через prefs вимикаємо діалог збереження файлів, а PageLoadStrategy.EAGER повертає керування після DOMContentLoaded, не чекаючи важких картинок. У Firefox те саме робиться через addPreference.

Example. For a CI run in Chrome we add --headless=new and --window-size=1920,1080, switch off the save-file dialog through prefs, and PageLoadStrategy.EAGER hands control back after DOMContentLoaded without waiting for heavy images. Firefox does the same through addPreference.

Підводні камені. Аргументи командного рядка специфічні для браузера: прапорець Chrome, переданий у Firefox, просто проігнорується, і ти будеш шукати баг не там. Копіювати з інтернету «магічні» прапорці на кшталт --no-sandbox без розуміння теж шкідливо: вони можуть приховати реальну проблему середовища. І ще: у Selenium 4 окремий клас DesiredCapabilities більше не потрібен — опції вже є капабіліті, а довільний ключ додають через setCapability.

Pitfalls. Command-line arguments are browser specific: a Chrome flag passed to Firefox is silently ignored, and you end up hunting the bug in the wrong place. Copying magic flags such as --no-sandbox from the internet without understanding them is also harmful, because they can hide a real environment problem. And in Selenium 4 you no longer need a separate DesiredCapabilities object: options already are capabilities, and any extra key goes in through setCapability.

Уточнення. Часто просять пояснити pageLoadStrategy: normal чекає на подію load, eager — лише на DOMContentLoaded, none повертається одразу, і тоді всі очікування лягають на тест.

Follow-up. You are often asked to explain pageLoadStrategy: normal waits for the load event, eager only for DOMContentLoaded, and none returns at once, which puts all the waiting on the test.

Chrome і Firefox для CI-прогону
Chrome and Firefox for a CI run
ChromeOptions chrome = new ChromeOptions();
chrome.addArguments("--headless=new", "--window-size=1920,1080", "--lang=uk");
chrome.setExperimentalOption("prefs", Map.of(
        "download.default_directory", "/tmp/downloads",
        "download.prompt_for_download", false));
chrome.setPageLoadStrategy(PageLoadStrategy.EAGER);   // не чекати на картинки
chrome.setAcceptInsecureCerts(true);                   // самопідписаний сертифікат стенду
WebDriver driver = new ChromeDriver(chrome);

FirefoxOptions firefox = new FirefoxOptions();
firefox.addArguments("-headless");
firefox.addPreference("intl.accept_languages", "uk");
firefox.setCapability("se:name", "checkout-smoke");    // довільний ключ, наприклад для Grid UI
ChromeOptions chrome = new ChromeOptions();
chrome.addArguments("--headless=new", "--window-size=1920,1080", "--lang=uk");
chrome.setExperimentalOption("prefs", Map.of(
        "download.default_directory", "/tmp/downloads",
        "download.prompt_for_download", false));
chrome.setPageLoadStrategy(PageLoadStrategy.EAGER);   // do not wait for images
chrome.setAcceptInsecureCerts(true);                   // self-signed test certificate
WebDriver driver = new ChromeDriver(chrome);

FirefoxOptions firefox = new FirefoxOptions();
firefox.addArguments("-headless");
firefox.addPreference("intl.accept_languages", "uk");
firefox.setCapability("se:name", "checkout-smoke");    // any extra key, e.g. for the Grid UI
🔧 Розбір синтаксису: addArguments додає прапорці запуску браузера. setExperimentalOption("prefs", Map.of(...)) пише внутрішні налаштування профілю Chrome. setPageLoadStrategy і setAcceptInsecureCerts — стандартні W3C-капабіліті. addPreference — аналог prefs для Firefox. setCapability кладе будь-який ключ, тут se:name, яким Grid підписує сесію.
🔧 Syntax breakdown: addArguments appends browser launch flags. setExperimentalOption("prefs", Map.of(...)) writes internal Chrome profile settings. setPageLoadStrategy and setAcceptInsecureCerts are standard W3C capabilities. addPreference is the Firefox counterpart of prefs. setCapability puts in any key, here se:name, which Grid uses to label the session.
💡 Простими словами: Капабіліті — це анкета, з якою тест приходить по браузер. Options — зручний бланк для цієї анкети під конкретний браузер.
💡 In plain words: Capabilities are the form a test fills in to ask for a browser. Options are a handy, browser-specific way to fill in that form.
🗣 Як читати вголос Я налаштовую браузер через класи Options, бо в Selenium 4 вони і є капабіліті. Стандартні речі на кшталт pageLoadStrategy чи acceptInsecureCerts однакові для всіх браузерів, а специфічне йде через аргументи і префи конкретного вендора. Для CI я зазвичай вмикаю новий headless, фіксую розмір вікна і теку завантажень. І я стежу, щоб прапорці Chrome не потрапляли у Firefox: там вони мовчки ігноруються, і тест поводиться не так, як я думаю.
🗣 How to say it out loud I configure the browser through the Options classes, because in Selenium 4 they are the capabilities. Standard things like pageLoadStrategy or acceptInsecureCerts look the same for every browser, while vendor details go through that browser's arguments and prefs. For CI I usually turn on the new headless mode, fix the window size and set a download folder. I also make sure Chrome flags do not leak into Firefox, where they are silently ignored and the test behaves differently from what I expect.
4 Чим quit() відрізняється від close(), і що з них викликати в @AfterEach? How is quit() different from close(), and which one belongs in @AfterEach?
АрхітектураArchitecture Junior ▶

Що це. close() закриває лише поточне вікно чи вкладку. quit() закриває всі вікна, видаляє сесію запитом DELETE /session/{id} і зупиняє локальний процес драйвера. Після close() останнього вікна сесія за W3C теж завершується, але процес chromedriver лишається жити, а будь-яка наступна команда кидає NoSuchSessionException.

What it is. close() closes only the current window or tab. quit() closes every window, deletes the session with DELETE /session/{id} and stops the local driver process. Under W3C the session also ends after close() on the last window, but the chromedriver process stays alive and any further command throws NoSuchSessionException.

Навіщо. Прибирання після тесту — питання ресурсів. Кожен незакритий браузер з’їдає сотні мегабайт пам’яті на агенті, а на Grid незавершена сесія займає слот ноди, доки її не прибере таймаут простою (типово 300 секунд). Сотня тестів без quit() — і черга Grid стоїть, хоча тести вже давно завершились.

Why it matters. Cleaning up after a test is about resources. Every browser left open eats hundreds of megabytes on the agent, and on Grid an unfinished session holds a node slot until the idle timeout removes it (300 seconds by default). A hundred tests without quit() and the Grid queue stalls even though the tests finished long ago.

Приклад. У тесті відкрили вкладку з договором: закриваємо її через close() і повертаємося у головне вікно за збереженим handle. А в @AfterEach викликаємо quit() з перевіркою на null, бо створення драйвера в @BeforeEach теж може впасти.

Example. A test opens a tab with a contract: we close that tab with close() and switch back to the main window using the saved handle. In @AfterEach we call quit() behind a null check, because creating the driver in @BeforeEach can fail too.

Підводні камені. Якщо quit() стоїть в останньому рядку самого тесту, то при падінні асерта він не виконається — тому прибирання живе лише в @AfterEach або в finally. Друга пастка — close() у @AfterEach «для акуратності»: на машині розробника це непомітно, а на агенті CI за день накопичуються десятки зомбі-процесів chromedriver.

Pitfalls. If quit() is the last line of the test itself, a failed assertion skips it, so cleanup belongs in @AfterEach or a finally block. The second trap is calling close() in @AfterEach to be tidy: nobody notices on a laptop, but a CI agent collects dozens of zombie chromedriver processes in a day.

Уточнення. Можуть спитати про @AfterAll. Він годиться, лише якщо браузер свідомо спільний для класу; тоді треба пам’ятати про очищення cookies між тестами, інакше стан одного тесту протече в інший.

Follow-up. You may be asked about @AfterAll. It fits only when the browser is deliberately shared by the whole class, and then you must clear cookies between tests or one test's state leaks into the next.

Закрити вкладку в тесті і прибрати сесію після нього
Close a tab inside a test and clean up the session after it
class ContractTest {
    private WebDriver driver;

    @BeforeEach
    void setUp() {
        driver = new ChromeDriver();
    }

    @Test
    void contractOpensInNewTab() {
        driver.get("https://shop.example.com/checkout");
        String main = driver.getWindowHandle();
        driver.findElement(By.linkText("Договір оферти")).click();
        // ... перейти у нову вкладку і перевірити її ...
        driver.close();                     // закрити тільки вкладку з договором
        driver.switchTo().window(main);     // повернутись, інакше наступна команда впаде
    }

    @AfterEach
    void tearDown() {
        if (driver != null) {
            driver.quit();                  // усі вікна + DELETE /session + стоп chromedriver
        }
    }
}
class ContractTest {
    private WebDriver driver;

    @BeforeEach
    void setUp() {
        driver = new ChromeDriver();
    }

    @Test
    void contractOpensInNewTab() {
        driver.get("https://shop.example.com/checkout");
        String main = driver.getWindowHandle();
        driver.findElement(By.linkText("Terms of sale")).click();
        // ... switch to the new tab and check it ...
        driver.close();                     // close only the contract tab
        driver.switchTo().window(main);     // switch back or the next command fails
    }

    @AfterEach
    void tearDown() {
        if (driver != null) {
            driver.quit();                  // all windows + DELETE /session + stop chromedriver
        }
    }
}
🔧 Розбір синтаксису: @BeforeEach і @AfterEach — анотації JUnit 5, що виконуються до і після кожного @Test, причому @AfterEach спрацьовує навіть після падіння тесту. getWindowHandle() повертає рядковий ідентифікатор поточного вікна. switchTo().window(handle) переводить фокус драйвера назад. Перевірка driver != null захищає від NullPointerException, якщо браузер не стартував.
🔧 Syntax breakdown: @BeforeEach and @AfterEach are JUnit 5 annotations that run before and after every @Test, and @AfterEach runs even when the test fails. getWindowHandle() returns the string id of the current window. switchTo().window(handle) moves the driver's focus back. The driver != null check prevents a NullPointerException when the browser never started.
💡 Простими словами: close() — закрити одну вкладку, quit() — вимкнути весь браузер і звільнити місце для наступного тесту.
💡 In plain words: close() shuts one tab; quit() switches off the whole browser and frees the seat for the next test.
🗣 Як читати вголос Для мене різниця проста: close закриває одне вікно, quit завершує всю сесію і зупиняє драйвер. Тому close я використовую всередині тесту, коли треба прибрати зайву вкладку, а quit — завжди в AfterEach з перевіркою на null. Якщо поставити quit в кінець тесту, при першому ж падінні асерта браузер лишиться висіти. На Grid це особливо боляче: сесія займає слот, поки її не прибере таймаут, і інші тести чекають у черзі без жодної причини.
🗣 How to say it out loud For me the difference is simple: close shuts one window, quit ends the whole session and stops the driver. So I use close inside a test when I need to get rid of an extra tab, and quit always goes into AfterEach behind a null check. If quit sits at the end of the test, the first failed assertion leaves the browser hanging. On Grid that hurts most: the session keeps its slot until a timeout cleans it up, and other tests wait in the queue for no reason.
5 Коли запускати тести в headless-режимі і чому падіння з CI варто відтворювати з видимим браузером? When should tests run headless, and why should a CI failure be reproduced in a visible browser?
АрхітектураArchitecture Middle ▶

Що це. Headless — режим, у якому браузер рендерить сторінку повністю, але не показує вікно. У Chrome сучасний варіант вмикається прапорцем --headless=new: це той самий браузер, що й звичайний, а не окрема спрощена збірка, як було раніше. У Firefox прапорець — -headless. Метод setHeadless(true) із Selenium 3 прибрали, тепер лише аргументи.

What it is. Headless is a mode in which the browser renders the page fully but shows no window. In Chrome the modern version is switched on with --headless=new: it is the same browser as the regular one rather than a separate stripped-down build, as it used to be. In Firefox the flag is -headless. The setHeadless(true) method from Selenium 3 is gone; only arguments remain.

Навіщо. На CI-агентах і в контейнерах немає дисплея, а headless не потребує X-сервера чи Xvfb, стартує швидше і споживає менше ресурсів. Для щоденних регресійних прогонів це розумний вибір за замовчуванням.

Why it matters. CI agents and containers have no display, and headless needs no X server or Xvfb, starts faster and uses fewer resources. For daily regression runs it is a sensible default.

Приклад. Тест кошика стабільно зелений локально і падає на CI з ElementClickInterceptedException. Причина — розмір вікна: headless Chrome за замовчуванням стартує з вікном 800×600, сайт перемикається на мобільну верстку, і кнопку перекриває липкий банер. Фікс — явне --window-size=1920,1080 в обох режимах, а не збільшення таймауту.

Example. The cart test is reliably green locally and fails on CI with ElementClickInterceptedException. The cause is the window size: headless Chrome starts with an 800×600 window by default, the site switches to its mobile layout and a sticky banner covers the button. The fix is an explicit --window-size=1920,1080 in both modes, not a longer timeout.

Підводні камені. Headless змінює не лише наявність вікна: інший розмір екрана, інколи інший user agent, відсутність фокуса вікна, свої особливості з буфером обміну і діалогами друку. Деякі антибот-захисти теж впізнають такий браузер. Тому підозріле падіння спершу відтворюють із видимим вікном і тим самим розміром — так видно, що саме бачив тест, а не що ти про це думаєш.

Pitfalls. Headless changes more than the presence of a window: a different screen size, sometimes a different user agent, no window focus, and its own quirks with the clipboard and print dialogs. Some anti-bot protections recognise it as well. So a suspicious failure is first reproduced with a visible window at the same size, which shows what the test actually saw rather than what you assume it saw.

Уточнення. Можуть спитати, чи headless швидший. Трохи — на запуску і ресурсах, але більшість часу тесту все одно йде на мережу й очікування, тому вибір роблять заради середовища, а не заради швидкості.

Follow-up. You may be asked whether headless is faster. Slightly, at start-up and on resources, but most of a test's time goes to the network and to waiting, so the choice is made for the environment rather than for speed.

Один перемикач для CI і локального налагодження
One switch for CI and local debugging
static ChromeOptions chromeOptions() {
    ChromeOptions options = new ChromeOptions();
    // однаковий розмір вікна в обох режимах, інакше верстка різна
    options.addArguments("--window-size=1920,1080");
    boolean headless = Boolean.parseBoolean(System.getProperty("headless", "true"));
    if (headless) {
        options.addArguments("--headless=new");
    }
    return options;
}

// локально відтворюємо падіння з CI:
// mvn test -Dtest=CartTest -Dheadless=false
static ChromeOptions chromeOptions() {
    ChromeOptions options = new ChromeOptions();
    // same window size in both modes, otherwise the layout differs
    options.addArguments("--window-size=1920,1080");
    boolean headless = Boolean.parseBoolean(System.getProperty("headless", "true"));
    if (headless) {
        options.addArguments("--headless=new");
    }
    return options;
}

// reproduce a CI failure locally:
// mvn test -Dtest=CartTest -Dheadless=false
🔧 Розбір синтаксису: System.getProperty("headless", "true") читає JVM-властивість, передану як -Dheadless=false, а друге значення — дефолт. Boolean.parseBoolean перетворює рядок на boolean. Прапорець --headless=new додається лише за потреби, а розмір вікна — завжди, тож обидва режими бачать однакову верстку.
🔧 Syntax breakdown: System.getProperty("headless", "true") reads a JVM property passed as -Dheadless=false, and the second value is the default. Boolean.parseBoolean turns the string into a boolean. The --headless=new flag is added only when needed, while the window size is always set, so both modes see the same layout.
💡 Простими словами: Headless — той самий браузер без вікна. Коли тест падає лише там, спершу перевір розмір вікна, а потім дивись очима.
💡 In plain words: Headless is the same browser without a window. When a test fails only there, check the window size first, then look with your own eyes.
🗣 Як читати вголос У CI я запускаю тести в headless, бо там немає дисплея, а новий headless у Chrome — це повноцінний браузер. Але я завжди задаю розмір вікна явно, бо за замовчуванням він маленький, і сайт перемикається на мобільну верстку. Найчастіше саме через це тест зелений локально і червоний на CI. Коли бачу таке падіння, я запускаю той самий тест із видимим вікном через системну властивість і дивлюся, що реально відбувається на екрані, а не підкручую таймаути навмання.
🗣 How to say it out loud On CI I run tests headless, because there is no display there, and the new Chrome headless is a complete browser. But I always set the window size explicitly, since the default one is small and the site flips to its mobile layout. That is the most common reason a test is green locally and red on CI. When I see such a failure, I rerun the same test with a visible window through a system property and watch what really happens on the screen instead of blindly raising timeouts.
6 Чи можна Selenium тестувати нативний мобільний застосунок, і що брати замість нього? Can Selenium test a native mobile app, and what do you use instead?
АрхітектураArchitecture Junior ▶

Що це. Selenium автоматизує лише браузери. Нативний застосунок на Android чи iOS — це не DOM, а дерево нативних віджетів, і жоден браузерний драйвер його не бачить. Для цього існує Appium: окремий сервер, який реалізує той самий протокол W3C WebDriver, але під капотом керує пристроєм через UiAutomator2 або Espresso на Android і XCUITest на iOS.

What it is. Selenium automates browsers only. A native Android or iOS app is not a DOM but a tree of native widgets, and no browser driver can see it. That is what Appium is for: a separate server that implements the same W3C WebDriver protocol but drives the device underneath through UiAutomator2 or Espresso on Android and XCUITest on iOS.

Навіщо це знати. Питання перевіряє, чи розумієш ти межі інструмента і не пообіцяєш на проєкті «покрити мобілку Селеніумом». Водночас спільний протокол означає, що досвід переноситься: Java-клієнт Appium побудований поверх класів Selenium, тож findElement, явні очікування і Page Object працюють так само.

Why it matters. The question checks that you know the tool's limits and will not promise to cover mobile with Selenium on a project. At the same time the shared protocol means the skills carry over: the Appium Java client is built on top of Selenium classes, so findElement, explicit waits and Page Object work the same way.

Приклад. Мобільну версію сайту можна частково перевірити і в Selenium: Chrome уміє емулювати екран телефона через опцію mobileEmulation. Але це лише інший розмір вікна та user agent у десктопному браузері. Нативний екран оплати в застосунку магазину тестують вже через AndroidDriver з Appium і локатори на кшталт AppiumBy.accessibilityId.

Example. The mobile version of a website can be partly checked in Selenium: Chrome can emulate a phone screen through the mobileEmulation option. But that is only a different window size and user agent in a desktop browser. The native payment screen in the shop app is tested with Appium's AndroidDriver and locators such as AppiumBy.accessibilityId.

Підводні камені. Емуляція в Chrome не ловить проблем справжніх пристроїв: жестів, клавіатури, яка перекриває поле, повільної мережі, особливостей Safari на iOS. Інша крайність — тягнути Appium туди, де вистачить адаптивної перевірки сайту: мобільна ферма коштує дорожче й працює повільніше, ніж Grid з браузерами.

Pitfalls. Chrome emulation misses the problems of real devices: gestures, a keyboard covering the input, a slow network, Safari quirks on iOS. The opposite mistake is dragging Appium into places where a responsive check of the site is enough: a device farm costs more and runs slower than a Grid of browsers.

Уточнення. Можуть спитати про гібридні застосунки з WebView. Appium уміє перемикати контекст з NATIVE_APP у WebView, і всередині вже діють звичні веб-локатори.

Follow-up. You may be asked about hybrid apps with a WebView. Appium can switch the context from NATIVE_APP into the WebView, and inside it the usual web locators apply.

Мобільна верстка в Chrome і нативний екран в Appium
Mobile layout in Chrome and a native screen in Appium
// 1. Selenium: адаптивна верстка сайту в емуляції телефона
ChromeOptions chrome = new ChromeOptions();
chrome.setExperimentalOption("mobileEmulation", Map.of(
        "deviceMetrics", Map.of("width", 390, "height", 844, "pixelRatio", 3.0)));
WebDriver web = new ChromeDriver(chrome);
web.get("https://shop.example.com");

// 2. Appium: нативний застосунок на Android
UiAutomator2Options caps = new UiAutomator2Options()
        .setDeviceName("Pixel_7")
        .setApp("/builds/shop.apk");
AndroidDriver app = new AndroidDriver(new URL("http://127.0.0.1:4723"), caps);
app.findElement(AppiumBy.accessibilityId("pay-button")).click();
// 1. Selenium: the site's responsive layout in phone emulation
ChromeOptions chrome = new ChromeOptions();
chrome.setExperimentalOption("mobileEmulation", Map.of(
        "deviceMetrics", Map.of("width", 390, "height", 844, "pixelRatio", 3.0)));
WebDriver web = new ChromeDriver(chrome);
web.get("https://shop.example.com");

// 2. Appium: a native Android app
UiAutomator2Options caps = new UiAutomator2Options()
        .setDeviceName("Pixel_7")
        .setApp("/builds/shop.apk");
AndroidDriver app = new AndroidDriver(new URL("http://127.0.0.1:4723"), caps);
app.findElement(AppiumBy.accessibilityId("pay-button")).click();
🔧 Розбір синтаксису: mobileEmulation з deviceMetrics задає ширину, висоту і щільність пікселів емульованого екрана. UiAutomator2Options з бібліотеки Appium java-client — типобезпечні капабіліті для Android. AndroidDriver підключається до сервера Appium 2 за адресою http://127.0.0.1:4723. AppiumBy.accessibilityId шукає за content-description елемента.
🔧 Syntax breakdown: mobileEmulation with deviceMetrics sets the width, height and pixel ratio of the emulated screen. UiAutomator2Options from the Appium java-client library are type-safe Android capabilities. AndroidDriver connects to an Appium 2 server at http://127.0.0.1:4723. AppiumBy.accessibilityId looks an element up by its content description.
💡 Простими словами: Selenium — для всього, що відкривається в браузері. Усе, що встановлюється з магазину застосунків, — до Appium.
💡 In plain words: Selenium is for anything that opens in a browser. Anything installed from an app store goes to Appium.
🗣 Як читати вголос Нативний застосунок Selenium не бачить, бо там немає DOM, лише нативні віджети. Для цього я беру Appium: він говорить тим самим протоколом WebDriver, тому мої звички з Selenium переносяться майже без змін. Мобільну верстку сайту я можу глянути й у Selenium через емуляцію в Chrome, але чесно кажу, що це лише розмір екрана і user agent, а не справжній пристрій. Тож для критичних мобільних сценаріїв потрібні реальні девайси або хмарна ферма.
🗣 How to say it out loud Selenium cannot see a native app, because there is no DOM there, only native widgets. For that I use Appium: it speaks the same WebDriver protocol, so my Selenium habits carry over almost unchanged. I can look at a site's mobile layout in Selenium through Chrome emulation, but I am honest that it is just a screen size and a user agent, not a real device. So critical mobile scenarios need real devices or a cloud device farm.
7 Як ви побудуєте кросбраузерне тестування на Selenium, щоб не множити час прогону на кількість браузерів? How would you set up cross-browser testing with Selenium without multiplying run time by the number of browsers?
АрхітектураArchitecture Senior ▶

Що це. Кросбраузерне тестування перевіряє, що критичні сценарії працюють у всіх браузерах, які підтримує продукт. Selenium тут сильний завдяки W3C-стандарту: той самий тест запускається в Chrome, Firefox, Edge і Safari лише зміною опцій.

What it is. Cross-browser testing checks that critical scenarios work in every browser the product supports. Selenium is strong here thanks to the W3C standard: the same test runs in Chrome, Firefox, Edge and Safari by changing only the options.

Навіщо думати про стратегію. Наївний підхід — прогнати всі 800 тестів у чотирьох браузерах — множить час і вартість Grid учетверо, а знаходить здебільшого ті самі баги, що й один браузер. Більшість дефектів — у логіці застосунку, а не в рушії браузера.

Why a strategy matters. The naive approach of running all 800 tests in four browsers multiplies time and Grid cost by four, and mostly finds the same bugs a single browser would. Most defects live in the application logic, not in the browser engine.

Приклад. Беремо аналітику: 68% Chrome, 17% Safari, 9% Edge, 5% Firefox. Повна регресія йде в Chrome на кожен пул-реквест. Смоук із 40 критичних сценаріїв (логін, пошук, кошик, оплата) щоночі проходить матрицею Firefox, Edge і Safari. Браузер вибирається системною властивістю, а матрицю паралельно розгортає CI, тож нічний прогін займає стільки ж часу, скільки найдовша гілка.

Example. Start from analytics: 68% Chrome, 17% Safari, 9% Edge, 5% Firefox. The full regression runs in Chrome on every pull request. A smoke set of 40 critical scenarios (login, search, cart, payment) runs every night as a Firefox, Edge and Safari matrix. The browser is picked through a system property and CI fans the matrix out in parallel, so the nightly run takes as long as its slowest branch.

Підводні камені. Safari запускається лише на macOS через вбудований safaridriver і не живе в Linux-контейнерах, тож потрібен Mac-агент або хмарний провайдер. Браузерні відмінності ховаються в конкретних місцях: нативні date-поля, завантаження файлів, drag and drop, шрифти, політики cookies. Ці зони варто покривати свідомо, а піксельні розбіжності віддати візуальним тестам, а не асертам у Selenium.

Pitfalls. Safari runs only on macOS through the built-in safaridriver and does not live in Linux containers, so you need a Mac agent or a cloud provider. Browser differences hide in specific places: native date inputs, file downloads, drag and drop, fonts, cookie policies. Those areas deserve deliberate coverage, while pixel differences belong to visual tests rather than to Selenium assertions.

Уточнення. Спитають, як обирати версії. Зазвичай поточна стабільна і попередня мажорна; стару версію закріплюють через browserVersion, щоб падіння можна було відтворити.

Follow-up. You will be asked how to pick versions. Usually the current stable and the previous major; an older version is pinned with browserVersion so that a failure can be reproduced.

Браузер із системної властивості, тести з тегами
Browser from a system property, tests with tags
static WebDriver createDriver() {
    String browser = System.getProperty("browser", "chrome");
    return switch (browser) {
        case "firefox" -> new FirefoxDriver(new FirefoxOptions());
        case "edge"    -> new EdgeDriver(new EdgeOptions());
        case "safari"  -> new SafariDriver(new SafariOptions()); // лише macOS
        default        -> new ChromeDriver(new ChromeOptions());
    };
}

@Tag("cross-browser")   // потрапляє в нічну матрицю
@Test
void guestCanPayForCart() {
    // ... критичний сценарій ...
}

// CI: mvn test -Dgroups=cross-browser -Dbrowser=firefox
static WebDriver createDriver() {
    String browser = System.getProperty("browser", "chrome");
    return switch (browser) {
        case "firefox" -> new FirefoxDriver(new FirefoxOptions());
        case "edge"    -> new EdgeDriver(new EdgeOptions());
        case "safari"  -> new SafariDriver(new SafariOptions()); // macOS only
        default        -> new ChromeDriver(new ChromeOptions());
    };
}

@Tag("cross-browser")   // goes into the nightly matrix
@Test
void guestCanPayForCart() {
    // ... critical scenario ...
}

// CI: mvn test -Dgroups=cross-browser -Dbrowser=firefox
🔧 Розбір синтаксису: Вираз switch зі стрілками (Java 14+) повертає значення без break. Кожна гілка створює драйвер зі своїм класом опцій. @Tag з JUnit 5 позначає тест, а параметр Maven Surefire -Dgroups=cross-browser запускає лише позначені. -Dbrowser=firefox потрапляє в System.getProperty.
🔧 Syntax breakdown: The arrow-form switch expression (Java 14+) returns a value without break. Each branch builds a driver with its own options class. JUnit 5 @Tag labels a test, and the Maven Surefire parameter -Dgroups=cross-browser runs only the labelled ones. -Dbrowser=firefox ends up in System.getProperty.
💡 Простими словами: Не кожен тест у кожному браузері, а кожен критичний сценарій у кожному важливому браузері.
💡 In plain words: Not every test in every browser, but every critical scenario in every browser that matters.
🗣 Як читати вголос Я не множу весь набір на всі браузери. Спершу дивлюся в аналітику, які браузери реально приносять користувачів. Повна регресія в мене йде в одному основному браузері на кожен пул-реквест, а невеликий набір критичних сценаріїв щоночі проходить матрицею інших браузерів паралельно в CI. Код тесту не змінюється, браузер приходить із системної властивості. Окремо я пам’ятаю про Safari: йому потрібен Mac або хмара, і це треба закласти в інфраструктуру заздалегідь.
🗣 How to say it out loud I do not multiply the whole suite by every browser. First I look at analytics to see which browsers actually bring users. My full regression runs in one main browser on every pull request, and a small set of critical scenarios goes through a matrix of the other browsers every night, in parallel on CI. The test code stays the same and the browser comes from a system property. I also keep Safari in mind: it needs a Mac or a cloud service, and that has to be planned into the infrastructure up front.
▶ 2 5
8 Який локатор вважається стабільним і як обрати стратегію By для конкретного елемента? What makes a locator stable, and how do you pick a By strategy for a given element?
ЛокаториLocators Junior ▶

Що це. Стабільний локатор спирається на контракт між тестом і розробкою, а не на випадкову форму DOM. Такий контракт — це атрибут data-testid, унікальний і незмінний id, атрибут name поля форми або доступні атрибути на кшталт aria-label. Позиція елемента в дереві, згенеровані класи CSS-модулів і текст, який перекладають, контрактом не є.

What it is. A stable locator relies on a contract between the tests and the developers, not on the accidental shape of the DOM. That contract is a data-testid attribute, a unique and fixed id, the name of a form field, or accessibility attributes such as aria-label. The element's position in the tree, class names generated by CSS modules and translated text are not a contract.

Навіщо. Більшість «падінь без багу» в UI-наборах — це локатори, що зламалися після рефакторингу верстки. Коли локатор описує призначення елемента, дизайнер може переставити блоки, змінити стилі чи обгорнути кнопку в новий div, і тест це переживе. Порядок вибору, який я пропоную команді: test id → стабільний id або name → доступні атрибути → короткий CSS за змістовними атрибутами → XPath лише коли без нього ніяк.

Why it matters. Most UI failures that are not real bugs come from locators that broke after a markup refactor. When a locator describes what the element is for, a designer can move blocks around, restyle them or wrap a button in a new div, and the test survives. The order I suggest to a team: test id, then a stable id or name, then accessibility attributes, then a short CSS selector on meaningful attributes, and XPath only when nothing else works.

Приклад. Скопійований з DevTools шлях /html/body/div[3]/div[2]/form/button[1] ламається, щойно над формою з’являється банер. Натомість By.cssSelector("[data-testid=checkout-submit]") описує саму кнопку оформлення замовлення і не залежить від сусідів. Для полів форми часто досить By.name("email"), бо name потрібен бекенду і змінюється рідко.

Example. A path copied from DevTools such as /html/body/div[3]/div[2]/form/button[1] breaks the moment a banner appears above the form. By.cssSelector("[data-testid=checkout-submit]") instead points at the checkout button itself and does not care about its neighbours. For form fields By.name("email") is often enough, because the backend needs the name and it rarely changes.

Підводні камені. Класи на кшталт .btn_x7f3a генерує збирач і вони змінюються з кожним білдом. Текст кнопки ламається при локалізації і A/B-тестах. Динамічні id типу ember1234 виглядають стабільними, але залежать від порядку рендеру. Ще одна пастка — локатор, що знаходить кілька елементів: findElement мовчки поверне перший, і тест клікне не туди.

Pitfalls. Classes like .btn_x7f3a are produced by the bundler and change with every build. Button text breaks with localisation and A/B experiments. Dynamic ids such as ember1234 look stable but depend on render order. Another trap is a locator that matches several elements: findElement silently returns the first one, and the test clicks the wrong thing.

Уточнення. На питання «а якщо розробники не додають test id?» відповідаю, що це домовленість на рівні команди: тестувальник робить PR з атрибутами сам або вносить їх у Definition of Done для нових компонентів. Локатори зберігаю в одному місці — Page Object, — щоб правка була в одному рядку.

Follow-up. If asked what to do when developers do not add test ids, I say it is a team agreement: the tester opens a PR with the attributes, or the attributes go into the Definition of Done for new components. I keep locators in one place, a Page Object, so a fix is a one-line change.

Крихкі й стабільні локатори для тієї самої кнопки
Brittle and stable locators for the same button
// Крихко: залежить від структури сторінки і збірки
By absolute = By.xpath("/html/body/div[3]/div[2]/form/button[1]");
By generated = By.cssSelector(".btn_x7f3a");
By byText = By.xpath("//button[text()='Оформити']");

// Стабільно: описує призначення елемента
By submit = By.cssSelector("[data-testid='checkout-submit']");
By email = By.name("email");
By search = By.cssSelector("input[aria-label='Search products']");

@Test
void checkoutButtonIsUnique() {
    // Контракт: рівно один елемент на сторінці
    assertEquals(1, driver.findElements(submit).size());
}
// Brittle: depends on page structure and the build
By absolute = By.xpath("/html/body/div[3]/div[2]/form/button[1]");
By generated = By.cssSelector(".btn_x7f3a");
By byText = By.xpath("//button[text()='Checkout']");

// Stable: describes what the element is for
By submit = By.cssSelector("[data-testid='checkout-submit']");
By email = By.name("email");
By search = By.cssSelector("input[aria-label='Search products']");

@Test
void checkoutButtonIsUnique() {
    // Contract: exactly one element on the page
    assertEquals(1, driver.findElements(submit).size());
}
🔧 Розбір синтаксису: By — об’єкт-опис пошуку, сам по собі він нічого не шукає. By.cssSelector("[data-testid='...']") — пошук за атрибутом у квадратних дужках, By.name — за атрибутом name. У тесті findElements(...).size() перевіряє, що локатор унікальний, бо findElements повертає список усіх збігів.
🔧 Syntax breakdown: By is a description of a search and finds nothing by itself. By.cssSelector("[data-testid='...']") matches by attribute in square brackets, and By.name matches the name attribute. The test uses findElements(...).size() to prove the locator is unique, because findElements returns a list of every match.
💡 Простими словами: Добрий локатор відповідає на питання «що це за елемент», а не «де він лежить».
💡 In plain words: A good locator answers "what is this element", not "where does it sit".
🗣 Як читати вголос Для мене стабільний локатор — це домовленість з розробниками. Найкраще працює окремий атрибут data-testid, далі стабільний id або name поля, потім aria-атрибути. Абсолютні XPath з DevTools, згенеровані класи і текст кнопок я не використовую, бо вони ламаються від будь-якого редизайну чи перекладу. Якщо test id немає, я сам додаю їх у PR або домовляюсь, що це частина Definition of Done. І я слідкую, щоб локатор знаходив рівно один елемент, інакше тест тихо клацне не по тому.
🗣 How to say it out loud To me a stable locator is an agreement with the developers. A dedicated data-testid attribute works best, then a stable id or the name of a field, then aria attributes. I avoid absolute XPath copied from DevTools, generated classes and button text, because any redesign or translation breaks them. When there are no test ids I add them in a PR myself or agree that they are part of the Definition of Done. I also make sure a locator matches exactly one element, otherwise the test quietly clicks the wrong one.
9 Коли в Selenium краще CSS-селектор, а коли без XPath не обійтись? When is a CSS selector the better choice in Selenium, and when can you not do without XPath?
ЛокаториLocators Junior ▶

Що це. Обидві мови описують шлях до вузла DOM, але по-різному. CSS — мова селекторів зі стилів: атрибути, класи, нащадки, сусіди, псевдокласи на кшталт :nth-child. XPath — мова запитів до дерева XML, яка вміє рухатися вгору до батька, фільтрувати за текстом і будувати умови з and та or.

What it is. Both languages describe a path to a DOM node, but in different ways. CSS is the selector language from stylesheets: attributes, classes, descendants, siblings and pseudo-classes like :nth-child. XPath is a query language for XML trees that can move up to a parent, filter by text and combine conditions with and and or.

Навіщо. CSS зазвичай коротший, його легше читати, і браузер обчислює його тим самим рушієм, що й стилі. Тому за замовчуванням я пишу CSS. XPath лишаю для задач, яких CSS не вміє: знайти елемент за видимим текстом, піднятися від знайденого елемента до предка або вибрати рядок таблиці за вмістом однієї з клітинок.

Why it matters. CSS is usually shorter and easier to read, and the browser evaluates it with the same engine it uses for styles. So CSS is my default. I keep XPath for jobs CSS cannot do: finding an element by its visible text, climbing from a found element to an ancestor, or picking a table row by what one of its cells contains.

Приклад. Кнопка видалення в рядку таблиці, де в першій клітинці ім’я користувача «Olena»: //tr[td[normalize-space()='Olena']]//button[@data-action='delete']. У CSS немає пошуку за текстом і немає переходу до батька, тож тут XPath чесно виграє. А поле пошуку input[name=q] на XPath виглядало б як //input[@name='q'] — довше і без жодної вигоди.

Example. The delete button in the table row whose first cell holds the user name Olena: //tr[td[normalize-space()='Olena']]//button[@data-action='delete']. CSS has no text matching and no step up to a parent, so XPath honestly wins here. The search field input[name=q], on the other hand, would be //input[@name='q'] in XPath, which is longer and gains nothing.

Підводні камені. Міф про те, що XPath завжди повільний, давно не актуальний: різниця в сучасних браузерах мізерна порівняно з мережевим колом кожної команди. Справжня проблема — крихкість: довгі XPath з індексами div[4] ламаються так само, як абсолютні шляхи. Ще одна пастка: text() не бачить текст вкладених тегів, тому надійніше normalize-space() або contains(., ...).

Pitfalls. The claim that XPath is always slow is outdated: in modern browsers the difference is tiny next to the network round trip every command already pays. The real problem is brittleness, since long XPath full of indexes like div[4] breaks just like absolute paths do. Another trap: text() ignores the text of nested tags, so normalize-space() or contains(., ...) is safer.

Уточнення. Часто просять пояснити // проти /: подвійна риска шукає нащадка на будь-якій глибині, одинарна — лише прямого нащадка. У CSS аналоги — пробіл і >.

Follow-up. You may be asked about // versus /: the double slash finds a descendant at any depth, the single slash only a direct child. In CSS the equivalents are a space and >.

Та сама сторінка: де CSS, а де XPath
The same page: where CSS fits and where XPath does
// CSS: коротко і читабельно для атрибутів та ієрархії
By searchBox = By.cssSelector("form#search > input[name='q']");
By firstResult = By.cssSelector("ul.results li:first-child a");

// XPath: пошук за текстом і перехід до предка
By deleteOlena = By.xpath(
    "//tr[td[normalize-space()='Olena']]//button[@data-action='delete']");
By labelledInput = By.xpath(
    "//label[normalize-space()='Email']/following-sibling::input");

@Test
void deletesUserByName() {
    driver.findElement(deleteOlena).click();
    // Рядок з Olena зник із таблиці
    assertTrue(driver.findElements(deleteOlena).isEmpty());
}
// CSS: short and readable for attributes and hierarchy
By searchBox = By.cssSelector("form#search > input[name='q']");
By firstResult = By.cssSelector("ul.results li:first-child a");

// XPath: matching by text and climbing to an ancestor
By deleteOlena = By.xpath(
    "//tr[td[normalize-space()='Olena']]//button[@data-action='delete']");
By labelledInput = By.xpath(
    "//label[normalize-space()='Email']/following-sibling::input");

@Test
void deletesUserByName() {
    driver.findElement(deleteOlena).click();
    // The row with Olena is gone from the table
    assertTrue(driver.findElements(deleteOlena).isEmpty());
}
🔧 Розбір синтаксису: > у CSS — прямий нащадок, :first-child — перший серед сусідів. У XPath tr[td[...]] означає «рядок, у якого є клітинка з умовою», @data-action — звернення до атрибута, following-sibling::input — вісь, що шукає наступного сусіда. normalize-space() обрізає пробіли і враховує текст вкладених тегів.
🔧 Syntax breakdown: In CSS > means a direct child and :first-child the first among its siblings. In XPath tr[td[...]] means a row that has a cell meeting the condition, @data-action reads an attribute, and following-sibling::input is an axis that looks for the next sibling. normalize-space() trims whitespace and includes the text of nested tags.
💡 Простими словами: CSS — для «який це елемент», XPath — для «який елемент поруч із цим текстом».
💡 In plain words: CSS is for "which element is this", XPath is for "which element sits next to this text".
🗣 Як читати вголос За замовчуванням я пишу CSS: він коротший, його легко читати, і для атрибутів та ієрархії його цілком досить. XPath я беру, коли потрібно те, чого CSS не вміє, — знайти елемент за текстом або піднятися до батька, наприклад знайти кнопку видалення в рядку таблиці з конкретним ім’ям. Аргумент, що XPath повільніший, я не вважаю серйозним, бо різниця губиться на фоні мережевих запитів. Головне для мене — не будувати довгі шляхи з індексами, бо вони однаково крихкі в обох мовах.
🗣 How to say it out loud CSS is my default: it is shorter, easy to read and enough for attributes and hierarchy. I reach for XPath when I need something CSS cannot do, matching by text or going up to a parent, like finding the delete button in the table row for a particular name. I do not take the idea that XPath is slower very seriously, because the difference disappears next to the network requests. What matters more to me is avoiding long paths full of indexes, since those are equally fragile in both languages.
10 Що таке відносні локатори в Selenium 4 і коли вони підводять? What are relative locators in Selenium 4, and when do they let you down?
ЛокаториLocators Middle ▶

Що це. Відносні локатори (раніше їх називали friendly locators) з’явилися в Selenium 4 у класі RelativeLocator. Ви задаєте, що шукати, через with(By...), а потім уточнюєте положення відносно іншого елемента: above, below, toLeftOf, toRightOf і near. Умови можна поєднувати в ланцюжок.

What it is. Relative locators, first called friendly locators, arrived in Selenium 4 in the RelativeLocator class. You say what to look for with with(By...) and then narrow it down by position relative to another element: above, below, toLeftOf, toRightOf and near. The conditions can be chained.

Навіщо. Вони допомагають, коли у потрібного елемента немає власних стабільних атрибутів, але поруч є надійний орієнтир. Типовий випадок — поле без id і name під підписом «Пароль» або кнопка праворуч від кнопки «Скасувати» у діалозі від сторонньої бібліотеки, яку не можна розмітити test id.

Why it matters. They help when the target element has no stable attributes of its own but has a reliable landmark nearby. The typical case is a field with no id or name under a Password caption, or the button to the right of Cancel in a dialog from a third-party library that you cannot tag with test ids.

Приклад. driver.findElement(with(By.tagName("input")).below(By.id("email"))) знаходить поле, яке на екрані розташоване нижче за поле email. Під капотом Selenium спочатку знаходить усіх кандидатів за By.tagName, потім через JavaScript бере їхні координати getBoundingClientRect() і відфільтровує та сортує їх за відстанню до орієнтира. near за замовчуванням шукає в межах 50 пікселів.

Example. driver.findElement(with(By.tagName("input")).below(By.id("email"))) finds the field that is rendered below the email field. Under the hood Selenium first finds every candidate with By.tagName, then uses JavaScript to read their getBoundingClientRect() coordinates, and filters and sorts them by distance to the landmark. near looks within 50 pixels by default.

Підводні камені. Відносні локатори працюють із відмальованою розкладкою, а не з DOM. Тому адаптивна верстка їх ламає: на вузькому екрані дві колонки стають однією, і «праворуч» перетворюється на «нижче». Елемент поза вікном, схований або ще не відмальований дає непередбачуваний результат. Ще одна пастка: below означає «будь-де нижче», тож при кількох кандидатах результат залежить від сортування за відстанню.

Pitfalls. Relative locators work on the rendered layout, not on the DOM. Responsive design breaks them: on a narrow screen two columns collapse into one and "to the right" becomes "below". An element that is off screen, hidden or not rendered yet gives unpredictable results. And below means anywhere below, so with several candidates the result depends on the distance sorting.

Уточнення. Я використовую їх як запасний варіант і завжди фіксую розмір вікна в тестах. Якщо можна додати test id, це надійніше за будь-яку геометрію.

Follow-up. I treat them as a fallback and always pin the window size in tests. If a test id can be added, it beats any geometry.

Поле пароля і кнопка підтвердження без власних атрибутів
A password field and a confirm button with no attributes of their own
import static org.openqa.selenium.support.locators.RelativeLocator.with;

@Test
void logsInThroughUnlabelledForm() {
    // Фіксований розмір вікна: геометрія не має залежати від екрана
    driver.manage().window().setSize(new Dimension(1366, 900));

    WebElement email = driver.findElement(By.id("email"));
    email.sendKeys("qa@example.com");

    // Поле введення, що відмальоване під полем email
    WebElement password = driver.findElement(with(By.tagName("input")).below(email));
    password.sendKeys("s3cret");

    // Кнопка праворуч від «Скасувати» і нижче за пароль
    WebElement confirm = driver.findElement(
        with(By.tagName("button")).toRightOf(By.id("cancel")).below(password));
    confirm.click();
}
import static org.openqa.selenium.support.locators.RelativeLocator.with;

@Test
void logsInThroughUnlabelledForm() {
    // Fixed window size: geometry must not depend on the screen
    driver.manage().window().setSize(new Dimension(1366, 900));

    WebElement email = driver.findElement(By.id("email"));
    email.sendKeys("qa@example.com");

    // The input rendered below the email field
    WebElement password = driver.findElement(with(By.tagName("input")).below(email));
    password.sendKeys("s3cret");

    // The button right of Cancel and below the password field
    WebElement confirm = driver.findElement(
        with(By.tagName("button")).toRightOf(By.id("cancel")).below(password));
    confirm.click();
}
🔧 Розбір синтаксису: import static ... RelativeLocator.with дає змогу писати with(...) без імені класу. with(By.tagName("input")) задає кандидатів, а .below(email) приймає або готовий WebElement, або By орієнтира. Виклики .toRightOf(...).below(...) поєднують умови через «і». Результат — звичайний By, тож його можна передати у findElement чи очікування.
🔧 Syntax breakdown: import static ... RelativeLocator.with lets you write with(...) without the class name. with(By.tagName("input")) sets the candidates, and .below(email) takes either a ready WebElement or the landmark's By. Chaining .toRightOf(...).below(...) combines the conditions with a logical and. The result is an ordinary By, so it can go into findElement or a wait.
💡 Простими словами: Відносний локатор шукає «як бачить око» — тому він ламається, щойно сторінка виглядає інакше.
💡 In plain words: A relative locator searches the way the eye sees the page, so it breaks as soon as the page looks different.
🗣 Як читати вголос Відносні локатори в Selenium 4 — це with плюс above, below, toLeftOf, toRightOf чи near. Я задаю тип елемента і орієнтир, а Selenium через JavaScript порівнює координати на екрані. Мені це стає в пригоді для сторонніх віджетів, де я не можу додати test id. Але я розумію обмеження: це геометрія, а не DOM, тож на мобільній ширині чи після редизайну результат змінюється. Тому я фіксую розмір вікна і вважаю такий локатор тимчасовим, поки розробники не додадуть атрибути.
🗣 How to say it out loud Relative locators in Selenium 4 are with plus above, below, toLeftOf, toRightOf or near. I give the kind of element and a landmark, and Selenium compares on-screen coordinates using JavaScript. I find them handy for third-party widgets where I cannot add a test id. I know the limits though: this is geometry rather than DOM, so at mobile width or after a redesign the answer changes. That is why I pin the window size and treat such a locator as temporary until developers add proper attributes.
11 Як у Selenium 4 знайти елемент усередині Shadow DOM? How do you find an element inside Shadow DOM with Selenium 4?
ЛокаториLocators Middle ▶

Що це. Shadow DOM — це інкапсульоване піддерево веб-компонента, прикріплене до елемента-хоста. Звичайний driver.findElement шукає лише у світлому DOM документа, тому поле всередині, скажімо, <my-login> для нього невидиме. У Selenium 4 є метод WebElement.getShadowRoot(), який повертає SearchContext тіньового кореня, і шукати треба вже в ньому.

What it is. Shadow DOM is the encapsulated subtree of a web component, attached to a host element. A plain driver.findElement only searches the light DOM of the document, so a field inside, say, <my-login> is invisible to it. Selenium 4 adds WebElement.getShadowRoot(), which returns the shadow root as a SearchContext, and that is where you search.

Навіщо. Дизайн-системи на Lit, Stencil чи Salesforce Lightning, а також вбудовані контроли браузера широко використовують Shadow DOM. Без правильного підходу тест падає з NoSuchElementException, хоча в DevTools елемент видно, і команда витрачає години на пошук причини.

Why it matters. Design systems built on Lit, Stencil or Salesforce Lightning, and the browser's own built-in controls, use Shadow DOM heavily. Without the right approach the test fails with NoSuchElementException while the element is clearly visible in DevTools, and the team loses hours looking for the reason.

Приклад. Знаходимо хост By.cssSelector("my-login"), беремо getShadowRoot() і вже в ньому шукаємо input[name=password]. Якщо компоненти вкладені, ланцюжок повторюється: хост, тіньовий корінь, внутрішній хост, його тіньовий корінь, і так до потрібного рівня.

Example. Find the host with By.cssSelector("my-login"), call getShadowRoot(), and search for input[name=password] inside it. With nested components the chain repeats: host, shadow root, inner host, its shadow root, down to the level you need.

Підводні камені. Усередині тіньового кореня в Chromium надійно працює лише CSS: XPath там не підтримується і дає помилку про недійсний локатор. Закритий тіньовий корінь (mode: closed) взагалі недоступний, і getShadowRoot() кидає NoSuchShadowRootException, так само як для елемента без тіньового кореня. Посилання на корінь теж може застаріти після перерендеру компонента, тому його варто знаходити заново, а не кешувати в полі класу.

Pitfalls. Inside a shadow root Chromium reliably supports CSS only: XPath is not supported there and fails with an invalid locator error. A closed shadow root (mode: closed) cannot be reached at all, and getShadowRoot() throws NoSuchShadowRootException, just as it does for an element with no shadow root. The root reference can also go stale after the component re-renders, so find it again rather than caching it in a field.

Уточнення. Питають і про старий спосіб через executeScript("return arguments[0].shadowRoot"). Він працював у Selenium 3, але в Selenium 4 є рідний метод, і обхід через JavaScript уже не потрібен.

Follow-up. Interviewers also ask about the old trick with executeScript("return arguments[0].shadowRoot"). It was needed in Selenium 3, but Selenium 4 has a native method and the JavaScript workaround is no longer necessary.

Вхід через веб-компонент із вкладеним Shadow DOM
Logging in through a web component with nested Shadow DOM
@Test
void logsInThroughWebComponent() {
    // Хост у світлому DOM і його тіньовий корінь
    WebElement host = driver.findElement(By.cssSelector("my-login"));
    SearchContext loginRoot = host.getShadowRoot();

    // Усередині кореня — лише CSS-селектори
    loginRoot.findElement(By.cssSelector("input[name='email']")).sendKeys("qa@example.com");
    loginRoot.findElement(By.cssSelector("input[name='password']")).sendKeys("s3cret");

    // Кнопка — окремий вкладений компонент зі своїм коренем
    SearchContext buttonRoot = loginRoot
        .findElement(By.cssSelector("my-button"))
        .getShadowRoot();
    buttonRoot.findElement(By.cssSelector("button")).click();
}
@Test
void logsInThroughWebComponent() {
    // The host in the light DOM and its shadow root
    WebElement host = driver.findElement(By.cssSelector("my-login"));
    SearchContext loginRoot = host.getShadowRoot();

    // Inside the root: CSS selectors only
    loginRoot.findElement(By.cssSelector("input[name='email']")).sendKeys("qa@example.com");
    loginRoot.findElement(By.cssSelector("input[name='password']")).sendKeys("s3cret");

    // The button is a nested component with its own root
    SearchContext buttonRoot = loginRoot
        .findElement(By.cssSelector("my-button"))
        .getShadowRoot();
    buttonRoot.findElement(By.cssSelector("button")).click();
}
🔧 Розбір синтаксису: getShadowRoot() повертає SearchContext — той самий інтерфейс, що має WebDriver і WebElement, тому на ньому доступні findElement і findElements. Ланцюжок loginRoot.findElement(...).getShadowRoot() спускається на рівень глибше у вкладений компонент. Лапки всередині селектора одинарні, щоб не екранувати подвійні в Java-рядку.
🔧 Syntax breakdown: getShadowRoot() returns a SearchContext, the same interface WebDriver and WebElement implement, so findElement and findElements are available on it. The chain loginRoot.findElement(...).getShadowRoot() goes one level deeper into a nested component. The selector uses single quotes so the double quotes of the Java string need no escaping.
💡 Простими словами: Тіньовий DOM — це кімната із зачиненими дверима: спершу знайди двері (хост), відчини їх (getShadowRoot) і тільки тоді шукай усередині.
💡 In plain words: Shadow DOM is a room behind a door: find the door (the host), open it (getShadowRoot) and only then look inside.
🗣 Як читати вголос Якщо елемент у DevTools видно, а findElement його не знаходить, я першим ділом перевіряю, чи немає над ним shadow-root. У Selenium 4 я знаходжу хост, викликаю getShadowRoot і шукаю вже в отриманому SearchContext, а для вкладених компонентів повторюю це кілька разів. Всередині кореня пишу лише CSS, бо XPath там у Chrome не працює. Закритий shadow root автоматизувати так не вийде, тоді я прошу розробників відкрити його в тестовій збірці або тестую цей компонент окремо.
🗣 How to say it out loud If DevTools shows the element and findElement still cannot find it, the first thing I check is whether there is a shadow root above it. In Selenium 4 I find the host, call getShadowRoot and search inside the returned SearchContext, repeating that for nested components. Inside a root I only write CSS, because XPath does not work there in Chrome. A closed shadow root cannot be automated this way, so I either ask the developers to make it open in the test build or cover that component separately.
12 Чим findElement відрізняється від findElements і як шукати всередині батьківського елемента? How does findElement differ from findElements, and how do you search inside a parent element?
ЛокаториLocators Junior ▶

Що це. findElement повертає перший збіг або кидає NoSuchElementException, якщо нічого не знайшов. findElements повертає List<WebElement> з усіма збігами і ніколи не кидає виняток через відсутність елементів — просто дає порожній список. Обидва методи є і на WebDriver, і на WebElement: у другому випадку пошук обмежується піддеревом цього елемента.

What it is. findElement returns the first match or throws NoSuchElementException when nothing is found. findElements returns a List<WebElement> with every match and never throws for missing elements, it just gives an empty list. Both methods exist on WebDriver and on WebElement; on an element the search is limited to that element's subtree.

Навіщо. Різна поведінка підказує, де що брати. Коли елемент обов’язковий, findElement одразу падає з ясною помилкою. Коли треба перевірити відсутність чи порахувати кількість, findElements дає це без try/catch. Пошук від батька потрібен для повторюваних блоків: картки товарів, рядки таблиць, коментарі, де всередині кожного блоку однакові класи.

Why it matters. The different behaviour tells you which to use. When an element is mandatory, findElement fails straight away with a clear error. When you need to prove absence or count items, findElements does it without try/catch. Searching from a parent is essential for repeated blocks such as product cards, table rows or comments, where every block uses the same classes inside.

Приклад. У каталозі десять карток з однаковою розміткою. Спершу знаходимо картку за назвою товару, а вже в ній — ціну і кнопку «У кошик». Так тест ніколи не візьме ціну з сусідньої картки, навіть якщо порядок карток зміниться.

Example. A catalogue shows ten cards with identical markup. First find the card by product name, then look inside it for the price and the Add to cart button. That way the test never picks up the price of a neighbouring card, even if the order of the cards changes.

Підводні камені. Якщо увімкнено неявне очікування, findElements для відсутнього елемента чекає весь його таймаут, перш ніж повернути порожній список, і перевірки відсутності стають повільними. Інша класична помилка — XPath у пошуку від елемента: //span шукає по всьому документу, а не в батьку, тому потрібна крапка на початку, .//span. Для CSS такої проблеми немає.

Pitfalls. With an implicit wait switched on, findElements for a missing element waits out the whole timeout before returning an empty list, so absence checks become slow. Another classic mistake is XPath in a search from an element: //span searches the whole document, not the parent, so it needs a leading dot, .//span. CSS does not have this problem.

Уточнення. Питають, як перевірити, що елемента немає. Я пишу assertTrue(driver.findElements(by).isEmpty()) або чекаю invisibilityOfElementLocated, а не ловлю виняток від findElement.

Follow-up. You may be asked how to check that an element is absent. I write assertTrue(driver.findElements(by).isEmpty()) or wait for invisibilityOfElementLocated, instead of catching the exception from findElement.

Ціна і кнопка з конкретної картки товару
Price and button from one specific product card
@Test
void addsKeyboardToCart() {
    List<WebElement> cards = driver.findElements(By.cssSelector("[data-testid='product-card']"));
    assertFalse(cards.isEmpty(), "Каталог порожній");

    // Картка з потрібною назвою
    WebElement keyboard = cards.stream()
        .filter(c -> c.findElement(By.cssSelector(".title")).getText().equals("Keyboard K2"))
        .findFirst()
        .orElseThrow();

    // Пошук лише всередині цієї картки
    String price = keyboard.findElement(By.cssSelector(".price")).getText();
    keyboard.findElement(By.xpath(".//button[@data-action='add']")).click();

    assertEquals("$79", price);
    // Банер про помилку не з'явився
    assertTrue(driver.findElements(By.cssSelector(".error-banner")).isEmpty());
}
@Test
void addsKeyboardToCart() {
    List<WebElement> cards = driver.findElements(By.cssSelector("[data-testid='product-card']"));
    assertFalse(cards.isEmpty(), "Catalogue is empty");

    // The card with the right title
    WebElement keyboard = cards.stream()
        .filter(c -> c.findElement(By.cssSelector(".title")).getText().equals("Keyboard K2"))
        .findFirst()
        .orElseThrow();

    // Search only inside this card
    String price = keyboard.findElement(By.cssSelector(".price")).getText();
    keyboard.findElement(By.xpath(".//button[@data-action='add']")).click();

    assertEquals("$79", price);
    // No error banner appeared
    assertTrue(driver.findElements(By.cssSelector(".error-banner")).isEmpty());
}
🔧 Розбір синтаксису: findElements повертає List<WebElement>, тому по ньому можна пройтися стрімом: filter відбирає картку за назвою, findFirst().orElseThrow() падає, якщо такої немає. keyboard.findElement(...) шукає лише в піддереві картки. У XPath .//button крапка означає «від поточного вузла», без неї пошук пішов би від кореня документа.
🔧 Syntax breakdown: findElements returns a List<WebElement>, so you can stream over it: filter picks the card by title and findFirst().orElseThrow() fails if there is none. keyboard.findElement(...) searches only the card's subtree. In the XPath .//button the dot means "from the current node"; without it the search would start at the document root.
💡 Простими словами: findElement — «дай мені його або впади», findElements — «покажи всі, навіть якщо їх нуль».
💡 In plain words: findElement means "give me one or fail", findElements means "show me all of them, even if there are none".
🗣 Як читати вголос Різниця проста: findElement дає перший збіг або кидає NoSuchElementException, а findElements повертає список, який може бути порожнім. Тому для обов’язкових елементів я беру перший, а для підрахунку чи перевірки відсутності — другий. Для повторюваних блоків, як картки товарів, я спершу знаходжу потрібний блок, а тоді шукаю всередині нього. Тут я завжди пам’ятаю про крапку в XPath, бо без неї пошук іде по всій сторінці, і про те, що неявне очікування сповільнює перевірку відсутності.
🗣 How to say it out loud The difference is simple: findElement gives the first match or throws NoSuchElementException, while findElements returns a list that can be empty. So I use the first for mandatory elements and the second for counting or proving absence. For repeated blocks like product cards I find the right block first and then search inside it. There I always remember the leading dot in XPath, because without it the search covers the whole page, and I remember that an implicit wait makes absence checks slow.
▶ 3 6
13 Чому getText() для поля вводу повертає порожній рядок і як прочитати те, що ввів користувач? Why does getText() return an empty string for an input field, and how do you read what the user typed?
ВзаємодіяInteraction Junior ▶

Що це. getText() повертає видимий відрендерений текст елемента разом із нащадками — приблизно те, що людина бачить очима. У <input> немає текстових дочірніх вузлів: введене значення живе у DOM-властивості value. Тому для поля вводу getText() дає порожній рядок, а поточне значення читається через getDomProperty("value").

What it is. getText() returns the visible, rendered text of an element and its descendants — roughly what a person sees. An <input> has no text child nodes: whatever was typed lives in the DOM property value. So for an input field getText() gives an empty string, and the current value is read with getDomProperty("value").

Навіщо. У Selenium 4 є три різні методи, і плутанина між ними дає хибні асерти. getDomProperty читає живу властивість, яку змінює користувач і JavaScript. getDomAttribute читає атрибут із розмітки, тобто початкове значення. Старий getAttribute — «найкраща здогадка»: спершу пробує властивість, потім атрибут, тож для value теж поверне поточний текст.

Why it matters. Selenium 4 has three different methods, and mixing them up produces wrong assertions. getDomProperty reads the live property that the user and JavaScript change. getDomAttribute reads the attribute from the markup, which is the initial value. The older getAttribute is a best guess: it tries the property first and then the attribute, so for value it also returns the current text.

Приклад. Форма профілю приходить з value="Olena", тест стирає поле і вводить «Oksana». Тепер getDomProperty("value") поверне Oksana, getDomAttribute("value") — досі Olena, а getText() — порожній рядок. Асерт на getText() тут завжди впаде або, гірше, пройде при перевірці на порожнечу.

Example. A profile form arrives with value="Olena", the test clears the field and types Oksana. Now getDomProperty("value") returns Oksana, getDomAttribute("value") still returns Olena, and getText() returns an empty string. An assertion on getText() here either always fails or, worse, passes when you check for emptiness.

Підводні камені. getText() не бачить прихованого тексту: для елемента з display: none він поверне порожній рядок, і це не баг Selenium. Ще він нормалізує пробіли і враховує CSS-перетворення на кшталт text-transform: uppercase, тому очікуване значення в тесті інколи треба писати великими літерами.

Pitfalls. getText() ignores hidden text: an element with display: none gives an empty string, and that is not a Selenium bug. It also normalises whitespace and applies CSS transforms such as text-transform: uppercase, so the expected value in a test sometimes has to be written in capitals.

Уточнення. Можуть спитати про <textarea> і чекбокси. Для textarea поточний текст так само береться з властивості value, а стан чекбокса надійніше перевіряти через isSelected(), ніж читати атрибут checked, який відображає лише початковий стан.

Follow-up. You may be asked about <textarea> and checkboxes. A textarea also exposes its current text through the value property, and a checkbox state is better checked with isSelected() than by reading the checked attribute, which only reflects the initial state.

Три способи прочитати одне поле
Three ways to read the same field
@Test
void readsCurrentValueOfInput() {
    driver.get("https://shop.example.com/profile");
    WebElement name = driver.findElement(By.id("first-name"));

    name.clear();
    name.sendKeys("Oksana");

    assertEquals("", name.getText());                         // у input немає текстових вузлів
    assertEquals("Oksana", name.getDomProperty("value"));     // поточне значення
    assertEquals("Olena", name.getDomAttribute("value"));     // значення з HTML-розмітки

    WebElement title = driver.findElement(By.cssSelector("h1.profile-title"));
    assertEquals("PROFILE", title.getText());                 // видимий текст з урахуванням CSS
}
@Test
void readsCurrentValueOfInput() {
    driver.get("https://shop.example.com/profile");
    WebElement name = driver.findElement(By.id("first-name"));

    name.clear();
    name.sendKeys("Oksana");

    assertEquals("", name.getText());                         // an input has no text nodes
    assertEquals("Oksana", name.getDomProperty("value"));     // current value
    assertEquals("Olena", name.getDomAttribute("value"));     // value from the HTML markup

    WebElement title = driver.findElement(By.cssSelector("h1.profile-title"));
    assertEquals("PROFILE", title.getText());                 // visible text after CSS is applied
}
🔧 Розбір синтаксису: clear() очищає поле, sendKeys друкує текст як клавіатура. getDomProperty("value") звертається до JS-властивості вузла, getDomAttribute("value") — до атрибута з розмітки. assertEquals(expected, actual) з JUnit 5 спершу приймає очікуване значення. Останній асерт показує, що getText() повертає текст уже після text-transform.
🔧 Syntax breakdown: clear() empties the field and sendKeys types text like a keyboard. getDomProperty("value") reads the node's JS property, getDomAttribute("value") reads the markup attribute. JUnit 5 assertEquals(expected, actual) takes the expected value first. The last assertion shows that getText() returns text after text-transform has been applied.
💡 Простими словами: getText — це те, що видно на екрані; value — те, що лежить усередині поля. Для інпутів завжди друге.
💡 In plain words: getText is what you see on screen; value is what sits inside the field. For inputs it is always the second one.
🗣 Як читати вголос Я одразу розводжу видимий текст і значення поля. getText повертає тільки відрендерений текст, а в інпута текстових вузлів немає, тому там порожньо. Поточне значення я читаю через getDomProperty value, а якщо треба перевірити, що прийшло з сервера в розмітці, — через getDomAttribute. Старий getAttribute працює як здогадка і для value теж дасть поточний текст, але я волію явні методи. Ще пам’ятаю, що прихований елемент дає порожній getText, і це очікувана поведінка.
🗣 How to say it out loud I keep visible text and a field value apart. getText only returns rendered text, and an input has no text nodes, so it comes back empty. I read the current value with getDomProperty value, and when I want to check what the server put into the markup I use getDomAttribute. The old getAttribute is a guess and does return the current text for value, but I prefer the explicit methods. I also remember that a hidden element gives an empty getText, and that is expected behaviour.
14 Що перевіряють isDisplayed(), isEnabled() та isSelected() і чого вони не гарантують? What do isDisplayed(), isEnabled() and isSelected() check, and what do they not guarantee?
ВзаємодіяInteraction Junior ▶

Що це. Три булеві методи WebElement про стан уже знайденого елемента. isDisplayed() каже, чи елемент відрендерений видимим: немає display: none, visibility: hidden, нульового розміру чи прихованого предка. isEnabled() повертає false для контролів форми з атрибутом disabled. isSelected() працює для чекбоксів, радіокнопок і <option>.

What it is. Three boolean WebElement methods about the state of an element you have already found. isDisplayed() tells whether the element renders as visible: no display: none, no visibility: hidden, no zero size and no hidden ancestor. isEnabled() returns false for form controls carrying the disabled attribute. isSelected() works for checkboxes, radio buttons and <option> elements.

Навіщо. Ними перевіряють бізнес-правила інтерфейсу: кнопка «Оплатити» неактивна, поки не прийняті умови; чекбокс розсилки вимкнений за замовчуванням; банер помилки з’являється після невдалого входу. Це прості й читабельні асерти, які не потребують JavaScript.

Why it matters. They check UI business rules: the Pay button stays inactive until the terms are accepted, the newsletter checkbox is off by default, an error banner appears after a failed login. These are short, readable assertions that need no JavaScript.

Приклад. Тест реєстрації перевіряє, що submit.isEnabled() дорівнює false, клікає чекбокс згоди, чекає зміни стану і перевіряє, що кнопка стала активною, а terms.isSelected() — true. Відсутність банера помилки перевіряється через findElements(...).isEmpty(), бо findElement на відсутньому вузлі кине виняток ще до виклику isDisplayed().

Example. A sign-up test asserts that submit.isEnabled() is false, clicks the consent checkbox, waits for the state change and asserts that the button became active and terms.isSelected() is true. The absence of the error banner is checked with findElements(...).isEmpty(), because findElement on a missing node throws before isDisplayed() is ever called.

Підводні камені. isDisplayed() не знає, чи елемент перекритий іншим шаром і чи він у зоні прокрутки: видимий за правилами CSS елемент під модальним вікном усе одно дасть true. isEnabled() не бачить «м’якої» неактивності через aria-disabled="true" чи клас на кшталт .btn-disabled — для кастомних кнопок треба перевіряти ці атрибути окремо.

Pitfalls. isDisplayed() knows nothing about another layer covering the element or about the scroll area: an element that is visible by CSS rules but sits under a modal still returns true. isEnabled() does not see soft inactivity through aria-disabled="true" or a class such as .btn-disabled, so custom buttons need those attributes checked separately.

Уточнення. Часто питають, чи можна одразу після дії робити assertTrue(el.isDisplayed()). Краще ні: стан змінюється асинхронно, тому спершу явне очікування, наприклад ExpectedConditions.elementToBeSelected або visibilityOf, а вже потім асерт.

Follow-up. A common question is whether you can call assertTrue(el.isDisplayed()) straight after an action. Better not: state changes asynchronously, so wait explicitly first, for example with ExpectedConditions.elementToBeSelected or visibilityOf, and assert afterwards.

Перевірка стану форми реєстрації
Checking the state of a sign-up form
@Test
void payButtonUnlocksAfterConsent() {
    WebElement submit = driver.findElement(By.cssSelector("[data-testid='submit']"));
    WebElement terms = driver.findElement(By.id("terms"));

    assertFalse(submit.isEnabled());          // атрибут disabled на кнопці
    assertFalse(terms.isSelected());          // чекбокс вимкнений за замовчуванням

    terms.click();
    wait.until(ExpectedConditions.elementToBeSelected(terms));
    wait.until(ExpectedConditions.elementToBeClickable(submit));

    // відсутність елемента перевіряємо списком, а не isDisplayed()
    assertTrue(driver.findElements(By.cssSelector(".error-banner")).isEmpty());

    // кастомна кнопка: isEnabled() не бачить aria-disabled
    WebElement promo = driver.findElement(By.cssSelector("[data-testid='apply-promo']"));
    assertEquals("true", promo.getDomAttribute("aria-disabled"));
}
@Test
void payButtonUnlocksAfterConsent() {
    WebElement submit = driver.findElement(By.cssSelector("[data-testid='submit']"));
    WebElement terms = driver.findElement(By.id("terms"));

    assertFalse(submit.isEnabled());          // disabled attribute on the button
    assertFalse(terms.isSelected());          // checkbox is off by default

    terms.click();
    wait.until(ExpectedConditions.elementToBeSelected(terms));
    wait.until(ExpectedConditions.elementToBeClickable(submit));

    // check absence with a list, not with isDisplayed()
    assertTrue(driver.findElements(By.cssSelector(".error-banner")).isEmpty());

    // custom button: isEnabled() does not see aria-disabled
    WebElement promo = driver.findElement(By.cssSelector("[data-testid='apply-promo']"));
    assertEquals("true", promo.getDomAttribute("aria-disabled"));
}
🔧 Розбір синтаксису: wait — це заздалегідь створений WebDriverWait. elementToBeSelected чекає, поки isSelected() стане true, elementToBeClickable — поки елемент видимий і активний. findElements повертає список, який буває порожнім, тому годиться для перевірки відсутності. getDomAttribute читає атрибут як рядок або повертає null, якщо його немає.
🔧 Syntax breakdown: wait is a WebDriverWait created beforehand. elementToBeSelected waits until isSelected() becomes true, elementToBeClickable until the element is visible and enabled. findElements returns a list that may be empty, which makes it right for checking absence. getDomAttribute reads the attribute as a string or returns null when it is missing.
💡 Простими словами: Ці методи відповідають на питання про CSS і атрибути, а не про те, чи людина реально може натиснути елемент.
💡 In plain words: These methods answer questions about CSS and attributes, not about whether a person can actually press the element.
🗣 Як читати вголос Я використовую ці три методи для асертів стану інтерфейсу: isDisplayed про видимість за CSS, isEnabled про атрибут disabled, isSelected для чекбоксів, радіо та опцій. Але я пам’ятаю їхні межі. Елемент під модалкою формально видимий, кастомна кнопка з aria-disabled для Selenium активна, а на відсутньому вузлі я взагалі отримаю NoSuchElementException ще до перевірки. Тому відсутність я перевіряю через findElements і порожній список, а перед асертом стану ставлю явне очікування.
🗣 How to say it out loud I use these three methods to assert UI state: isDisplayed for visibility by CSS, isEnabled for the disabled attribute, isSelected for checkboxes, radios and options. I also know where they stop. An element under a modal is formally visible, a custom button with aria-disabled is enabled as far as Selenium can tell, and for a missing node I get NoSuchElementException before any check runs. So I check absence with findElements and an empty list, and I put an explicit wait in front of a state assertion.
15 Як працювати з випадним списком через клас Select і чому він не підходить для кастомних дропдаунів? How do you work with a dropdown through the Select class, and why does it not fit custom dropdowns?
ВзаємодіяInteraction Junior ▶

Що це. org.openqa.selenium.support.ui.Select — обгортка над нативним HTML-елементом <select>. Вона вміє обирати опцію за видимим текстом, атрибутом value або індексом, повертати обрані опції і для списків з атрибутом multiple знімати вибір. Конструктор приймає WebElement і кидає UnexpectedTagNameException, якщо це не <select>.

What it is. org.openqa.selenium.support.ui.Select wraps a native HTML <select> element. It picks an option by visible text, by its value attribute or by index, returns the selected options, and can deselect in lists that carry the multiple attribute. The constructor takes a WebElement and throws UnexpectedTagNameException when it is not a <select>.

Навіщо. Нативний список браузер малює сам, і клікати по опціях у ньому незручно та платформозалежно. Select ховає цю різницю і дає читабельний код. Найстабільніший варіант зазвичай selectByValue: значення — це технічний контракт із бекендом, а видимий текст змінюється з перекладами.

Why it matters. The browser draws a native list itself, and clicking options inside it is awkward and platform-dependent. Select hides that difference and keeps the code readable. The most stable choice is usually selectByValue: the value is a technical contract with the backend, while visible text changes with translations.

Приклад. На сторінці доставки тест обирає країну selectByValue("UA"), а потім перевіряє, що getFirstSelectedOption().getText() дорівнює «Україна» — так перевіряється і збереження, і те, що бачить користувач. Для фільтра з множинним вибором isMultiple() повертає true, і можна викликати deselectAll().

Example. On a delivery page the test picks the country with selectByValue("UA") and then asserts that getFirstSelectedOption().getText() equals Ukraine, which covers both what gets saved and what the user sees. For a multi-select filter isMultiple() returns true, and deselectAll() is available.

Підводні камені. Більшість сучасних UI-бібліотек малюють дропдаун із div і li, а справжнього <select> там немає або він прихований. Select тоді або впаде на конструкторі, або змінить прихований елемент без подій, на які реагує фреймворк. Такий список відкривають кліком по тригеру і клікають потрібну опцію, чекаючи її появи. deselectAll() на звичайному одиночному списку кидає UnsupportedOperationException.

Pitfalls. Most modern UI libraries draw a dropdown out of div and li elements, with no real <select> or only a hidden one. Select then either fails in the constructor or changes the hidden element without the events the framework listens to. Such a list is opened by clicking its trigger, then the option is clicked after waiting for it to appear. deselectAll() on an ordinary single-choice list throws UnsupportedOperationException.

Уточнення. Інколи питають про індекс. selectByIndex найкрихкіший: додали опцію «Оберіть країну» на початок — і тест тихо обирає не те. Використовуйте його лише там, де порядок і є вимогою.

Follow-up. Index sometimes comes up. selectByIndex is the most brittle: someone adds a Choose a country entry at the top and the test quietly selects the wrong option. Use it only where the order itself is the requirement.

Нативний select і кастомний дропдаун
A native select and a custom dropdown
// нативний <select id="country">
Select country = new Select(driver.findElement(By.id("country")));
country.selectByValue("UA");                              // стабільний технічний ключ
assertEquals("Україна", country.getFirstSelectedOption().getText());

Select tags = new Select(driver.findElement(By.id("tags")));
if (tags.isMultiple()) {
    tags.deselectAll();
    tags.selectByVisibleText("Sale");
    tags.selectByVisibleText("New");
}
assertEquals(2, tags.getAllSelectedOptions().size());

// кастомний дропдаун без <select>: клік по тригеру, потім по опції
driver.findElement(By.cssSelector("[data-testid='city-trigger']")).click();
wait.until(ExpectedConditions.elementToBeClickable(
        By.cssSelector("[role='option'][data-value='kyiv']"))).click();
// native <select id="country">
Select country = new Select(driver.findElement(By.id("country")));
country.selectByValue("UA");                              // stable technical key
assertEquals("Ukraine", country.getFirstSelectedOption().getText());

Select tags = new Select(driver.findElement(By.id("tags")));
if (tags.isMultiple()) {
    tags.deselectAll();
    tags.selectByVisibleText("Sale");
    tags.selectByVisibleText("New");
}
assertEquals(2, tags.getAllSelectedOptions().size());

// custom dropdown without <select>: click the trigger, then the option
driver.findElement(By.cssSelector("[data-testid='city-trigger']")).click();
wait.until(ExpectedConditions.elementToBeClickable(
        By.cssSelector("[role='option'][data-value='kyiv']"))).click();
🔧 Розбір синтаксису: new Select(element) перевіряє тег у конструкторі. selectByValue, selectByVisibleText і selectByIndex — три способи вибору. getFirstSelectedOption() повертає WebElement опції, getAllSelectedOptions() — список. wait.until(...) повертає елемент, коли умова виконана, тому .click() можна викликати ланцюжком.
🔧 Syntax breakdown: new Select(element) checks the tag in its constructor. selectByValue, selectByVisibleText and selectByIndex are the three ways to choose. getFirstSelectedOption() returns the option as a WebElement, getAllSelectedOptions() returns a list. wait.until(...) returns the element once the condition holds, so .click() can be chained.
💡 Простими словами: Select — лише для справжнього тегу select. Якщо список намальований дівами, з ним працюють як людина: відкрий і клікни.
💡 In plain words: Select is only for a real select tag. If the list is drawn with divs, treat it like a person would: open it and click.
🗣 Як читати вголос Для нативного списку я беру клас Select і найчастіше обираю за value, бо це стабільний ключ, а видимий текст змінюється з локалізацією. Після вибору перевіряю getFirstSelectedOption, щоб бачити те саме, що користувач. Але спершу дивлюсь у DOM: у більшості сучасних інтерфейсів справжнього select немає, там div з роллю listbox. Тоді Select або впаде з UnexpectedTagNameException, або змінить прихований елемент, і фреймворк цього не помітить. Такий дропдаун я відкриваю кліком і обираю опцію після очікування.
🗣 How to say it out loud For a native list I use the Select class and usually pick by value, because that is a stable key while visible text changes with localisation. After selecting I check getFirstSelectedOption so I see what the user sees. But first I look at the DOM: most modern interfaces have no real select at all, just a div with the listbox role. Then Select either fails with UnexpectedTagNameException or changes a hidden element the framework never notices. I open that kind of dropdown with a click and pick the option after a wait.
16 Коли потрібен Actions API і як зробити hover, drag and drop та комбінацію клавіш? When do you need the Actions API, and how do you do a hover, a drag and drop and a key combination?
ВзаємодіяInteraction Middle ▶

Що це. Actions — будівник низькорівневих дій користувача: рух миші, натискання і відпускання кнопок, клавіш, прокрутка колесом. Кроки накопичуються в ланцюжку і відправляються драйверу одним W3C-запитом Perform Actions лише після виклику perform(). Так відтворюються жести, яких немає у звичайному click() чи sendKeys().

What it is. Actions builds low-level user input: mouse movement, pressing and releasing buttons and keys, wheel scrolling. The steps pile up in a chain and are sent to the driver as one W3C Perform Actions request only when perform() is called. That is how you reproduce gestures a plain click() or sendKeys() cannot express.

Навіщо. Меню, що розкривається при наведенні, перетягування карток на канбан-дошці, виділення тексту з Shift, гарячі клавіші Ctrl+A, контекстне меню правою кнопкою, подвійний клік — усе це Actions. У Selenium 4.2 з’явилися дії колеса, зокрема scrollToElement, яка прокручує сторінку без JavaScript.

Why it matters. A menu that opens on hover, dragging cards across a kanban board, selecting text with Shift, shortcuts like Ctrl+A, a right-click context menu, a double click — all of that is Actions. Selenium 4.2 added wheel actions, including scrollToElement, which scrolls the page without JavaScript.

Приклад. Щоб дістатися пункту «Налаштування» в меню профілю, тест викликає moveToElement(avatar), чекає появи пункту і клікає його. Для канбану — dragAndDrop(card, doneColumn) або повільніша послідовність clickAndHold, moveToElement, release, яку легше налагоджувати.

Example. To reach Settings in a profile menu the test calls moveToElement(avatar), waits for the item to show up and clicks it. For a kanban board it uses dragAndDrop(card, doneColumn), or the slower clickAndHold, moveToElement, release sequence, which is easier to debug.

Підводні камені. Найчастіша помилка — забутий perform(): ланцюжок збирається, але нічого не відбувається, і тест падає десь далі. Друга — Ctrl на macOS: там потрібен Keys.COMMAND, тож клавішу-модифікатор варто обирати за ОС. Третя — HTML5 drag and drop через атрибут draggable у Chromium часто не спрацьовує через dragAndDrop, бо синтетичні рухи миші не запускають події dragstart і drop; тоді допомагає перевірка через API або бібліотека самого застосунку.

Pitfalls. The most common mistake is a forgotten perform(): the chain is built, nothing happens, and the test fails somewhere later. The second is Ctrl on macOS, where you need Keys.COMMAND, so pick the modifier by operating system. The third is HTML5 drag and drop through the draggable attribute: in Chromium dragAndDrop often does nothing, because synthetic mouse moves do not fire dragstart and drop; checking through the API or using the app's own library helps there.

Уточнення. Можуть спитати, чим Actions.sendKeys відрізняється від WebElement.sendKeys. Перший друкує в елемент, що зараз у фокусі, імітуючи клавіатуру, другий спершу фокусує конкретний елемент. Для звичайних полів вистачає другого.

Follow-up. You may be asked how Actions.sendKeys differs from WebElement.sendKeys. The first types into whatever element has focus, imitating the keyboard; the second focuses a specific element first. For ordinary fields the second is enough.

Hover, перетягування і гарячі клавіші
Hover, drag and drop and shortcuts
Actions actions = new Actions(driver);

// hover: меню профілю розкривається при наведенні
WebElement avatar = driver.findElement(By.cssSelector("[data-testid='avatar']"));
actions.moveToElement(avatar).perform();
wait.until(ExpectedConditions.elementToBeClickable(By.linkText("Settings"))).click();

// перетягування картки в колонку Done
WebElement card = driver.findElement(By.id("task-42"));
WebElement done = driver.findElement(By.id("column-done"));
actions.clickAndHold(card).moveToElement(done).release().perform();

// Ctrl+A / Cmd+A, залежно від ОС
Keys mod = Platform.getCurrent().is(Platform.MAC) ? Keys.COMMAND : Keys.CONTROL;
WebElement editor = driver.findElement(By.id("notes"));
actions.click(editor).keyDown(mod).sendKeys("a").keyUp(mod).sendKeys(Keys.DELETE).perform();

// прокрутка колесом до футера (Selenium 4.2+)
actions.scrollToElement(driver.findElement(By.tagName("footer"))).perform();
Actions actions = new Actions(driver);

// hover: the profile menu opens on mouse over
WebElement avatar = driver.findElement(By.cssSelector("[data-testid='avatar']"));
actions.moveToElement(avatar).perform();
wait.until(ExpectedConditions.elementToBeClickable(By.linkText("Settings"))).click();

// drag a card into the Done column
WebElement card = driver.findElement(By.id("task-42"));
WebElement done = driver.findElement(By.id("column-done"));
actions.clickAndHold(card).moveToElement(done).release().perform();

// Ctrl+A / Cmd+A depending on the OS
Keys mod = Platform.getCurrent().is(Platform.MAC) ? Keys.COMMAND : Keys.CONTROL;
WebElement editor = driver.findElement(By.id("notes"));
actions.click(editor).keyDown(mod).sendKeys("a").keyUp(mod).sendKeys(Keys.DELETE).perform();

// wheel scroll to the footer (Selenium 4.2+)
actions.scrollToElement(driver.findElement(By.tagName("footer"))).perform();
🔧 Розбір синтаксису: Кожен метод Actions повертає той самий об’єкт, тому кроки пишуться ланцюжком. perform() наприкінці відправляє накопичені дії й очищає ланцюжок. keyDown і keyUp тримають модифікатор натиснутим між ними. Platform.getCurrent() повертає ОС, де запущено тест, — для віддаленого браузера на Grid ОС треба брати з капабіліті.
🔧 Syntax breakdown: Every Actions method returns the same object, so the steps are chained. perform() at the end sends the collected actions and clears the chain. keyDown and keyUp keep the modifier pressed between them. Platform.getCurrent() returns the OS the test runs on; for a remote browser on Grid take the OS from the capabilities instead.
💡 Простими словами: Actions — це пульт для миші й клавіатури. Натискаєш кнопки в ланцюжку, а perform() каже «поїхали».
💡 In plain words: Actions is a remote control for the mouse and keyboard. You queue the buttons in a chain, and perform() says go.
🗣 Як читати вголос Actions я беру тоді, коли звичайного click чи sendKeys мало: наведення для випадного меню, перетягування, комбінації клавіш, подвійний чи правий клік. Будую ланцюжок і обов’язково закінчую perform, бо без нього нічого не відправиться. На канбані я часто розписую clickAndHold, moveToElement і release окремо — так простіше зрозуміти, де зламалось. Пам’ятаю про Command на маку і про те, що HTML5 drag and drop у Chrome через Actions буває ненадійним. Для прокрутки з Selenium 4.2 є scrollToElement, тож JavaScript тут не потрібен.
🗣 How to say it out loud I reach for Actions when a plain click or sendKeys is not enough: hovering to open a menu, drag and drop, key combinations, double or right clicks. I build a chain and always finish with perform, because nothing is sent without it. On a kanban board I often spell out clickAndHold, moveToElement and release separately, which makes it easier to see where it broke. I remember Command on a Mac and that HTML5 drag and drop in Chrome can be unreliable through Actions. For scrolling there is scrollToElement since Selenium 4.2, so I do not need JavaScript for it.
17 Коли JavascriptExecutor доречний у тесті, а коли його використання — це обхід перевірки? When is JavascriptExecutor appropriate in a test, and when is using it a way of dodging the check?
ВзаємодіяInteraction Middle ▶

Що це. JavascriptExecutor — інтерфейс драйвера з методами executeScript і executeAsyncScript, які виконують JavaScript у контексті поточної сторінки чи фрейму. Аргументи з Java доступні як arguments[0], arguments[1], а WebElement передається як справжній DOM-вузол. Результат через return повертається в Java: числа як Long або Double, рядки, булеві, елементи, списки і мапи.

What it is. JavascriptExecutor is a driver interface with executeScript and executeAsyncScript, which run JavaScript in the context of the current page or frame. Java arguments are available as arguments[0], arguments[1], and a WebElement arrives as a real DOM node. Whatever the script returns comes back to Java: numbers as Long or Double, strings, booleans, elements, lists and maps.

Навіщо. Доречні сценарії — читати стан, якого немає в WebDriver API: document.readyState, значення з localStorage, прапорець застосунку на кшталт window.appReady, розмір канви, прокрутку контейнера. Також підготовка оточення: очистити сховище, встановити фіча-флаг, прокрутити всередині віртуалізованого списку.

Why it matters. The appropriate cases are reading state that the WebDriver API does not expose: document.readyState, a value in localStorage, an app flag such as window.appReady, canvas size, the scroll position of a container. It also helps prepare the environment: clearing storage, setting a feature flag, scrolling inside a virtualised list.

Приклад. Тест кошика перевіряє, що після додавання товару в localStorage з’явився ключ cart з однією позицією. WebDriver такого читати не вміє, а один executeScript повертає рядок JSON, який далі парситься й перевіряється. Сама дія «додати в кошик» при цьому виконується справжнім кліком.

Example. A cart test checks that after adding a product localStorage holds a cart key with one line item. WebDriver cannot read that, while one executeScript returns a JSON string that is then parsed and asserted. The add-to-cart action itself is still done with a real click.

Підводні камені. Зловживання — робити через JS те, що має робити користувач: записувати value напряму чи ставити checked = true. React і Vue слухають події input і change, тому стан компонента не зміниться, форма відправить старе значення, а тест матиме хибну картину. Ще одна пастка — повертати з JS великі структури: кожен виклик — це мережевий запит і серіалізація.

Pitfalls. Abuse means doing through JS what the user is supposed to do: writing value directly or setting checked = true. React and Vue listen for input and change events, so the component state does not change, the form submits the old value, and the test paints a false picture. Another trap is returning large structures from JS: every call is a network request plus serialisation.

Уточнення. Питають і про executeAsyncScript. Там останній аргумент — колбек, який скрипт має викликати, інакше спрацює таймаут scriptTimeout з driver.manage().timeouts().

Follow-up. executeAsyncScript also comes up. Its last argument is a callback the script must call, otherwise the scriptTimeout from driver.manage().timeouts() fires.

Читання стану через JS і антиприклад
Reading state through JS, and an anti-example
JavascriptExecutor js = (JavascriptExecutor) driver;

// дія — справжнім кліком, як у користувача
driver.findElement(By.cssSelector("[data-testid='add-to-cart']")).click();

// перевірка стану, недоступного в WebDriver API
String cart = (String) js.executeScript("return window.localStorage.getItem('cart');");
assertTrue(cart.contains("\"qty\":1"));

Long scrollTop = (Long) js.executeScript(
        "return Math.round(arguments[0].scrollTop);",
        driver.findElement(By.id("order-list")));

// антиприклад: поле змінено, але подія input не спрацювала
WebElement email = driver.findElement(By.id("email"));
js.executeScript("arguments[0].value = arguments[1];", email, "qa@example.com");
// форма все одно відправить старе значення з React-стану
JavascriptExecutor js = (JavascriptExecutor) driver;

// the action is a real click, like a user would do
driver.findElement(By.cssSelector("[data-testid='add-to-cart']")).click();

// checking state that the WebDriver API does not expose
String cart = (String) js.executeScript("return window.localStorage.getItem('cart');");
assertTrue(cart.contains("\"qty\":1"));

Long scrollTop = (Long) js.executeScript(
        "return Math.round(arguments[0].scrollTop);",
        driver.findElement(By.id("order-list")));

// anti-example: the field changes but no input event fires
WebElement email = driver.findElement(By.id("email"));
js.executeScript("arguments[0].value = arguments[1];", email, "qa@example.com");
// the form still submits the old value from React state
🔧 Розбір синтаксису: Драйвер приводиться до JavascriptExecutor, бо цей інтерфейс реалізують усі стандартні драйвери. return у скрипті обов’язковий, інакше Java отримає null. Цілі числа приходять як Long, тому Math.round гарантує тип. Аргументи після рядка скрипту потрапляють у масив arguments у тому ж порядку.
🔧 Syntax breakdown: The driver is cast to JavascriptExecutor, since every standard driver implements that interface. return inside the script is required, otherwise Java gets null. Whole numbers arrive as Long, so Math.round pins the type. Arguments after the script string land in the arguments array in the same order.
💡 Простими словами: JS у тесті — щоб підглянути або підготувати, а не щоб зробити дію замість користувача.
💡 In plain words: JS in a test is for peeking or preparing, not for performing the action in place of the user.
🗣 Як читати вголос Для мене JavascriptExecutor — інструмент спостереження і підготовки. Я читаю через нього те, чого немає у WebDriver: localStorage, readyState, прапорець готовності застосунку, прокрутку контейнера. Інколи чищу сховище перед тестом. А дії користувача — клік, введення, вибір — роблю лише через звичайний API. Якщо записати value через JS, React не отримає подію input, і тест перевірятиме стан, якого в реальності немає. Ще стежу, щоб з JS не тягнути великі об’єкти, бо кожен виклик іде по мережі.
🗣 How to say it out loud To me JavascriptExecutor is a tool for observing and preparing. I use it to read what WebDriver does not give me: localStorage, readyState, an app readiness flag, the scroll of a container. Sometimes I clear storage before a test. User actions, though — clicking, typing, choosing — go only through the normal API. If I write value through JS, React never gets the input event, and the test checks a state that does not exist for a real user. I also avoid pulling big objects out of JS, since every call travels over the network.
18 Чому клік через JavaScript може приховати справжній баг і що робити з ElementClickInterceptedException? Why can a JavaScript click hide a real bug, and what should you do about ElementClickInterceptedException?
ВзаємодіяInteraction Middle ▶

Що це. Звичайний click() у WebDriver прокручує елемент у видиму зону, перевіряє, що саме він отримає клік у своїй центральній точці, і відтворює повну послідовність подій миші. JS-клік arguments[0].click() просто генерує подію click на вузлі, пропускаючи всі ці перевірки. Він спрацює навіть на елементі, який перекритий, поза екраном чи нульового розміру.

What it is. A normal WebDriver click() scrolls the element into view, checks that this very element would receive the click at its centre point, and plays the full sequence of mouse events. A JS click, arguments[0].click(), just dispatches a click event on the node and skips all those checks. It works even on an element that is covered, off screen or zero-sized.

Навіщо. Перевірки WebDriver — це частина тесту: вони моделюють, чи зможе людина натиснути кнопку. Якщо поверх «Оформити замовлення» висить банер cookie, користувач кнопку не натисне, і це реальний дефект. ElementClickInterceptedException з текстом «Other element would receive the click» — корисний сигнал, який прямо називає елемент-перекривач.

Why it matters. WebDriver checks are part of the test: they model whether a person could press the button. If a cookie banner hangs over Place order, the user cannot press it, and that is a real defect. ElementClickInterceptedException with the text Other element would receive the click is a useful signal that names the covering element outright.

Приклад. Після релізу тести почали падати на кроці оплати з цим винятком. Хтось замінив клік на JS — тести позеленіли. За тиждень прийшли скарги: на мобільних ширинах банер згоди перекривав кнопку без можливості закрити. Правильна реакція на виняток — прочитати, хто перехоплює клік, і вирішити, це баг продукту чи стан тесту, який треба підготувати.

Example. After a release the tests began failing at the payment step with this exception. Someone replaced the click with a JS one and the tests went green. A week later complaints arrived: at mobile widths the consent banner covered the button and could not be closed. The right response to the exception is to read which element intercepts the click and decide whether it is a product bug or test state that needs preparing.

Підводні камені. Якщо перекривач очікуваний — спінер чи анімація модалки, — треба чекати його зникнення через invisibilityOfElementLocated. Якщо це банер згоди, його закривають один раз у підготовці або виставляють cookie згоди до відкриття сторінки. Також JS-клік не надсилає pointerdown і mousedown, тож компоненти, які слухають саме їх, поводяться інакше, ніж у людини.

Pitfalls. When the cover is expected — a spinner or a modal animation — wait for it to go away with invisibilityOfElementLocated. When it is a consent banner, close it once during setup or set the consent cookie before opening the page. A JS click also sends no pointerdown or mousedown, so components listening for those behave differently from a real user.

Уточнення. Чи буває JS-клік виправданим? Рідко: наприклад, для прихованого технічного елемента, з яким людина не взаємодіє напряму. Але таке місце варто позначити коментарем із причиною, щоб його не копіювали як «фікс» для нестабільних кліків.

Follow-up. Is a JS click ever justified? Rarely: for a hidden technical element a person never interacts with directly, for instance. Even then mark the spot with a comment stating the reason, so nobody copies it as a fix for unstable clicks.

Перекритий банером клік: погана і правильна реакція
A click covered by a banner: the bad and the right response
By placeOrder = By.cssSelector("[data-testid='place-order']");

// погано: тест зелений, хоча людина кнопку не натисне
// ((JavascriptExecutor) driver).executeScript("arguments[0].click();", driver.findElement(placeOrder));

// добре: прибрати очікуваний перекривач у підготовці тесту
driver.manage().addCookie(new Cookie("cookie_consent", "accepted"));
driver.navigate().refresh();

wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".spinner")));
try {
    driver.findElement(placeOrder).click();
} catch (ElementClickInterceptedException e) {
    // повідомлення містить: Other element would receive the click: <div class="cookie-banner">
    fail("Place order is covered by another element: " + e.getMessage());
}
By placeOrder = By.cssSelector("[data-testid='place-order']");

// bad: the test is green although a person cannot press the button
// ((JavascriptExecutor) driver).executeScript("arguments[0].click();", driver.findElement(placeOrder));

// good: remove the expected cover while preparing the test
driver.manage().addCookie(new Cookie("cookie_consent", "accepted"));
driver.navigate().refresh();

wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".spinner")));
try {
    driver.findElement(placeOrder).click();
} catch (ElementClickInterceptedException e) {
    // the message includes: Other element would receive the click: <div class="cookie-banner">
    fail("Place order is covered by another element: " + e.getMessage());
}
🔧 Розбір синтаксису: Закоментований рядок — типовий JS-клік через executeScript. addCookie працює лише на вже відкритому домені, тому після нього refresh() перечитує сторінку з cookie згоди. invisibilityOfElementLocated повертає true, коли елемент зник або його немає в DOM. fail(...) з JUnit 5 валить тест з поясненням, куди підставлено текст винятку.
🔧 Syntax breakdown: The commented-out line is a typical JS click through executeScript. addCookie only works on a domain that is already open, so refresh() afterwards reloads the page with the consent cookie. invisibilityOfElementLocated returns true once the element is hidden or gone from the DOM. JUnit 5 fail(...) fails the test with an explanation that includes the exception text.
💡 Простими словами: Виняток про перехоплений клік — не завада, а знахідка. JS-клік просто вимикає сигналізацію.
💡 In plain words: An intercepted-click exception is not a nuisance, it is a finding. A JS click just switches the alarm off.
🗣 Як читати вголос Я не лагоджу нестабільні кліки через JavaScript, і пояснюю чому. Звичайний click перевіряє, що елемент видимий і саме він отримає клік, а JS-клік це все пропускає. У мене був випадок, коли банер згоди на мобільній ширині перекривав кнопку оплати: тест з JS-кліком був зеленим, а користувачі не могли купити. Тепер ElementClickInterceptedException для мене — підказка: у повідомленні видно, хто перехопив клік. Далі вирішую, це баг чи треба підготувати стан, наприклад поставити cookie згоди.
🗣 How to say it out loud I do not fix flaky clicks with JavaScript, and I can explain why. A normal click checks that the element is visible and is the one that gets the click; a JS click skips all of that. I once had a consent banner covering the pay button at mobile width: the test with a JS click was green while users could not buy anything. So ElementClickInterceptedException is a hint for me, because the message shows what intercepted the click. Then I decide whether it is a bug or state I should prepare, such as setting the consent cookie.
▶ 4 7
19 Чому Thread.sleep у тестах — погана ідея і чим його замінити? Why is Thread.sleep a bad idea in tests, and what should replace it?
ОчікуванняWaits Junior ▶

Що це. Thread.sleep(5000) — фіксована пауза: потік просто спить п’ять секунд, нічого не знаючи про стан сторінки. Він не перевіряє, чи з’явилась кнопка, чи зник спінер, чи прийшла відповідь від API. Пауза або задовга, або закоротка — влучити точно неможливо.

What it is. Thread.sleep(5000) is a fixed pause: the thread sleeps for five seconds and knows nothing about the page. It does not check whether the button appeared, the spinner went away or the API answered. The pause is always either too long or too short, and you cannot hit the right value.

Навіщо. Якщо сторінка готова за 300 мс, тест все одно чекає 5 секунд; помножте на сотні кроків — і прогін замість 10 хвилин триває годину. Якщо ж на CI сервер відповів за 6 секунд, пауза закінчилась раніше, і тест падає з NoSuchElementException. Так народжуються flaky-тести, які «лікують», збільшуючи число, і робиться лише гірше.

Why it matters. If the page is ready in 300 ms, the test still waits 5 seconds; multiply that by hundreds of steps and a 10-minute run takes an hour. If the CI server needs 6 seconds, the pause ends too early and the test fails with NoSuchElementException. That is how flaky tests are born, and bumping the number only makes the suite slower without making it reliable.

Приклад. Після кліку «Зберегти» замість сну чекаємо на конкретну подію: wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".toast-success"))). WebDriverWait опитує умову кожні 500 мс і повертає керування, щойно вона виконалась, тому швидке оточення проходить швидко, а повільне отримує запас до таймауту.

Example. After clicking Save, instead of sleeping we wait for a concrete event: wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".toast-success"))). WebDriverWait polls the condition every 500 ms and returns as soon as it holds, so a fast environment finishes quickly while a slow one still gets headroom up to the timeout.

Підводні камені. Сон часто ховається в хелперах на кшталт sleepAbit() або в «тимчасовому» фіксі, що живе роками. Інша пастка — замінити сон на явне очікування неправильної умови: чекати на присутність кнопки, коли насправді треба дочекатись, поки зникне оверлей. Очікування має описувати саме той стан, від якого залежить наступний крок.

Pitfalls. Sleeps like to hide in helpers such as sleepAbit() or in a temporary fix that survives for years. Another trap is swapping the sleep for an explicit wait on the wrong condition: waiting for the button to be present when the real blocker is an overlay that has not disappeared yet. The wait must describe exactly the state the next step depends on.

Уточнення. Чи буває сон виправданим? Хіба в разовому діагностичному скрипті або коли стан узагалі неможливо спостерігати, наприклад анімація без жодної зміни в DOM. Навіть тоді краще винести його в один метод з поясненням і тікетом, ніж розкидати по тестах.

Follow-up. Is a sleep ever acceptable? Perhaps in a one-off debugging script, or when the state truly cannot be observed, such as an animation that changes nothing in the DOM. Even then, keep it in a single method with a comment and a ticket rather than scattering it across tests.

Фіксована пауза проти очікування на стан
A fixed pause versus waiting for a state
@Test
void savesProfile() {
    driver.findElement(By.id("save")).click();

    // Погано: завжди 5 с, і все одно може бути замало
    // Thread.sleep(5000);

    // Добре: повертається, щойно тост з'явився, максимум 10 с
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    WebElement toast = wait.until(
        ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".toast-success")));

    assertEquals("Profile saved", toast.getText());
}
@Test
void savesProfile() {
    driver.findElement(By.id("save")).click();

    // Bad: always 5 s, and it may still be too short
    // Thread.sleep(5000);

    // Good: returns as soon as the toast is visible, 10 s at most
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    WebElement toast = wait.until(
        ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".toast-success")));

    assertEquals("Profile saved", toast.getText());
}
🔧 Розбір синтаксису: new WebDriverWait(driver, Duration.ofSeconds(10)) — конструктор Selenium 4 з Duration; старий варіант з числом секунд застарів. wait.until(...) повторює умову, доки вона не поверне не-null значення, і віддає це значення — тут знайдений WebElement. Закоментований Thread.sleep ще й кидає перевірений InterruptedException, що змушує засмічувати сигнатури тестів.
🔧 Syntax breakdown: new WebDriverWait(driver, Duration.ofSeconds(10)) is the Selenium 4 constructor taking a Duration; the old seconds-as-number variant is deprecated. wait.until(...) retries the condition until it returns a non-null value and hands that value back, here the found WebElement. The commented-out Thread.sleep also throws the checked InterruptedException, which clutters test signatures.
💡 Простими словами: Сон питає «скільки часу минуло?», а очікування питає «чи сталося те, що мені потрібно?». Тестам цікаве лише друге.
💡 In plain words: A sleep asks how much time has passed; a wait asks whether the thing I need has happened. Tests only care about the second question.
🗣 Як читати вголос Я не використовую Thread.sleep у тестах, бо це ставка навмання: на швидкому стенді я марную секунди, на повільному все одно падаю. Замість цього я формулюю, чого саме чекаю, — появи тосту, зникнення спінера, зміни URL — і кладу це в WebDriverWait з розумним таймаутом. Він опитує умову і відпускає тест одразу, як вона виконалась. Якщо в чужому коді бачу сон, я не збільшую число, а шукаю реальну подію, від якої залежить наступний крок.
🗣 How to say it out loud I keep Thread.sleep out of tests because it is a blind bet: on a fast environment I waste seconds, on a slow one I still fail. Instead I spell out what I am actually waiting for, a toast appearing, a spinner going away, the URL changing, and put that into a WebDriverWait with a sensible timeout. It polls the condition and releases the test the moment it holds. When I find a sleep in existing code, I do not raise the number, I look for the real event the next step depends on.
20 Як працює implicit wait, чим він небезпечний і чому його не можна змішувати з explicit wait? How does an implicit wait work, why is it risky, and why must it not be mixed with explicit waits?
ОчікуванняWaits Middle ▶

Що це. driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)) — налаштування всієї сесії: кожен findElement і findElements драйвер повторює до 10 секунд, поки не знайде хоча б один елемент. Очікування живе на боці драйвера, а не в Java-коді, і стосується лише пошуку, а не видимості чи клікабельності.

What it is. driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)) is a session-wide setting: every findElement and findElements is retried by the driver for up to 10 seconds until at least one element is found. The wait lives on the driver side, not in Java code, and it covers only lookup, not visibility or clickability.

Навіщо. Implicit wait здається зручним: одна строка — і NoSuchElement зникає. Але він не вміє чекати на видимість, текст чи зникнення елемента, а перевірка відсутності стає дорогою: findElements(...).isEmpty() щоразу спалює всі 10 секунд, перш ніж повернути порожній список.

Why it matters. An implicit wait feels convenient: one line and NoSuchElement disappears. But it cannot wait for visibility, text or disappearance, and checking absence becomes expensive: findElements(...).isEmpty() burns the full 10 seconds every time before returning an empty list.

Приклад. Implicit 10 с плюс WebDriverWait на 15 с з умовою visibilityOfElementLocated. Кожне опитування явного очікування саме викликає findElement, який може блокуватись до 10 с. Якщо чергова спроба стартувала на 14-й секунді, вона завершиться лише на 24-й, і таймаут спрацює приблизно там. Документація Selenium наводить саме такий приклад: 10 + 15 може дати падіння через 20 секунд, тому прямо радить не змішувати.

Example. Implicit 10 s plus a 15 s WebDriverWait on visibilityOfElementLocated. Each poll of the explicit wait calls findElement itself, and that call can block for up to 10 s. If a poll starts at second 14, it only returns at second 24, and the timeout fires around then. The Selenium documentation uses this exact example, noting that 10 plus 15 can time out after 20 seconds, and plainly advises against mixing.

Підводні камені. invisibilityOfElementLocated з увімкненим implicit wait повертає true лише після того, як пошук вичерпає свої 10 секунд, — тест «чекає, що спінер зник» довше, ніж сам спінер існував. Ще гірше, коли хтось вмикає implicit wait у базовому класі, а інший автор про це не знає і дивується розкиду часу.

Pitfalls. With an implicit wait on, invisibilityOfElementLocated returns true only after the lookup has used up its 10 seconds, so a test waiting for a spinner to vanish waits longer than the spinner ever existed. It gets worse when someone enables the implicit wait in a base class and another author, unaware of it, is puzzled by the timing spread.

Уточнення. Практичне правило: implicit wait дорівнює нулю (це і є значення за замовчуванням), а всі очікування явні і локальні для кроку. Якщо легасі-фреймворк тримається на implicit wait, міграцію роблять поступово: обнуляють його і замінюють пошуки на явні очікування в Page Object.

Follow-up. The working rule: keep the implicit wait at zero, which is already the default, and make every wait explicit and local to its step. If a legacy framework relies on an implicit wait, migrate gradually: set it to zero and replace lookups in page objects with explicit waits.

Звідки береться «непередбачуваний» таймаут
Where the unpredictable timeout comes from
// Налаштування на всю сесію: кожен пошук чекає до 10 с
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
long start = System.currentTimeMillis();
try {
    // кожне опитування саме може висіти до 10 с усередині findElement
    wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("report")));
} catch (TimeoutException e) {
    // тут часто буде 20-25 с, а не 15
    System.out.println("Waited ms: " + (System.currentTimeMillis() - start));
}

// Перевірка відсутності «коштує» повний implicit wait
boolean gone = driver.findElements(By.cssSelector(".spinner")).isEmpty();

// Рішення: implicit = 0, лише явні очікування
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
// Session-wide setting: every lookup waits up to 10 s
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
long start = System.currentTimeMillis();
try {
    // every poll can itself hang for up to 10 s inside findElement
    wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("report")));
} catch (TimeoutException e) {
    // this often prints 20-25 s, not 15
    System.out.println("Waited ms: " + (System.currentTimeMillis() - start));
}

// Checking absence costs the full implicit wait
boolean gone = driver.findElements(By.cssSelector(".spinner")).isEmpty();

// The fix: implicit = 0, explicit waits only
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
🔧 Розбір синтаксису: manage().timeouts().implicitlyWait(...) приймає Duration у Selenium 4 і діє до кінця сесії або до наступного виклику. Duration.ZERO вимикає implicit wait. findElements при порожньому результаті не кидає виняток, але повертає список лише після спливання implicit wait. Блок try/catch з System.currentTimeMillis() тут лише для того, щоб побачити реальну тривалість.
🔧 Syntax breakdown: manage().timeouts().implicitlyWait(...) takes a Duration in Selenium 4 and stays in force for the rest of the session or until the next call. Duration.ZERO turns the implicit wait off. findElements never throws on an empty result, but it returns the list only after the implicit wait has expired. The try/catch with System.currentTimeMillis() is there only to show the real duration.
💡 Простими словами: Implicit wait — це таймер, захований у кожному пошуку. Коли явне очікування ще й саме шукає елементи, таймери складаються.
💡 In plain words: An implicit wait is a timer hidden inside every lookup. When an explicit wait performs lookups too, the timers add up.
🗣 Як читати вголос Implicit wait я тримаю на нулі. Він діє на всю сесію і змушує кожен пошук повторюватись до заданого часу, але нічого не знає про видимість чи зникнення. Найнеприємніше — змішування з явним очікуванням: всередині кожної спроби WebDriverWait знову працює implicit wait, і при десяти плюс п’ятнадцяти секундах тест може впасти десь на двадцятій. Ще й перевірка, що спінера немає, щоразу коштує повні десять секунд. Тому в мене всі очікування явні й описують конкретний стан.
🗣 How to say it out loud I keep the implicit wait at zero. It applies to the whole session and makes every lookup retry up to the set time, yet it knows nothing about visibility or disappearance. The nasty part is mixing it with explicit waits: inside every WebDriverWait attempt the implicit wait runs again, so with ten plus fifteen seconds a test may fail somewhere around twenty. On top of that, confirming a spinner is gone costs the full ten seconds each time. So all my waits are explicit and name a concrete state.
21 Як працюють WebDriverWait і ExpectedConditions, як написати власну умову і що робити з TimeoutException? How do WebDriverWait and ExpectedConditions work, how do you write a custom condition, and what do you do with a TimeoutException?
ОчікуванняWaits Middle ▶

Що це. WebDriverWait — явне очікування: воно викликає умову кожні 500 мс (за замовчуванням), доки та не поверне значення, відмінне від null і false, або доки не мине таймаут. ExpectedConditions — набір готових умов: visibilityOfElementLocated, elementToBeClickable, textToBePresentInElementLocated, urlContains, numberOfElementsToBe тощо.

What it is. WebDriverWait is an explicit wait: it calls a condition every 500 ms by default until the condition returns something other than null or false, or until the timeout runs out. ExpectedConditions is a set of ready-made conditions: visibilityOfElementLocated, elementToBeClickable, textToBePresentInElementLocated, urlContains, numberOfElementsToBe and more.

Навіщо. Явне очікування прив’язане до конкретного кроку і конкретного стану, тому тест чекає рівно стільки, скільки треба. Коли готової умови бракує, пишуть лямбду: ExpectedCondition — це по суті функція від WebDriver, тож wait.until(d -> ...) приймає будь-який вираз.

Why it matters. An explicit wait is tied to one step and one state, so the test waits exactly as long as needed. When no ready condition fits, you write a lambda: an ExpectedCondition is essentially a function of WebDriver, so wait.until(d -> ...) accepts any expression.

Приклад. Таблиця замовлень підвантажується порціями, і нам потрібно дочекатись рівно 20 рядків. Готової умови «рівно N і текст першого рядка такий-то» немає, тому пишемо лямбду, яка повертає Boolean. Якщо умова так і не справдилась, until кидає TimeoutException з текстом на кшталт «Expected condition failed: waiting for ... (tried for 10 second(s) with 500 milliseconds interval)».

Example. An orders table loads in chunks and we need exactly 20 rows. There is no built-in condition for exactly N rows with a given first-row text, so we write a lambda returning a Boolean. If the condition never becomes true, until throws a TimeoutException with text like: Expected condition failed: waiting for ... (tried for 10 second(s) with 500 milliseconds interval).

Підводні камені. Для лямбди опис у повідомленні буде невиразним, тому додавайте withMessage(...) — у звіті з’явиться зрозуміла причина. Не ловіть TimeoutException, щоб «продовжити тест»: це приховує дефект. Виняток ловлять лише тоді, коли відсутність стану — очікуваний сценарій, або щоб додати контекст і кинути далі. І пам’ятайте, що StaleElementReferenceException за замовчуванням не ігнорується.

Pitfalls. A lambda has no readable description in that message, so add withMessage(...) and the report will state a clear reason. Do not catch TimeoutException just to let the test carry on: that hides a defect. Catch it only when the missing state is a legitimate scenario, or to add context and rethrow. And remember that StaleElementReferenceException is not ignored by default.

Уточнення. Що саме ігнорує WebDriverWait? Конструктор додає NotFoundException, тож NoSuchElementException під час опитування не перериває очікування. Інші винятки можна додати через ignoring(...).

Follow-up. What does WebDriverWait ignore out of the box? Its constructor adds NotFoundException, so a NoSuchElementException during polling does not break the wait. Other exceptions can be added with ignoring(...).

Готова умова, лямбда і читабельна помилка
A built-in condition, a lambda and a readable failure
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

// Готова умова: чекаємо, що кнопка видима і активна
wait.until(ExpectedConditions.elementToBeClickable(By.id("load-orders"))).click();

// Власна умова: рівно 20 рядків і перший — потрібне замовлення
wait.withMessage("очікували 20 рядків, першим ORD-1001")
    .until(d -> {
        List<WebElement> rows = d.findElements(By.cssSelector("table.orders tbody tr"));
        return rows.size() == 20 && rows.get(0).getText().startsWith("ORD-1001");
    });

// TimeoutException ловимо лише для очікуваного сценарію
try {
    wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".promo")));
} catch (TimeoutException e) {
    // банер показується не всім, це не дефект
}
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

// Built-in condition: wait until the button is visible and enabled
wait.until(ExpectedConditions.elementToBeClickable(By.id("load-orders"))).click();

// Custom condition: exactly 20 rows and the first one is the right order
wait.withMessage("expected 20 rows, first one ORD-1001")
    .until(d -> {
        List<WebElement> rows = d.findElements(By.cssSelector("table.orders tbody tr"));
        return rows.size() == 20 && rows.get(0).getText().startsWith("ORD-1001");
    });

// Catch TimeoutException only for a legitimate scenario
try {
    wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".promo")));
} catch (TimeoutException e) {
    // the banner is not shown to every user, not a defect
}
🔧 Розбір синтаксису: until повертає результат умови: для elementToBeClickable це WebElement, тому одразу можна викликати click(). d -> {...} — лямбда, де d — той самий WebDriver; вона повертає Boolean, і очікування триває, поки результат false. withMessage змінює той самий об’єкт очікування і додає текст до повідомлення TimeoutException.
🔧 Syntax breakdown: until returns the condition result: for elementToBeClickable that is a WebElement, so click() can be chained. d -> {...} is a lambda where d is the same WebDriver; it returns a Boolean and the wait keeps going while the result is false. withMessage modifies the same wait object and adds its text to the TimeoutException message.
💡 Простими словами: Явне очікування — це «перевіряй кожні пів секунди, доки не стане правдою, але не довше за N». Лямбда дозволяє описати будь-яку «правду».
💡 In plain words: An explicit wait means: check every half second until it is true, but no longer than N. A lambda lets you define any kind of true.
🗣 Як читати вголос Для очікувань я беру WebDriverWait з таймаутом через Duration. Якщо є готова умова в ExpectedConditions, наприклад видимість чи клікабельність, використовую її, а коли ні — пишу лямбду, що повертає true, коли стан досягнуто: скажімо, у таблиці рівно двадцять рядків. Обов’язково додаю withMessage, щоб у звіті було видно, чого саме не дочекались. TimeoutException я не ковтаю — ловлю лише там, де відсутність елемента є нормальним сценарієм.
🗣 How to say it out loud For waiting I use WebDriverWait with a Duration timeout. If ExpectedConditions already has what I need, visibility or clickability for example, I take it; if not, I write a lambda that returns true once the state is reached, say the table has exactly twenty rows. I always add withMessage so the report shows what we were waiting for. I do not swallow a TimeoutException; I only catch it where a missing element is a normal scenario.
22 Чим відрізняються presenceOfElementLocated, visibilityOfElementLocated і elementToBeClickable? What is the difference between presenceOfElementLocated, visibilityOfElementLocated and elementToBeClickable?
ОчікуванняWaits Junior ▶

Що це. Це три дедалі суворіші умови. presenceOfElementLocated справджується, щойно елемент є в DOM, навіть прихований через display: none. visibilityOfElementLocated вимагає ще й відображення: елемент має бути видимий і мати ненульові розміри. elementToBeClickable додає перевірку isEnabled(), тобто видимий і не заблокований.

What it is. These are three increasingly strict conditions. presenceOfElementLocated holds as soon as the element exists in the DOM, even if display: none hides it. visibilityOfElementLocated also requires the element to be displayed with a non-zero size. elementToBeClickable adds an isEnabled() check, meaning visible and not disabled.

Навіщо. Вибір умови залежить від наступної дії. Щоб прочитати атрибут прихованого поля або переконатись, що рядок додано в DOM, досить присутності. Щоб прочитати текст, який бачить користувач, потрібна видимість, бо getText() прихованого елемента повертає порожній рядок. Перед кліком логічно чекати клікабельність.

Why it matters. The right condition depends on the next action. Presence is enough to read an attribute of a hidden field or to confirm a row was added to the DOM. To read text the user sees you need visibility, because getText() on a hidden element returns an empty string. Before a click it makes sense to wait for clickability.

Приклад. Модальне вікно рендериться в DOM одразу, але показується з анімацією. Умова присутності поверне елемент миттєво, і клік по кнопці всередині впаде з ElementNotInteractableException. З elementToBeClickable тест дочекається, поки кнопка стане видимою і активною, і лише тоді клікне.

Example. A modal is rendered into the DOM immediately but shown with an animation. The presence condition returns the element at once, and a click on a button inside fails with ElementNotInteractableException. With elementToBeClickable the test waits until the button is visible and enabled and only then clicks.

Підводні камені. Назва elementToBeClickable обманлива: вона не перевіряє, чи не перекриває елемент інший шар. Якщо над кнопкою ще висить напівпрозорий оверлей або спінер, умова скаже «готово», а клік завершиться ElementClickInterceptedException. Тоді треба окремо чекати зникнення оверлею через invisibilityOfElementLocated.

Pitfalls. The name elementToBeClickable is misleading: it does not check whether another layer covers the element. If a semi-transparent overlay or spinner still sits above the button, the condition says ready and the click ends with ElementClickInterceptedException. In that case you wait separately for the overlay to disappear with invisibilityOfElementLocated.

Уточнення. Що вважається видимим? Selenium застосовує власний алгоритм відображення: враховує display, visibility, opacity та розміри, але елемент за межами екрана все одно може бути видимим, і перед кліком драйвер сам прокрутить до нього.

Follow-up. What counts as visible? Selenium uses its own displayedness algorithm that looks at display, visibility, opacity and size, yet an element outside the viewport can still be visible, and the driver scrolls to it before clicking.

Три умови і оверлей, якого вони не бачать
Three conditions and the overlay they do not see
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

// є в DOM, навіть якщо прихований
WebElement csrf = wait.until(
    ExpectedConditions.presenceOfElementLocated(By.name("csrf")));

// видимий: можна читати текст, який бачить користувач
String title = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".modal h2"))).getText();

// видимий і enabled, але перекриття оверлеєм не перевіряється
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".overlay")));
wait.until(ExpectedConditions.elementToBeClickable(By.id("confirm"))).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

// in the DOM, even if hidden
WebElement csrf = wait.until(
    ExpectedConditions.presenceOfElementLocated(By.name("csrf")));

// visible: safe to read the text the user sees
String title = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".modal h2"))).getText();

// visible and enabled, but being covered by an overlay is not checked
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".overlay")));
wait.until(ExpectedConditions.elementToBeClickable(By.id("confirm"))).click();
🔧 Розбір синтаксису: Усі три умови приймають By і повертають WebElement, тому результат можна одразу використати: .getText() чи .click(). invisibilityOfElementLocated повертає Boolean і справджується, коли елемент прихований або його немає в DOM. Порядок рядків важливий: спершу чекаємо зникнення оверлею, потім клікабельність кнопки.
🔧 Syntax breakdown: All three conditions take a By and return a WebElement, so the result can be used straight away with .getText() or .click(). invisibilityOfElementLocated returns a Boolean and holds when the element is hidden or absent from the DOM. The order of the lines matters: first wait for the overlay to go, then for the button to be clickable.
💡 Простими словами: Присутність — «є в коді сторінки», видимість — «видно на екрані», клікабельність — «видно і не заблоковано». Про те, що зверху щось лежить, жодна з них не знає.
💡 In plain words: Presence means it is in the page code, visibility means you can see it, clickable means you can see it and it is not disabled. None of them knows that something is lying on top.
🗣 Як читати вголос Я розрізняю ці умови за суворістю. Presence — елемент просто є в DOM, цього вистачає для прихованих полів чи атрибутів. Visibility — його ще й видно, і це мінімум, якщо я читаю текст. Clickable додає перевірку, що елемент enabled. Важливий нюанс, на якому часто ловлять: clickable не знає про оверлеї, тож якщо зверху висить спінер, клік однаково перехоплять. Тому перед такими кліками я окремо чекаю, поки оверлей стане невидимим.
🗣 How to say it out loud I rank these conditions by strictness. Presence means the element is merely in the DOM, which is fine for hidden fields or attributes. Visibility means it is also shown, and that is the minimum when I read text. Clickable adds the enabled check. The nuance interviewers like to probe is that clickable knows nothing about overlays, so if a spinner still covers the button the click gets intercepted anyway. That is why before such clicks I wait separately for the overlay to become invisible.
23 Що таке FluentWait, які в нього налаштування і коли він кращий за звичайний WebDriverWait? What is FluentWait, what can you configure on it, and when is it better than a plain WebDriverWait?
ОчікуванняWaits Middle ▶

Що це. FluentWait<T> — загальний клас очікування, де все налаштовується явно: withTimeout задає загальний ліміт, pollingEvery — інтервал між спробами, ignoring — винятки, які не переривають очікування, withMessage — текст помилки. WebDriverWait є його підкласом із типом WebDriver, опитуванням кожні 500 мс і вже доданим NotFoundException.

What it is. FluentWait<T> is the generic wait class where everything is set explicitly: withTimeout sets the overall limit, pollingEvery the gap between attempts, ignoring the exceptions that do not break the wait, and withMessage the failure text. WebDriverWait is its subclass typed on WebDriver, polling every 500 ms with NotFoundException already ignored.

Навіщо. FluentWait потрібен, коли дефолти не підходять. Наприклад, віджет курсу валют перемальовується кожну секунду, і посилання на елемент постійно застаріває: тоді треба ігнорувати StaleElementReferenceException. Або стан змінюється за 50–100 мс, і опитування раз на пів секунди додає зайву затримку до кожного кроку.

Why it matters. FluentWait is for cases where the defaults do not fit. For instance, an exchange-rate widget re-renders every second and the element reference keeps going stale, so StaleElementReferenceException must be ignored. Or the state changes within 50 to 100 ms, and polling every half second adds needless delay to each step.

Приклад. Чекаємо, поки лічильник кошика покаже «3», а сам бейдж React перестворює при кожному оновленні. FluentWait з таймаутом 15 с, опитуванням кожні 200 мс, ігноруванням NoSuchElementException і StaleElementReferenceException переживе і момент, коли бейджа ще немає, і момент, коли його замінили новим вузлом.

Example. We wait for the cart counter to read 3, while React recreates the badge on every update. A FluentWait with a 15 s timeout, 200 ms polling and both NoSuchElementException and StaleElementReferenceException ignored survives both the moment the badge is missing and the moment it has been replaced by a new node.

Підводні камені. Надто часте опитування — наприклад, кожні 10 мс — засипає драйвер запитами, особливо через Grid, і робить прогін повільнішим, а не швидшим. Ігнорування надто широкого винятку, скажімо WebDriverException, маскує справжні збої на кшталт впалої сесії: тест мовчки чекає до таймауту замість миттєвого падіння з причиною.

Pitfalls. Polling too often, every 10 ms for example, floods the driver with requests, especially through Grid, and makes the run slower rather than faster. Ignoring an overly broad exception such as WebDriverException masks real failures like a dead session: the test silently waits until the timeout instead of failing at once with the cause.

Уточнення. Оскільки тип узагальнений, FluentWait можна застосувати не лише до драйвера: new FluentWait<>(element) або навіть до шляху файлу, щоб дочекатись завершення завантаження на диск.

Follow-up. Because the type is generic, FluentWait is not limited to the driver: new FluentWait<>(element) works, and so does a file path when you wait for a download to finish on disk.

FluentWait для бейджа, який постійно перемальовується
FluentWait for a badge that keeps re-rendering
Wait<WebDriver> wait = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(15))     // загальний ліміт
    .pollingEvery(Duration.ofMillis(200))    // інтервал між спробами
    .ignoring(NoSuchElementException.class)  // бейджа ще немає
    .ignoring(StaleElementReferenceException.class) // бейдж перестворили
    .withMessage("лічильник кошика не показав 3");

// кожна спроба шукає елемент заново, тож старе посилання не заважає
String count = wait.until(d -> {
    String text = d.findElement(By.cssSelector("[data-testid=cart-count]")).getText();
    return text.equals("3") ? text : null;
});
Wait<WebDriver> wait = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(15))     // overall limit
    .pollingEvery(Duration.ofMillis(200))    // gap between attempts
    .ignoring(NoSuchElementException.class)  // badge not rendered yet
    .ignoring(StaleElementReferenceException.class) // badge was recreated
    .withMessage("cart counter never showed 3");

// every attempt looks the element up again, so a stale reference does no harm
String count = wait.until(d -> {
    String text = d.findElement(By.cssSelector("[data-testid=cart-count]")).getText();
    return text.equals("3") ? text : null;
});
🔧 Розбір синтаксису: Wait<WebDriver> — інтерфейс, який реалізують і FluentWait, і WebDriverWait. Методи налаштування повертають той самий об’єкт, тому їх можна ланцюжити. ignoring приймає клас винятку; його можна викликати кілька разів. Лямбда повертає рядок або null: null означає «ще ні», а непорожнє значення завершує очікування і повертається з until.
🔧 Syntax breakdown: Wait<WebDriver> is the interface that both FluentWait and WebDriverWait implement. The configuration methods return the same object, so they chain. ignoring takes an exception class and can be called several times. The lambda returns a string or null: null means not yet, while a non-null value ends the wait and is returned from until.
💡 Простими словами: FluentWait — той самий WebDriverWait, але з ручним керуванням: як часто перевіряти, скільки чекати і на які помилки не зважати.
💡 In plain words: FluentWait is the same idea as WebDriverWait with manual controls: how often to check, how long to wait and which errors to shrug off.
🗣 Як читати вголос FluentWait я беру, коли стандартного WebDriverWait замало. Там я сам задаю загальний таймаут, як часто опитувати і які винятки ігнорувати. Типовий випадок — елемент, який фреймворк постійно перемальовує: я ігнорую NoSuchElement і StaleElement, а в лямбді щоразу шукаю елемент заново. При цьому стежу, щоб не опитувати надто часто і не ігнорувати загальний WebDriverException, інакше справжня аварія сесії перетвориться на тихе очікування до таймауту.
🗣 How to say it out loud I reach for FluentWait when the standard WebDriverWait is not enough. There I set the overall timeout, how often to poll and which exceptions to ignore. The typical case is an element the framework keeps re-rendering: I ignore NoSuchElement and StaleElement and look the element up afresh inside the lambda each time. I am careful not to poll too aggressively and not to ignore the generic WebDriverException, otherwise a real session crash turns into a quiet wait until the timeout.
24 Як правильно дочекатися завершення навігації, особливо в SPA? How do you correctly wait for navigation to finish, especially in a single-page app?
ОчікуванняWaits Middle ▶

Що це. driver.get(url) блокується відповідно до pageLoadStrategy: NORMAL (за замовчуванням) чекає, поки document.readyState стане complete, EAGER — стану interactive, NONE повертається одразу. А от клік по посиланню чи кнопці «Далі» нічого такого не гарантує: команда кліку завершується, а перехід ще триває.

What it is. driver.get(url) blocks according to pageLoadStrategy: NORMAL, the default, waits for document.readyState to be complete, EAGER waits for interactive, and NONE returns immediately. A click on a link or a Next button guarantees none of that: the click command finishes while the navigation is still under way.

Навіщо. Якщо одразу після кліку шукати елемент нової сторінки, тест може знайти однойменний елемент ще на старій, отримати його, а потім упасти зі StaleElementReferenceException. Надійна схема — дочекатись ознаки, що ми вже на цільовій сторінці: urlContains, urlToBe, titleIs, а головне — ключовий елемент, який є лише там.

Why it matters. If you look for an element of the new page right after the click, the test may find an element with the same locator still on the old page, grab it and then fail with StaleElementReferenceException. The reliable pattern is to wait for proof that you are on the target page: urlContains, urlToBe, titleIs, and above all a key element that exists only there.

Приклад. Після оплати застосунок переходить на /orders/123/confirmation. Чекаємо urlMatches з регулярним виразом, а потім видимість заголовка підтвердження. URL доводить, що роутер спрацював, заголовок — що компонент намалювався з даними.

Example. After payment the app moves to /orders/123/confirmation. We wait for urlMatches with a regular expression and then for the confirmation heading to be visible. The URL proves the router fired; the heading proves the component rendered with its data.

Підводні камені. У SPA перехід між «сторінками» робиться через history.pushState без перезавантаження, тож document.readyState весь час лишається complete, а дані підтягуються окремими запитами вже після зміни URL. Перевірка readyState через JavascriptExecutor тут нічого не дає. Так само titleIs ненадійний, якщо заголовок вкладки оновлюється із запізненням або однаковий для всіх роутів.

Pitfalls. In a SPA a page change happens through history.pushState without a reload, so document.readyState stays complete the whole time and the data arrives via separate requests after the URL has changed. Checking readyState through JavascriptExecutor tells you nothing there. Likewise titleIs is unreliable when the tab title updates late or is identical for every route.

Уточнення. Коли має сенс EAGER? Коли сторінка довго вантажить сторонні скрипти чи рекламу, які тесту не потрібні: driver.get повернеться раніше, а готовність потрібних елементів тест і так перевіряє явними очікуваннями.

Follow-up. When does EAGER make sense? When a page spends a long time loading third-party scripts or ads the test does not need: driver.get returns earlier, and the test checks the elements it cares about with explicit waits anyway.

Перехід після кліку: URL плюс ключовий елемент
Navigation after a click: URL plus a key element
// Стратегія завантаження задається до старту сесії
ChromeOptions options = new ChromeOptions();
options.setPageLoadStrategy(PageLoadStrategy.EAGER);
WebDriver driver = new ChromeDriver(options);

driver.get("https://shop.example.com/checkout"); // блокується до interactive
driver.findElement(By.id("pay")).click();         // навігацію не чекає

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
// 1) роутер перейшов на сторінку підтвердження
wait.until(ExpectedConditions.urlMatches("/orders/\d+/confirmation$"));
// 2) компонент намалювався з даними
WebElement heading = wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.cssSelector("[data-testid=confirmation-title]")));
// The load strategy is set before the session starts
ChromeOptions options = new ChromeOptions();
options.setPageLoadStrategy(PageLoadStrategy.EAGER);
WebDriver driver = new ChromeDriver(options);

driver.get("https://shop.example.com/checkout"); // blocks until interactive
driver.findElement(By.id("pay")).click();         // does not wait for navigation

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
// 1) the router moved to the confirmation page
wait.until(ExpectedConditions.urlMatches("/orders/\d+/confirmation$"));
// 2) the component rendered with its data
WebElement heading = wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.cssSelector("[data-testid=confirmation-title]")));
🔧 Розбір синтаксису: setPageLoadStrategy(PageLoadStrategy.EAGER) — метод опцій, що стає капабіліті сесії pageLoadStrategy; змінити його посеред сесії не можна. urlMatches приймає регулярний вираз і шукає збіг у поточному URL; подвійний зворотний слеш потрібен, бо в Java-рядку \d записується як дві косі. urlContains та urlToBe працюють з підрядком і точним значенням.
🔧 Syntax breakdown: setPageLoadStrategy(PageLoadStrategy.EAGER) is an options method that becomes the session capability pageLoadStrategy; it cannot be changed mid-session. urlMatches takes a regular expression and looks for a match in the current URL; the doubled backslash is needed because a Java string writes \d with two slashes. urlContains and urlToBe work with a substring and an exact value.
💡 Простими словами: Клік не знає, що після нього буде перехід. Тому тест сам має спитати: «Ми вже там?» — і перевірити адресу та щось, що є лише на новій сторінці.
💡 In plain words: A click has no idea a navigation follows it. So the test has to ask whether it has arrived yet, checking both the address and something that exists only on the new page.
🗣 Як читати вголос Я пам’ятаю, що driver.get чекає завантаження згідно з pageLoadStrategy, а клік — ні. Тому після дії, яка веде на іншу сторінку, я чекаю дві речі: що URL змінився на очікуваний, наприклад через urlContains чи urlMatches, і що з’явився ключовий елемент цільової сторінки. У SPA на document.readyState не покладаюсь, бо роутер міняє адресу без перезавантаження і readyState завжди complete. Якщо сторінку гальмують сторонні скрипти, ставлю стратегію eager.
🗣 How to say it out loud I keep in mind that driver.get waits for loading according to pageLoadStrategy, but a click does not. So after an action that leads to another page I wait for two things: the URL changing to what I expect, with urlContains or urlMatches, and a key element of the target page appearing. In a SPA I never rely on document.readyState, because the router changes the address without a reload and readyState is always complete. If third-party scripts slow the page down, I switch the strategy to eager.
25 Як ви спроєктуєте шар очікувань для великого набору тестів, щоб він був швидким і стабільним? How would you design the wait layer for a large test suite so that it is both fast and stable?
ОчікуванняWaits Senior ▶

Що це. Шар очікувань — це набір правил і утиліт, через які проходить кожна взаємодія з UI: єдине джерело таймаутів, бібліотека власних умов і заборона на сон та implicit wait. Мета — щоб автор тесту описував бізнес-крок, а не думав, скільки секунд і як саме чекати.

What it is. The wait layer is a set of rules and utilities every UI interaction goes through: one source of timeouts, a library of custom conditions, and a ban on sleeps and implicit waits. The goal is for a test author to describe a business step instead of thinking about how many seconds to wait and how.

Навіщо. У наборі з тисячі тестів розкидані new WebDriverWait(driver, Duration.ofSeconds(30)) з різними числами дають дві біди: частина тестів падає на повільному стенді, а частина чекає по пів хвилини на те, що має статись за секунду. Централізація дозволяє змінити поведінку для оточення одним параметром і міряти, де ми реально чекаємо.

Why it matters. In a suite of a thousand tests, scattered new WebDriverWait(driver, Duration.ofSeconds(30)) calls with assorted numbers cause two problems: some tests fail on a slow environment, others wait half a minute for something that should take one second. Centralising lets you change behaviour per environment with one parameter and measure where the suite actually waits.

Приклад. Клас Timeouts читає значення з системної властивості: 10 с локально, 20 с на спільному staging. Клас Waits надає умови, яких немає в ExpectedConditions: «усі спінери зникли», «кількість рядків перестала змінюватись протягом 500 мс», «кнопка не перекрита». Page Object викликає лише ці методи, а code review і правило в ArchUnit ловлять Thread.sleep та implicitlyWait.

Example. A Timeouts class reads values from a system property: 10 s locally, 20 s on a shared staging. A Waits class offers conditions ExpectedConditions lacks: all spinners gone, row count unchanged for 500 ms, button not covered. Page objects call only these methods, while code review and an ArchUnit rule catch Thread.sleep and implicitlyWait.

Підводні камені. Глобальне збільшення таймауту «щоб не падало» ховає реальну деградацію продукту; краще логувати тривалість кожного очікування і дивитись на 95-й перцентиль по кроках. Ще одна пастка — універсальний waitForPageReady(), що чекає все підряд: він повільний і все одно не знає про конкретний стан. Власні умови мають бути малими і названими за змістом.

Pitfalls. Raising the timeout globally so things stop failing hides real product degradation; it is better to log the duration of every wait and watch the 95th percentile per step. Another trap is a universal waitForPageReady() that waits for everything: it is slow and still knows nothing about the specific state. Custom conditions should be small and named after what they mean.

Уточнення. Що робити з асинхронністю, яку UI не показує? Дати розробникам тестовий хук, наприклад атрибут data-loading на кореневому елементі чи лічильник активних запитів, і чекати саме на нього. Це дешевше, ніж вгадувати за зовнішніми ознаками.

Follow-up. What about asynchronous work the UI does not show? Ask developers for a test hook, such as a data-loading attribute on the root element or a counter of in-flight requests, and wait on that. It is cheaper than guessing from outside signs.

Централізовані таймаути і власні умови
Central timeouts and custom conditions
public final class Timeouts {
    // -Dui.timeout=20 на staging, 10 за замовчуванням
    public static final Duration UI =
        Duration.ofSeconds(Long.getLong("ui.timeout", 10));
}

public final class Waits {
    private final WebDriverWait wait;

    public Waits(WebDriver driver) {
        this.wait = new WebDriverWait(driver, Timeouts.UI);
    }

    // усі спінери зникли з екрана
    public void spinnersGone() {
        wait.withMessage("спінер не зник")
            .until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".spinner")));
    }

    // кількість рядків не змінювалась два опитування поспіль
    public int rowCountStable(By rows) {
        int[] last = {-1};
        return wait.withMessage("список не стабілізувався").until(d -> {
            int now = d.findElements(rows).size();
            boolean stable = now == last[0];
            last[0] = now;
            return stable ? now : null;
        });
    }
}
public final class Timeouts {
    // -Dui.timeout=20 on staging, 10 by default
    public static final Duration UI =
        Duration.ofSeconds(Long.getLong("ui.timeout", 10));
}

public final class Waits {
    private final WebDriverWait wait;

    public Waits(WebDriver driver) {
        this.wait = new WebDriverWait(driver, Timeouts.UI);
    }

    // every spinner has left the screen
    public void spinnersGone() {
        wait.withMessage("spinner did not disappear")
            .until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".spinner")));
    }

    // row count unchanged across two polls in a row
    public int rowCountStable(By rows) {
        int[] last = {-1};
        return wait.withMessage("list never settled").until(d -> {
            int now = d.findElements(rows).size();
            boolean stable = now == last[0];
            last[0] = now;
            return stable ? now : null;
        });
    }
}
🔧 Розбір синтаксису: Long.getLong("ui.timeout", 10) читає системну властивість JVM, передану через -D, з дефолтом 10. int[] last — масив з одного елемента, бо лямбда не може змінювати звичайну локальну змінну, а вміст масиву — може. Лямбда повертає Integer або null, тож until поверне стабільну кількість рядків. Інтервал стабільності дорівнює кроку опитування, 500 мс.
🔧 Syntax breakdown: Long.getLong("ui.timeout", 10) reads a JVM system property passed with -D, defaulting to 10. int[] last is a one-element array because a lambda cannot reassign a plain local variable but can change an array element. The lambda returns an Integer or null, so until returns the settled row count. The stability window equals the polling step, 500 ms.
💡 Простими словами: Автор тесту має писати «дочекайся, поки список завантажиться», а не «почекай 7 секунд». Скільки і як — вирішує один спільний клас.
💡 In plain words: A test author should write wait until the list has loaded, not wait seven seconds. How long and how is decided by one shared class.
🗣 Як читати вголос У великому наборі я виношу очікування в окремий шар. Таймаути беруться з конфігурації і залежать від оточення, а в Page Object є тільки осмислені методи на кшталт «спінери зникли» чи «список стабілізувався». Thread.sleep і implicit wait заборонені правилом у збірці. Тривалість кожного очікування я логую, щоб бачити, які кроки повільнішають, а не просто піднімати таймаут. Для асинхронних речей, яких не видно в UI, домовляюсь із розробниками про тестовий атрибут і чекаю саме на нього.
🗣 How to say it out loud In a large suite I move waiting into its own layer. Timeouts come from configuration and depend on the environment, and page objects only get meaningful methods such as spinners gone or list settled. Thread.sleep and implicit waits are banned by a rule in the build. I log how long each wait takes, so I can see which steps are getting slower instead of just raising the timeout. For asynchronous work invisible in the UI, I agree a test attribute with the developers and wait on that.
▶ 5 3
26 Коли виникає StaleElementReferenceException, як відтворити його в коді і як виправити без сліпих ретраїв? When does StaleElementReferenceException happen, how do you reproduce it in code, and how do you fix it without blind retries?
ВиняткиExceptions Middle ▶

Що це. WebElement у Selenium — лише посилання на конкретний DOM-вузол. Якщо фронтенд видалив цей вузол і намалював новий, навіть з тим самим id і текстом, старе посилання «протухає», і будь-яка дія з ним кидає StaleElementReferenceException. Те саме стається після driver.navigate().refresh() або переходу на іншу сторінку.

What it is. A WebElement in Selenium is only a reference to one specific DOM node. If the front end removes that node and renders a new one, even with the same id and text, the old reference goes stale, and any action on it throws StaleElementReferenceException. The same happens after driver.navigate().refresh() or a move to another page.

Навіщо. У React, Vue чи Angular перемальовування — норма: сортування таблиці, фільтр, оновлення після AJAX. Тест, який один раз знайшов елемент і тримає його в полі класу, падає нестабільно — залежно від того, встиг фреймворк перебудувати список чи ні.

Why it matters. In React, Vue or Angular re-rendering is routine: sorting a table, applying a filter, refreshing after an AJAX call. A test that finds an element once and keeps it in a class field fails intermittently, depending on whether the framework managed to rebuild the list before the next command.

Приклад. Знаходимо перший рядок таблиці замовлень, клікаємо сортування за датою, а потім читаємо текст старого рядка — виняток гарантований, бо tbody збудовано заново. Виправлення: зберігати не WebElement, а By, після дії, що перемальовує DOM, дочекатися ExpectedConditions.stalenessOf(oldRow) — тобто підтвердження, що старий вузол зник, — і лише тоді шукати рядок наново.

Example. We grab the first row of the orders table, click sort by date and then read the text of the old row: the exception is guaranteed because the tbody was rebuilt. The fix is to keep a By rather than a WebElement, and after an action that re-renders the DOM wait for ExpectedConditions.stalenessOf(oldRow), which confirms the old node is gone, and only then locate the row again.

Підводні камені. Обгортка «спіймай stale і повтори тричі» маскує справжню проблему: тест може прочитати дані ще до сортування і пройти з неправильним результатом. Кешування через @CacheLookup у PageFactory робить stale майже гарантованим на динамічних сторінках. Також не варто ловити виняток усередині page object і мовчки повертати порожній рядок.

Pitfalls. A wrapper that catches stale and retries three times hides the real problem: the test may read data before the sort finished and pass with the wrong result. Caching with @CacheLookup in PageFactory makes stale references almost certain on dynamic pages. Swallowing the exception inside a page object and returning an empty string is just as harmful.

Уточнення. Питання «чим stale відрізняється від NoSuchElement?» — NoSuchElement означає, що пошук нічого не знайшов, а stale — що знайдений раніше вузол більше не прикріплений до документа.

Follow-up. "How is stale different from NoSuchElement?" NoSuchElement means the search found nothing, while stale means a node found earlier is no longer attached to the document.

Відтворення і чесне виправлення
Reproducing it and fixing it properly
By firstRow = By.cssSelector("#orders tbody tr");

// Відтворення: посилання на вузол, який зараз перемалюють
WebElement row = driver.findElement(firstRow);
driver.findElement(By.id("sort-by-date")).click();
row.getText(); // StaleElementReferenceException

// Виправлення: дочекатися зникнення старого вузла і знайти заново
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement oldRow = driver.findElement(firstRow);
driver.findElement(By.id("sort-by-date")).click();
wait.until(ExpectedConditions.stalenessOf(oldRow));
String newest = wait
    .until(ExpectedConditions.visibilityOfElementLocated(firstRow))
    .getText();
By firstRow = By.cssSelector("#orders tbody tr");

// Reproduce: a reference to a node that is about to be re-rendered
WebElement row = driver.findElement(firstRow);
driver.findElement(By.id("sort-by-date")).click();
row.getText(); // StaleElementReferenceException

// Fix: wait until the old node is gone, then locate again
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement oldRow = driver.findElement(firstRow);
driver.findElement(By.id("sort-by-date")).click();
wait.until(ExpectedConditions.stalenessOf(oldRow));
String newest = wait
    .until(ExpectedConditions.visibilityOfElementLocated(firstRow))
    .getText();
🔧 Розбір синтаксису: By — це рецепт пошуку, його можна виконувати скільки завгодно разів. stalenessOf(element) повертає true, коли переданий елемент відчепився від DOM, тож очікування саме підтверджує, що перемальовування сталося. visibilityOfElementLocated(By) шукає вузол заново на кожній ітерації опитування і повертає вже свіжий WebElement.
🔧 Syntax breakdown: By is a search recipe that can be executed any number of times. stalenessOf(element) returns true once the given element is detached from the DOM, so the wait itself proves the re-render happened. visibilityOfElementLocated(By) searches again on every polling iteration and returns a fresh WebElement.
💡 Простими словами: Зберігай адресу, а не будинок: будинок можуть знести й звести новий, а адреса лишиться тією самою.
💡 In plain words: Keep the address, not the house: the house can be torn down and rebuilt, but the address stays the same.
🗣 Як читати вголос Stale для мене — це сигнал, що я тримаю посилання на вузол, якого вже немає: фронтенд перемалював таблицю або сторінка перезавантажилась. Я відтворюю це просто: знайшов рядок, клікнув сортування, звернувся до старого рядка. Виправляю не ретраями, а дизайном: у page object зберігаю локатори, а не елементи, і після дії, що змінює DOM, чекаю stalenessOf старого елемента, а потім шукаю заново. Так тест ще й перевіряє, що перемальовування справді відбулось.
🗣 How to say it out loud To me a stale exception means I am holding a reference to a node that no longer exists: the front end re-rendered a table or the page reloaded. I reproduce it simply: find a row, click sort, touch the old row. I fix it with design rather than retries. Page objects keep locators, not elements, and after an action that changes the DOM I wait for stalenessOf on the old element and then look it up again. As a bonus the test now proves the re-render actually happened.
27 Що означає NoSuchElementException і як швидко знайти його справжню причину? What does NoSuchElementException mean, and how do you quickly find its real cause?
ВиняткиExceptions Junior ▶

Що це. findElement кидає NoSuchElementException, коли в поточному контексті пошуку жоден вузол не відповідає локатору. Ключове слово — поточний контекст: WebDriver шукає лише в активному вікні, в активному фреймі і лише у звичайному DOM, без заходу в shadow root.

What it is. findElement throws NoSuchElementException when no node in the current search context matches the locator. The key words are current context: WebDriver searches only the active window, only the active frame and only the regular DOM, without stepping into a shadow root.

Навіщо. Це найчастіший виняток у звітах, і за ним ховаються зовсім різні причини: друкарська помилка в селекторі, сторінка ще не домалювалась, елемент усередині iframe, клік відкрив нову вкладку, а драйвер лишився в старій, або компонент живе в Shadow DOM. Лікування для кожної причини своє, тому спершу треба діагностувати.

Why it matters. It is the most common exception in reports, and very different causes hide behind it: a typo in the selector, a page that has not finished rendering, an element inside an iframe, a click that opened a new tab while the driver stayed in the old one, or a component living in Shadow DOM. Each cause has its own cure, so diagnosis comes first.

Приклад. Кнопка оплати не знаходиться, хоча в DevTools вона є. Перевіряю по черзі: getCurrentUrl() і кількість вікон — чи я там, де думаю; findElements(...).size() — чи локатор взагалі щось знаходить; дерево в Elements — чи немає над кнопкою тегу iframe або позначки #shadow-root. У цьому випадку форма оплати була у фреймі платіжного провайдера, і рішенням став frameToBeAvailableAndSwitchToIt.

Example. The pay button cannot be found although DevTools shows it. I check in order: getCurrentUrl() and the number of windows, to see whether I am where I think I am; findElements(...).size(), to see whether the locator matches anything at all; and the Elements tree, looking for an iframe tag or a #shadow-root marker above the button. Here the payment form sat inside the payment provider's frame, and frameToBeAvailableAndSwitchToIt solved it.

Підводні камені. Збільшення implicit wait до 30 секунд не допоможе, якщо елемент у фреймі, — тест просто падатиме повільніше. Селектор, скопійований з DevTools через Copy selector, часто прив’язаний до позиції і ламається від першої зміни верстки. І не варто перевіряти відсутність елемента через try/catch навколо findElement: для цього є findElements з порожнім списком.

Pitfalls. Raising the implicit wait to 30 seconds does not help when the element is in a frame; the test just fails more slowly. A selector copied from DevTools with Copy selector is often tied to position and breaks on the first layout change. Also avoid checking for absence with try/catch around findElement; findElements returning an empty list exists for that.

Уточнення. Якщо питають, чим допоможе explicit wait, відповідь така: він закриває лише причину «ще не з’явився». Решту — неправильний контекст чи локатор — жодне очікування не виправить.

Follow-up. If asked how an explicit wait helps, the answer is that it only covers the not-there-yet cause. No wait fixes a wrong context or a wrong locator.

Діагностичний чекліст у коді
A diagnostic checklist in code
By payButton = By.cssSelector("[data-testid='pay']");

// 1. Чи я в тому вікні й на тій сторінці?
System.out.println(driver.getCurrentUrl() + " / вікон: "
    + driver.getWindowHandles().size());

// 2. Чи знаходить локатор хоч щось (без винятку)?
int matches = driver.findElements(payButton).size();

// 3. Кнопка у фреймі провайдера: спершу перейти в нього
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions
    .frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe#payment")));
wait.until(ExpectedConditions.elementToBeClickable(payButton)).click();
driver.switchTo().defaultContent();
By payButton = By.cssSelector("[data-testid='pay']");

// 1. Am I in the right window and on the right page?
System.out.println(driver.getCurrentUrl() + " / windows: "
    + driver.getWindowHandles().size());

// 2. Does the locator match anything (no exception)?
int matches = driver.findElements(payButton).size();

// 3. The button is in the provider frame: switch into it first
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions
    .frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe#payment")));
wait.until(ExpectedConditions.elementToBeClickable(payButton)).click();
driver.switchTo().defaultContent();
🔧 Розбір синтаксису: getWindowHandles() повертає набір ідентифікаторів відкритих вікон, його розмір більший за один — привід перевірити, куди перемкнутись. findElements ніколи не кидає виняток і повертає список, тому зручний для діагностики. frameToBeAvailableAndSwitchToIt чекає фрейм і одразу перемикає в нього контекст, а defaultContent() повертає драйвер до головного документа.
🔧 Syntax breakdown: getWindowHandles() returns the set of open window ids; a size above one is a hint to check where to switch. findElements never throws and returns a list, which makes it handy for diagnosis. frameToBeAvailableAndSwitchToIt waits for the frame and switches the context into it at once, and defaultContent() brings the driver back to the top-level document.
💡 Простими словами: Драйвер шукає лише там, куди він зараз дивиться. Перш ніж лаяти локатор, переконайся, що драйвер дивиться в правильне вікно й фрейм.
💡 In plain words: The driver only searches where it is currently looking. Before blaming the locator, make sure it is looking at the right window and frame.
🗣 Як читати вголос Коли бачу NoSuchElement, я не збільшую таймаут, а проходжу короткий чекліст. Спершу дивлюсь URL і кількість вікон: можливо, клік відкрив нову вкладку. Потім рахую збіги через findElements, щоб зрозуміти, чи локатор узагалі щось бачить. Далі шукаю в DevTools iframe або shadow root над елементом. І лише якщо все це в порядку, думаю про таймінг і ставлю explicit wait. Здебільшого причина виявляється в контексті, а не в швидкості сторінки.
🗣 How to say it out loud When I see NoSuchElement I do not raise the timeout, I run a short checklist. First the URL and the number of windows, because the click may have opened a new tab. Then I count matches with findElements to learn whether the locator sees anything at all. Next I look in DevTools for an iframe or a shadow root above the element. Only when all of that is fine do I think about timing and add an explicit wait. Most of the time the cause turns out to be context, not page speed.
28 Чим ElementClickInterceptedException відрізняється від ElementNotInteractableException і як їх виправляти чесно? How does ElementClickInterceptedException differ from ElementNotInteractableException, and how do you fix them honestly?
ВиняткиExceptions Middle ▶

Що це. Обидва винятки означають, що елемент знайдено, але взаємодія неможлива. ElementClickInterceptedException — клік потрапив би в інший елемент, який лежить зверху в точці кліку: оверлей завантаження, банер cookies, липкий хедер. ElementNotInteractableException — з самим елементом не можна взаємодіяти: він прихований, має нульовий розмір або до нього не вдається прокрутити.

What it is. Both exceptions mean the element was found but the interaction is impossible. ElementClickInterceptedException means the click would land on another element lying on top at the click point: a loading overlay, a cookie banner, a sticky header. ElementNotInteractableException means the element itself cannot be interacted with: it is hidden, has zero size or cannot be scrolled into view.

Навіщо. Повідомлення винятку підказує, куди дивитися. У тексті intercepted драйвер прямо пише, який елемент отримав би клік, наприклад <div class="spinner-backdrop">. Для not interactable типова причина — локатор знайшов не той із двох однакових елементів, наприклад приховану мобільну версію кнопки.

Why it matters. The exception message tells you where to look. For intercepted clicks the driver names the element that would receive the click, for example <div class="spinner-backdrop">. For not interactable the usual cause is a locator that matched the wrong one of two similar elements, such as a hidden mobile copy of a button.

Приклад. Після збереження форми на пів секунди з’являється напівпрозорий бекдроп, і клік по «Далі» перехоплюється. Правильне виправлення — дочекатися invisibilityOfElementLocated бекдропа, а потім elementToBeClickable кнопки. Якщо заважає банер cookies, тест закриває його так, як зробив би користувач, або вимикає через cookie згоди ще до відкриття сторінки.

Example. After saving a form a translucent backdrop appears for half a second and the click on Next is intercepted. The right fix is to wait for invisibilityOfElementLocated on the backdrop and then for elementToBeClickable on the button. If a cookie banner is in the way, the test closes it the way a user would, or sets the consent cookie before the page is opened.

Підводні камені. elementToBeClickable перевіряє лише, що елемент видимий і enabled; перекриття іншим елементом він не бачить, тож від intercepted сам по собі не рятує. Найгірша «латка» — клік через JavaScript: він обходить перекриття, і тест проходить там, де реальний користувач застряг би. Для липкого хедера допомагає прокрутка елемента в центр, а не догори.

Pitfalls. elementToBeClickable only checks that the element is visible and enabled; it cannot see another element covering it, so on its own it does not prevent intercepted clicks. The worst patch is a JavaScript click: it bypasses the overlay and the test passes where a real user would be stuck. For a sticky header, scrolling the element to the centre of the viewport helps more than scrolling it to the top.

Уточнення. Якщо оверлей не зникає ніколи, це вже баг продукту, і тест має впасти з понятним повідомленням, а не обійти проблему.

Follow-up. If the overlay never goes away, that is a product bug, and the test should fail with a clear message instead of working around it.

Дочекатися оверлея і клікнути як користувач
Wait for the overlay and click like a user
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
By backdrop = By.cssSelector(".spinner-backdrop");
By next = By.cssSelector("[data-testid='wizard-next']");

// Бекдроп після збереження має зникнути сам
wait.until(ExpectedConditions.invisibilityOfElementLocated(backdrop));

// Прокрутка, щоб липкий хедер не перекривав кнопку
WebElement button = wait.until(ExpectedConditions.elementToBeClickable(next));
new Actions(driver).scrollToElement(button).perform();
button.click(); // справжній клік, без JavaScript
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
By backdrop = By.cssSelector(".spinner-backdrop");
By next = By.cssSelector("[data-testid='wizard-next']");

// The backdrop after saving must disappear by itself
wait.until(ExpectedConditions.invisibilityOfElementLocated(backdrop));

// Scroll so the sticky header does not cover the button
WebElement button = wait.until(ExpectedConditions.elementToBeClickable(next));
new Actions(driver).scrollToElement(button).perform();
button.click(); // a real click, no JavaScript
🔧 Розбір синтаксису: invisibilityOfElementLocated повертає true, коли елемента немає в DOM або він невидимий, тож підходить і для оверлея, що видаляється, і для того, що ховається через CSS. elementToBeClickable повертає сам елемент, коли той видимий і enabled. Actions.scrollToElement з’явився в Selenium 4.2 і прокручує через Actions API, а perform() відправляє накопичені дії драйверу.
🔧 Syntax breakdown: invisibilityOfElementLocated returns true when the element is absent from the DOM or invisible, so it fits both an overlay that is removed and one hidden with CSS. elementToBeClickable returns the element itself once it is visible and enabled. Actions.scrollToElement arrived in Selenium 4.2 and scrolls through the Actions API, and perform() sends the queued actions to the driver.
💡 Простими словами: Intercepted — «на кнопці щось лежить», not interactable — «з кнопкою щось не так». Прибирай те, що лежить зверху, а не натискай крізь нього.
💡 In plain words: Intercepted means something is lying on the button, not interactable means something is wrong with the button. Remove what is on top instead of pressing through it.
🗣 Як читати вголос Я розрізняю ці винятки так: intercepted — елемент нормальний, але поверх нього щось лежить, і драйвер навіть пише, що саме; not interactable — сам елемент прихований або нульового розміру, часто я просто знайшов не ту з двох кнопок. Лікую чесно: чекаю, поки зникне спінер чи бекдроп, закриваю банер як користувач, прокручую кнопку в центр. JavaScript-клік не використовую, бо він сховає справжній баг з оверлеєм, який заважатиме людям.
🗣 How to say it out loud I separate the two like this: intercepted means the element is fine but something lies on top of it, and the driver even says what; not interactable means the element itself is hidden or has zero size, and often I simply matched the wrong one of two buttons. I fix it honestly: wait for the spinner or backdrop to go, close the banner as a user would, scroll the button to the centre. I avoid a JavaScript click, because it would hide a real overlay bug that would block people.
▶ 6 7
29 Як працювати з iframe у Selenium і чим defaultContent() відрізняється від parentFrame()? How do you work with an iframe in Selenium, and how is defaultContent() different from parentFrame()?
КонтекстиContexts Junior ▶

Що це. Iframe — окремий документ усередині сторінки зі своїм DOM. WebDriver завжди шукає елементи лише в поточному контексті перегляду, тому поле оплати у фреймі платіжного провайдера для основного документа просто не існує. Перейти всередину можна трьома способами: switchTo().frame(0) за індексом, switchTo().frame("payment") за атрибутом name чи id і switchTo().frame(element) за знайденим WebElement.

What it is. An iframe is a separate document inside the page with its own DOM. WebDriver only searches the current browsing context, so a card field inside a payment provider frame simply does not exist for the main document. You step inside in one of three ways: switchTo().frame(0) by index, switchTo().frame("payment") by the name or id attribute, or switchTo().frame(element) with a located WebElement.

Навіщо. Фрейми трапляються в платіжних формах, капчах, WYSIWYG-редакторах і вбудованих віджетах. Без перемикання контексту тест отримує NoSuchElementException, хоча елемент чудово видно в DevTools, і новачок годинами переписує локатор замість однієї команди.

Why it matters. Frames show up in payment forms, captchas, rich text editors and embedded widgets. Without a context switch the test gets NoSuchElementException even though the element is plainly visible in DevTools, and a newcomer spends hours rewriting the locator when one command was missing.

Приклад. На сторінці checkout фрейм з картою вантажиться асинхронно, тому я чекаю його через ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe[title='Card']")): умова дочекається фрейму і одразу переключить у нього. Далі вводжу номер карти, а для кнопки «Оплатити» в основному документі повертаюсь через defaultContent().

Example. On the checkout page the card frame loads asynchronously, so I wait for it with ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe[title='Card']")): the condition waits for the frame and switches into it in one go. Then I type the card number, and for the Pay button that lives in the main document I go back with defaultContent().

Підводні камені. defaultContent() повертає на самий верх, до головного документа, а parentFrame() — лише на один рівень вище. У вкладених фреймах це важливо: з внутрішнього фрейму до сусіднього в межах того ж батька треба піти в parentFrame(), а не на верх. Індекс крихкий: рекламний банер з новим iframe зсуне нумерацію. І забутий вихід з фрейму ламає всі наступні кроки тесту.

Pitfalls. defaultContent() jumps all the way up to the top-level document, while parentFrame() moves exactly one level up. With nested frames that matters: to reach a sibling frame under the same parent you go to parentFrame(), not to the top. Switching by index is brittle, since an ad banner with a new iframe shifts the numbering. And forgetting to leave a frame breaks every later step of the test.

Уточнення. Питають, чи треба перемикатись у Shadow DOM так само. Ні: shadow root — частина того самого документа, його відкривають через getShadowRoot(), а не через switchTo().

Follow-up. People ask whether Shadow DOM needs the same switch. It does not: a shadow root belongs to the same document and is opened with getShadowRoot(), not with switchTo().

Фрейм з карткою на сторінці оплати
A card frame on the checkout page
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

// чекаємо фрейм і одразу переходимо в нього
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(
        By.cssSelector("iframe[title='Card']")));
driver.findElement(By.name("cardnumber")).sendKeys("4242424242424242");

// вкладений фрейм з CVC всередині фрейму карти
driver.switchTo().frame("cvc-frame");
driver.findElement(By.name("cvc")).sendKeys("123");
driver.switchTo().parentFrame();   // назад у фрейм карти

driver.switchTo().defaultContent(); // на верхній рівень сторінки
driver.findElement(By.id("pay")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

// wait for the frame and switch into it right away
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(
        By.cssSelector("iframe[title='Card']")));
driver.findElement(By.name("cardnumber")).sendKeys("4242424242424242");

// nested CVC frame inside the card frame
driver.switchTo().frame("cvc-frame");
driver.findElement(By.name("cvc")).sendKeys("123");
driver.switchTo().parentFrame();   // back to the card frame

driver.switchTo().defaultContent(); // up to the top-level page
driver.findElement(By.id("pay")).click();
🔧 Розбір синтаксису: frameToBeAvailableAndSwitchToIt приймає By, індекс, ім’я чи WebElement і сама виконує перемикання, коли фрейм з’явився. switchTo().frame("cvc-frame") шукає фрейм за name або id лише в поточному контексті. parentFrame() піднімає на рівень вище, defaultContent() — до головного документа.
🔧 Syntax breakdown: frameToBeAvailableAndSwitchToIt accepts a By, an index, a name or a WebElement and performs the switch itself once the frame is there. switchTo().frame("cvc-frame") looks for a frame by name or id only within the current context. parentFrame() climbs one level, defaultContent() returns to the main document.
💡 Простими словами: Фрейм — це сторінка в сторінці. Щоб щось у ній натиснути, спершу треба «увійти», а потім не забути «вийти».
💡 In plain words: A frame is a page inside a page. To click anything in it you first step in, and afterwards you must remember to step out.
🗣 Як читати вголос Для мене iframe — це окремий документ, тому першим ділом я перевіряю, чи елемент не сидить у фреймі, коли бачу NoSuchElement при правильному локаторі. Заходжу я туди через очікування frameToBeAvailableAndSwitchToIt, бо платіжні фрейми вантажаться із затримкою. Індексом майже не користуюсь, він ламається від будь-якого нового банера. А виходжу свідомо: parentFrame, якщо фрейми вкладені і мені потрібен сусід, і defaultContent, коли треба повернутись до основної сторінки.
🗣 How to say it out loud For me an iframe is a separate document, so when I see NoSuchElement with a locator I know is right, the first thing I check is whether the element lives in a frame. I get in with a frameToBeAvailableAndSwitchToIt wait, because payment frames load late. I hardly ever switch by index, since any new banner breaks it. And I leave on purpose: parentFrame when frames are nested and I need a sibling, defaultContent when I have to get back to the main page.
30 Як тест перемикається між вікнами та вкладками і що змінилось у Selenium 4? How does a test switch between windows and tabs, and what changed in Selenium 4?
КонтекстиContexts Junior ▶

Що це. Кожне вікно чи вкладка в сесії має window handle — унікальний рядок-ідентифікатор. getWindowHandle() повертає handle поточного вікна, getWindowHandles() — множину всіх відкритих. Команди завжди йдуть у поточне вікно, а змінити його можна лише явно через switchTo().window(handle): драйвер не переходить у нову вкладку сам, навіть якщо браузер її показав.

What it is. Every window or tab in a session has a window handle, a unique string id. getWindowHandle() returns the handle of the current window and getWindowHandles() returns the set of all open ones. Commands always go to the current window, and you change it only explicitly with switchTo().window(handle): the driver never follows a new tab by itself, even when the browser shows it.

Навіщо. Посилання з target="_blank", OAuth-логін через Google, перегляд PDF чи сторінка підтримки часто відкриваються в новій вкладці. Тест має знайти її, перевірити вміст і повернутись назад, не загубивши вихідне вікно.

Why it matters. Links with target="_blank", an OAuth login through Google, a PDF preview or a help page often open in a new tab. The test has to find that tab, check its content and come back without losing the original window.

Приклад. Запам’ятовую handle оригіналу, клікаю «Умови використання», чекаю numberOfWindowsToBe(2), перебираю handles і перемикаюсь на той, що відрізняється від оригіналу. Перевіряю заголовок, закриваю вкладку через close() і повертаюсь командою switchTo().window(original).

Example. I store the original handle, click Terms of Use, wait for numberOfWindowsToBe(2), loop over the handles and switch to the one that differs from the original. I check the title, close the tab with close() and return with switchTo().window(original).

Підводні камені. getWindowHandles() повертає Set, і порядок у ньому не гарантований, тож брати «другий елемент» ненадійно. Після close() драйвер лишається «в порожнечі», і наступна команда без перемикання кине NoSuchWindowException. Нова вкладка з’являється не миттєво, тому без очікування кількості вікон тест стає flaky.

Pitfalls. getWindowHandles() returns a Set with no guaranteed order, so taking the second element is unreliable. After close() the driver is left pointing at nothing, and the next command without a switch throws NoSuchWindowException. A new tab does not appear instantly, so without waiting for the window count the test turns flaky.

Уточнення. У Selenium 4 з’явився switchTo().newWindow(WindowType.TAB) або WindowType.WINDOW: він відкриває порожню вкладку і одразу перемикає в неї. Це зручно, щоб у тій самій сесії з тими ж cookies відкрити адмінку паралельно з клієнтською частиною.

Follow-up. Selenium 4 added switchTo().newWindow(WindowType.TAB) or WindowType.WINDOW: it opens a blank tab and switches into it immediately. That is handy for opening the admin panel next to the customer site in the same session with the same cookies.

Нова вкладка з посилання і повернення назад
A new tab from a link and the way back
String original = driver.getWindowHandle();
driver.findElement(By.linkText("Terms of Use")).click();

new WebDriverWait(driver, Duration.ofSeconds(5))
        .until(ExpectedConditions.numberOfWindowsToBe(2));

for (String handle : driver.getWindowHandles()) {
    if (!handle.equals(original)) {
        driver.switchTo().window(handle); // перемикаємось явно
    }
}
assertThat(driver.getTitle()).contains("Terms");

driver.close();                       // закриває лише поточну вкладку
driver.switchTo().window(original);   // без цього буде NoSuchWindowException

// Selenium 4: відкрити порожню вкладку і одразу перейти в неї
driver.switchTo().newWindow(WindowType.TAB);
driver.get("https://admin.example.com");
String original = driver.getWindowHandle();
driver.findElement(By.linkText("Terms of Use")).click();

new WebDriverWait(driver, Duration.ofSeconds(5))
        .until(ExpectedConditions.numberOfWindowsToBe(2));

for (String handle : driver.getWindowHandles()) {
    if (!handle.equals(original)) {
        driver.switchTo().window(handle); // switch explicitly
    }
}
assertThat(driver.getTitle()).contains("Terms");

driver.close();                       // closes only the current tab
driver.switchTo().window(original);   // otherwise NoSuchWindowException

// Selenium 4: open a blank tab and switch into it at once
driver.switchTo().newWindow(WindowType.TAB);
driver.get("https://admin.example.com");
🔧 Розбір синтаксису: getWindowHandle() повертає String, getWindowHandles() — Set<String>. numberOfWindowsToBe(2) — готова умова з ExpectedConditions. Цикл шукає handle, не рівний оригіналу. assertThat тут з AssertJ. WindowType — перелік із двома значеннями: TAB і WINDOW.
🔧 Syntax breakdown: getWindowHandle() returns a String, getWindowHandles() a Set<String>. numberOfWindowsToBe(2) is a ready-made condition from ExpectedConditions. The loop looks for the handle that is not the original. assertThat comes from AssertJ. WindowType is an enum with two values, TAB and WINDOW.
💡 Простими словами: Браузер може відкрити десять вкладок, але Selenium «дивиться» лише в одну — ту, на яку ти його явно переключив.
💡 In plain words: The browser may open ten tabs, but Selenium looks at only one of them: the one you explicitly switched to.
🗣 Як читати вголос Я завжди починаю з того, що запам’ятовую handle поточного вікна. Потім клікаю посилання, дочікуюсь, поки вікон стане два, і шукаю handle, який не збігається з оригіналом, бо порядок у множині не гарантований. Після перевірки закриваю вкладку і обов’язково перемикаюсь назад, інакше наступна команда впаде з NoSuchWindow. А в Selenium 4 мені подобається newWindow: одним викликом відкриваю нову вкладку, наприклад з адмінкою, в тій самій сесії.
🗣 How to say it out loud I always start by saving the handle of the current window. Then I click the link, wait until there are two windows and pick the handle that differs from the original, because a set has no reliable order. After the check I close that tab and I make sure to switch back, or the next command fails with NoSuchWindow. In Selenium 4 I like newWindow: a single call opens a fresh tab, say with the admin panel, inside the same session.
31 Як обробити JavaScript alert, confirm чи prompt у Selenium і що буде, якщо алерт ніхто не закрив? How do you handle a JavaScript alert, confirm or prompt in Selenium, and what happens if nobody closes it?
КонтекстиContexts Junior ▶

Що це. alert(), confirm() і prompt() — нативні діалоги браузера, а не частина DOM, тому знайти їх через findElement неможливо. З ними працюють через driver.switchTo().alert(), який повертає об’єкт Alert з методами getText(), accept(), dismiss() і sendKeys() для введення тексту в prompt.

What it is. alert(), confirm() and prompt() are native browser dialogs, not part of the DOM, so findElement can never reach them. You work with them through driver.switchTo().alert(), which returns an Alert object with getText(), accept(), dismiss() and sendKeys() for typing into a prompt.

Навіщо. Підтвердження видалення, попередження про незбережені зміни або старі адмінки часто спираються саме на нативні діалоги. Поки діалог відкритий, браузер блокує сторінку, і будь-яка інша команда падає.

Why it matters. Delete confirmations, unsaved-changes warnings and older admin panels often rely on native dialogs. While one is open the browser blocks the page, and every other command fails.

Приклад. Тест видаляє замовлення: клік по «Видалити» викликає confirm('Delete order 42?'). Я чекаю діалог через ExpectedConditions.alertIsPresent(), перевіряю текст, щоб переконатися, що видаляється саме те замовлення, і натискаю accept(). Для негативного сценарію викликаю dismiss() і перевіряю, що рядок лишився в таблиці.

Example. A test deletes an order: clicking Delete calls confirm('Delete order 42?'). I wait for the dialog with ExpectedConditions.alertIsPresent(), check the text to be sure the right order is being removed, and call accept(). For the negative case I call dismiss() and verify the row is still in the table.

Підводні камені. Модальне вікно на HTML і CSS — не алерт: воно живе в DOM, і switchTo().alert() кине NoAlertPresentException. Якщо нативний діалог несподівано з’явився і його ніхто не обробив, наступна команда отримає UnhandledAlertException. Поведінку в такій ситуації задає капабіліті unhandledPromptBehavior; за замовчуванням за W3C діалог закривається з помилкою, але її можна змінити, наприклад, на автоматичне прийняття.

Pitfalls. A modal built with HTML and CSS is not an alert: it lives in the DOM, and switchTo().alert() throws NoAlertPresentException. If a native dialog pops up unexpectedly and nobody handles it, the next command gets UnhandledAlertException. The unhandledPromptBehavior capability decides what happens then; the W3C default dismisses the dialog and reports an error, but you can change it, for example to accept automatically.

Уточнення. Питають, чи варто всюди вмикати автоприйняття. Я б не став: несподіваний алерт часто і є багом, а тихе прийняття сховає його від тесту.

Follow-up. You may be asked whether to turn on auto-accept everywhere. I would not: an unexpected alert is often the bug itself, and silently accepting it hides that from the test.

Підтвердження видалення через confirm()
A delete confirmation through confirm()
driver.findElement(By.cssSelector("[data-test='delete-order-42']")).click();

Alert alert = new WebDriverWait(driver, Duration.ofSeconds(5))
        .until(ExpectedConditions.alertIsPresent());
assertThat(alert.getText()).isEqualTo("Delete order 42?");
alert.accept();                        // «OK»; dismiss() натиснув би «Скасувати»

// prompt(): текст вводиться в сам діалог
// alert.sendKeys("reason: duplicate");

// поведінка для алертів, які тест не чекав
ChromeOptions options = new ChromeOptions();
options.setUnhandledPromptBehaviour(UnexpectedAlertBehaviour.DISMISS_AND_NOTIFY);
driver.findElement(By.cssSelector("[data-test='delete-order-42']")).click();

Alert alert = new WebDriverWait(driver, Duration.ofSeconds(5))
        .until(ExpectedConditions.alertIsPresent());
assertThat(alert.getText()).isEqualTo("Delete order 42?");
alert.accept();                        // OK; dismiss() would press Cancel

// prompt(): the text goes into the dialog itself
// alert.sendKeys("reason: duplicate");

// behaviour for alerts the test did not expect
ChromeOptions options = new ChromeOptions();
options.setUnhandledPromptBehaviour(UnexpectedAlertBehaviour.DISMISS_AND_NOTIFY);
🔧 Розбір синтаксису: alertIsPresent() чекає діалог і повертає вже готовий Alert, тож окремий switchTo().alert() не потрібен. accept() відповідає кнопці OK, dismiss() — Cancel. setUnhandledPromptBehaviour (британське написання в назві методу) записує капабіліті unhandledPromptBehavior; UnexpectedAlertBehaviour має значення ACCEPT, DISMISS, ACCEPT_AND_NOTIFY, DISMISS_AND_NOTIFY та IGNORE.
🔧 Syntax breakdown: alertIsPresent() waits for the dialog and returns a ready Alert, so a separate switchTo().alert() is not needed. accept() maps to OK and dismiss() to Cancel. setUnhandledPromptBehaviour (British spelling in the method name) writes the unhandledPromptBehavior capability; UnexpectedAlertBehaviour has the values ACCEPT, DISMISS, ACCEPT_AND_NOTIFY, DISMISS_AND_NOTIFY and IGNORE.
💡 Простими словами: Нативний діалог браузера не має HTML — з ним говорять окремою «рацією» switchTo().alert(). Якщо діалог можна знайти в Elements, це звичайний елемент сторінки.
💡 In plain words: A native browser dialog has no HTML, so you talk to it over a separate channel, switchTo().alert(). If you can find the dialog in Elements, it is an ordinary page element.
🗣 Як читати вголос Спершу я з’ясовую, чи це справжній нативний алерт, чи модалка на HTML: відкриваю DevTools, і якщо вікно видно в дереві елементів, працюю з ним як з будь-яким елементом. Для нативного діалогу я чекаю alertIsPresent, читаю текст, щоб перевірити повідомлення, і вже потім приймаю чи відхиляю. Про unhandledPromptBehavior теж знаю, але автоприйняття не вмикаю бездумно, бо неочікуваний алерт часто і є тим багом, який тест мав упіймати.
🗣 How to say it out loud First I find out whether it is a real native alert or an HTML modal: I open DevTools, and if the dialog shows up in the element tree I treat it like any other element. For a native dialog I wait with alertIsPresent, read the text to check the message, and only then accept or dismiss it. I know about unhandledPromptBehavior as well, but I do not switch on auto-accept blindly, because an unexpected alert is often exactly the bug the test should catch.
32 Як завантажити файл на сервер через Selenium і чому не можна клікати по кнопці «Обрати файл»? How do you upload a file with Selenium, and why should you not click the Choose file button?
КонтекстиContexts Junior ▶

Що це. Upload у Selenium робиться через sendKeys з абсолютним шляхом до файлу, надісланим прямо в елемент <input type="file">. Драйвер сам підставляє файл у поле, і браузер поводиться так, ніби користувач обрав його в діалозі.

What it is. An upload in Selenium is done with sendKeys, sending the absolute file path straight into the <input type="file"> element. The driver attaches the file itself, and the browser behaves as if the user had picked it in the dialog.

Навіщо. Клік по кнопці відкриває системне вікно вибору файлу ОС, а воно вже поза межами браузера: WebDriver не бачить його і не може ним керувати. Тому будь-які спроби натиснути «Обрати файл» і потім «ввести шлях» закінчуються зависанням тесту.

Why it matters. Clicking the button opens the operating system file picker, which lives outside the browser: WebDriver cannot see or drive it. Any attempt to click Choose file and then type a path ends with a hanging test.

Приклад. Тест аватара бере файл з ресурсів проєкту, перетворює шлях на абсолютний через Path.toAbsolutePath() і шле його в поле. Для кількох файлів поле має мати атрибут multiple, а шляхи з’єднуються символом нового рядка. Після цього тест чекає прев’ю або рядок у списку завантажених.

Example. An avatar test takes a file from the project resources, turns the path into an absolute one with Path.toAbsolutePath() and sends it into the field. For several files the input needs the multiple attribute, and the paths are joined with a newline character. Then the test waits for the preview or a row in the uploaded list.

Підводні камені. Сучасні дизайни ховають справжній input і показують стилізовану кнопку. Якщо input має display: none, sendKeys може кинути ElementNotInteractableException; частіше input просто прозорий, і тоді все працює. Відносний шлях теж ламається, бо браузер не знає робочої теки тесту. На Selenium Grid файл лежить на машині тесту, а браузер — на ноді, тому потрібен LocalFileDetector: він передає файл на ноду перед завантаженням.

Pitfalls. Modern designs hide the real input behind a styled button. If the input has display: none, sendKeys may throw ElementNotInteractableException; more often the input is merely transparent and everything works. A relative path also fails, because the browser has no idea of the test working directory. On Selenium Grid the file sits on the test machine while the browser runs on a node, so you need LocalFileDetector, which ships the file to the node before the upload.

Уточнення. Буває, що питають про drag-and-drop зони без input. Там зазвичай все одно є прихований input, а якщо його немає, я тестую завантаження через API, а в UI лише перевіряю відображення.

Follow-up. You might get asked about drag-and-drop zones with no input. There is usually still a hidden input; if there really is none, I cover the upload through the API and in the UI only check how the result is displayed.

Один і кілька файлів, локально і на Grid
One or several files, locally and on Grid
Path avatar = Path.of("src/test/resources/avatar.png").toAbsolutePath();

// у RemoteWebDriver файл спершу копіюється на ноду Grid
if (driver instanceof RemoteWebDriver remote) {
    remote.setFileDetector(new LocalFileDetector());
}

WebElement input = driver.findElement(By.cssSelector("input[type='file']"));
input.sendKeys(avatar.toString());     // жодного кліку по кнопці

// кілька файлів: потрібен атрибут multiple, шляхи через \n
String both = avatar + "\n" + Path.of("src/test/resources/cv.pdf").toAbsolutePath();
driver.findElement(By.id("attachments")).sendKeys(both);
Path avatar = Path.of("src/test/resources/avatar.png").toAbsolutePath();

// with RemoteWebDriver the file is copied to the Grid node first
if (driver instanceof RemoteWebDriver remote) {
    remote.setFileDetector(new LocalFileDetector());
}

WebElement input = driver.findElement(By.cssSelector("input[type='file']"));
input.sendKeys(avatar.toString());     // no click on the button at all

// several files: needs the multiple attribute, paths joined by \n
String both = avatar + "\n" + Path.of("src/test/resources/cv.pdf").toAbsolutePath();
driver.findElement(By.id("attachments")).sendKeys(both);
🔧 Розбір синтаксису: Path.of(...).toAbsolutePath() з java.nio.file робить шлях абсолютним. instanceof RemoteWebDriver remote — pattern matching з Java 16+, який одразу оголошує змінну. setFileDetector(new LocalFileDetector()) каже клієнту перевіряти, чи рядок у sendKeys — локальний файл, і якщо так, завантажувати його на ноду. "\n" розділяє кілька шляхів.
🔧 Syntax breakdown: Path.of(...).toAbsolutePath() from java.nio.file makes the path absolute. instanceof RemoteWebDriver remote is Java 16+ pattern matching that declares the variable on the spot. setFileDetector(new LocalFileDetector()) tells the client to check whether a sendKeys string is a local file and, if so, upload it to the node. "\n" separates several paths.
💡 Простими словами: Файл не «обирають мишкою», а «вписують» шлях прямо в поле input. Системне вікно вибору файлу для Selenium невидиме.
💡 In plain words: You do not pick the file with the mouse, you write its path straight into the input. The system file picker is invisible to Selenium.
🗣 Як читати вголос Я ніколи не клікаю по кнопці вибору файлу, бо відкривається вікно операційної системи, яким Selenium керувати не вміє. Натомість знаходжу сам input з типом file і шлю в нього абсолютний шлях через sendKeys. Якщо input прихований стилями, перевіряю, чи він просто прозорий, чи справді має display none. А коли тести їдуть на Grid, вмикаю LocalFileDetector, бо файл лежить у мене на агенті, а браузер — на зовсім іншій машині.
🗣 How to say it out loud I never click the choose file button, because it opens an operating system window that Selenium cannot control. Instead I locate the input of type file and send the absolute path into it with sendKeys. If styling hides the input, I check whether it is merely transparent or really has display none. And when the tests run on Grid I turn on LocalFileDetector, because the file sits on my build agent while the browser runs on a completely different machine.
33 Як перевірити завантаження файлу з браузера в Selenium-тесті? How do you verify a file download in a Selenium test?
КонтекстиContexts Middle ▶

Що це. Selenium не має команди «перевір завантажений файл»: він бачить сторінку, а не файлову систему. Тому тест налаштовує браузер зберігати файли в відому теку без діалогу, клікає «Експорт», а потім уже засобами Java чекає появу файлу і перевіряє його ім’я, розмір і вміст.

What it is. Selenium has no command to check a downloaded file: it sees the page, not the file system. So the test configures the browser to save files into a known folder without a dialog, clicks Export, and then uses plain Java to wait for the file and check its name, size and content.

Навіщо. Звіти CSV, рахунки PDF і вивантаження Excel — часто бізнес-критичні функції. Перевірка лише того, що кнопка натиснулась, нічого не гарантує: файл може бути порожнім, з неправильним кодуванням чи назвою.

Why it matters. CSV reports, PDF invoices and Excel exports are often business-critical. Checking only that the button was clicked guarantees nothing: the file can be empty, in the wrong encoding or badly named.

Приклад. Для Chrome задаю prefs download.default_directory з унікальною текою на кожен тест і download.prompt_for_download=false. Після кліку чекаю через FluentWait, поки в теці з’явиться report.csv і зникне тимчасовий файл .crdownload. Далі читаю рядки і перевіряю заголовок та кількість записів.

Example. For Chrome I set the prefs download.default_directory to a unique folder per test and download.prompt_for_download=false. After the click I wait with a FluentWait until report.csv is in the folder and the temporary .crdownload file is gone. Then I read the lines and check the header and the number of records.

Підводні камені. Спільна тека для паралельних тестів дає змішані файли з чужих прогонів. Перевірка одразу після кліку ловить недописаний файл. На Grid файл з’являється на ноді, а не на машині тесту, тож локальна перевірка теки марна; Selenium 4 має керовані завантаження через options.setEnableDownloads(true), коли нода зберігає файл і віддає його клієнту на запит.

Pitfalls. A shared folder for parallel tests mixes files from other runs. Checking right after the click catches a half-written file. On Grid the file lands on the node, not on the test machine, so inspecting a local folder is pointless; Selenium 4 offers managed downloads through options.setEnableDownloads(true), where the node keeps the file and hands it to the client on request.

Уточнення. Альтернатива — не завантажувати через браузер взагалі: взяти href посилання, передати cookies сесії в HTTP-клієнт і скачати файл напряму. Це швидше і стабільніше, але не перевіряє саму поведінку браузера, наприклад заголовок Content-Disposition в дії.

Follow-up. An alternative is to skip the browser download entirely: read the link href, pass the session cookies to an HTTP client and fetch the file directly. It is faster and steadier, but it does not exercise the browser behaviour itself, such as the Content-Disposition header in action.

Окрема тека на тест і очікування готового файлу
A folder per test and waiting for a finished file
Path dir = Files.createTempDirectory("downloads");
Map<String, Object> prefs = new HashMap<>();
prefs.put("download.default_directory", dir.toString());
prefs.put("download.prompt_for_download", false);
ChromeOptions options = new ChromeOptions();
options.setExperimentalOption("prefs", prefs);
WebDriver driver = new ChromeDriver(options);

driver.get("https://shop.example.com/reports");
driver.findElement(By.id("export-csv")).click();

Path report = dir.resolve("report.csv");
new FluentWait<>(dir)
        .withTimeout(Duration.ofSeconds(30))
        .pollingEvery(Duration.ofMillis(500))
        .until(d -> Files.exists(report)
                && !Files.exists(d.resolve("report.csv.crdownload"))); // файл дописано

List<String> lines = Files.readAllLines(report);
assertThat(lines.get(0)).isEqualTo("id,total,status"); // заголовок CSV
assertThat(lines).hasSize(11);                         // заголовок + 10 рядків
Path dir = Files.createTempDirectory("downloads");
Map<String, Object> prefs = new HashMap<>();
prefs.put("download.default_directory", dir.toString());
prefs.put("download.prompt_for_download", false);
ChromeOptions options = new ChromeOptions();
options.setExperimentalOption("prefs", prefs);
WebDriver driver = new ChromeDriver(options);

driver.get("https://shop.example.com/reports");
driver.findElement(By.id("export-csv")).click();

Path report = dir.resolve("report.csv");
new FluentWait<>(dir)
        .withTimeout(Duration.ofSeconds(30))
        .pollingEvery(Duration.ofMillis(500))
        .until(d -> Files.exists(report)
                && !Files.exists(d.resolve("report.csv.crdownload"))); // fully written

List<String> lines = Files.readAllLines(report);
assertThat(lines.get(0)).isEqualTo("id,total,status"); // CSV header
assertThat(lines).hasSize(11);                         // header + 10 rows
🔧 Розбір синтаксису: setExperimentalOption("prefs", prefs) передає Chrome налаштування профілю. Files.createTempDirectory дає унікальну теку, тож паралельні тести не заважають одне одному. FluentWait тут параметризований не драйвером, а Path: лямбда отримує теку і повертає true, коли файл готовий. .crdownload — тимчасове розширення, яке Chrome знімає після завершення.
🔧 Syntax breakdown: setExperimentalOption("prefs", prefs) passes profile settings to Chrome. Files.createTempDirectory gives a unique folder, so parallel tests do not step on each other. The FluentWait here is typed with a Path rather than the driver: the lambda gets the folder and returns true once the file is ready. .crdownload is the temporary extension Chrome removes when it finishes.
💡 Простими словами: Браузер зберігає файл, а перевіряє його вже звичайний Java-код. Головне — знати, куди файл ляже, і дочекатися, поки він допишеться.
💡 In plain words: The browser saves the file and ordinary Java code checks it. The key is knowing where it lands and waiting until it is fully written.
🗣 Як читати вголос Я розділяю цю задачу на дві частини. Браузеру через prefs кажу зберігати файли без діалогу в окрему тимчасову теку, свою для кожного тесту. А далі працює звичайна Java: FluentWait чекає, поки з’явиться файл і зникне crdownload, потім я читаю вміст і перевіряю заголовки та кількість рядків. На Grid вмикаю керовані завантаження. А якщо мені важливий лише вміст файлу, простіше скачати його HTTP-клієнтом з cookies сесії.
🗣 How to say it out loud I split this job into two parts. Through prefs I tell the browser to save files without a dialog into a separate temp folder, one per test. From there it is plain Java: a FluentWait waits until the file shows up and the crdownload file is gone, then I read the content and check the header and row count. On Grid I switch on managed downloads. And if only the file content matters to me, it is simpler to fetch it with an HTTP client carrying the session cookies.
34 Як автоматично зберігати скріншот при падінні тесту і чому його часто знімають уже із закритого браузера? How do you save a screenshot automatically when a test fails, and why is it often taken from an already closed browser?
КонтекстиContexts Middle ▶

Що це. Будь-який драйвер Selenium реалізує інтерфейс TakesScreenshot: getScreenshotAs(OutputType.BYTES) або OutputType.FILE повертає знімок видимої області вікна. У Selenium 4 WebElement теж уміє знімати себе, що зручно для перевірки конкретного віджета.

What it is. Every Selenium driver implements the TakesScreenshot interface: getScreenshotAs(OutputType.BYTES) or OutputType.FILE returns an image of the visible part of the window. In Selenium 4 a WebElement can capture itself too, which is handy for checking one specific widget.

Навіщо. Стектрейс «елемент не клікабельний» мало що каже, а скріншот одразу показує банер cookies, спінер чи сторінку помилки 500. Разом із HTML сторінки і поточним URL це три артефакти, з якими падіння в CI можна розібрати без локального відтворення.

Why it matters. A stack trace saying the element is not clickable tells you little, while a screenshot instantly shows a cookie banner, a spinner or a 500 error page. Together with the page HTML and the current URL, those are three artefacts that let you analyse a CI failure without reproducing it locally.

Приклад. У JUnit 5 я пишу розширення, що реалізує AfterTestExecutionCallback. Воно викликається одразу після тіла тесту, ще до методів @AfterEach, перевіряє context.getExecutionException() і, якщо там є помилка, зберігає PNG і getPageSource() у теку артефактів з назвою тесту.

Example. In JUnit 5 I write an extension implementing AfterTestExecutionCallback. It runs right after the test body and before any @AfterEach method, looks at context.getExecutionException() and, if an error is present, saves a PNG and getPageSource() into the artefacts folder under the test name.

Підводні камені. Класична помилка — знімати скріншот у TestWatcher.testFailed: цей колбек спрацьовує вже після @AfterEach, де драйвер закрито через quit(), і замість знімка маємо ще одну помилку. Друге: знімок охоплює лише видиму частину, а не всю сторінку, тож елемент нижче згину може в кадр не потрапити. Третє: у headless без заданого розміру вікна знімок буде маленьким і незрозумілим.

Pitfalls. The classic mistake is taking the screenshot in TestWatcher.testFailed: that callback fires after @AfterEach, where the driver has already been shut with quit(), so instead of an image you get one more exception. Second, the capture covers only the visible area, not the whole page, so an element below the fold may be out of frame. Third, in headless mode without a set window size the image comes out small and hard to read.

Уточнення. Питають про повносторінкові знімки. У Firefox є FirefoxDriver.getFullPageScreenshotAs, у Chromium зазвичай йдуть через CDP; я б не робив цього на кожне падіння, бо файли великі.

Follow-up. Full-page captures come up too. Firefox has FirefoxDriver.getFullPageScreenshotAs, while for Chromium people usually go through CDP; I would not do it on every failure, since the files are large.

JUnit 5-розширення, що встигає до quit()
A JUnit 5 extension that runs before quit()
public interface HasDriver { WebDriver driver(); }

public class ScreenshotOnFailure implements AfterTestExecutionCallback {
    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        if (context.getExecutionException().isEmpty()) return; // тест пройшов
        WebDriver driver = ((HasDriver) context.getRequiredTestInstance()).driver();
        Path dir = Files.createDirectories(Path.of("target/artifacts"));
        String name = context.getRequiredTestMethod().getName();

        byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
        Files.write(dir.resolve(name + ".png"), png);
        Files.writeString(dir.resolve(name + ".html"), driver.getPageSource());
    }
}

// знімок одного елемента (Selenium 4)
File chart = driver.findElement(By.id("sales-chart")).getScreenshotAs(OutputType.FILE);
public interface HasDriver { WebDriver driver(); }

public class ScreenshotOnFailure implements AfterTestExecutionCallback {
    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        if (context.getExecutionException().isEmpty()) return; // the test passed
        WebDriver driver = ((HasDriver) context.getRequiredTestInstance()).driver();
        Path dir = Files.createDirectories(Path.of("target/artifacts"));
        String name = context.getRequiredTestMethod().getName();

        byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
        Files.write(dir.resolve(name + ".png"), png);
        Files.writeString(dir.resolve(name + ".html"), driver.getPageSource());
    }
}

// a capture of one element (Selenium 4)
File chart = driver.findElement(By.id("sales-chart")).getScreenshotAs(OutputType.FILE);
🔧 Розбір синтаксису: getExecutionException() повертає Optional<Throwable>: порожній означає успіх. getRequiredTestInstance() дає об’єкт тестового класу, а інтерфейс HasDriver дозволяє дістати з нього драйвер без рефлексії. Каст до TakesScreenshot потрібен, бо WebDriver сам цього методу не має. Розширення підключають анотацією @ExtendWith(ScreenshotOnFailure.class).
🔧 Syntax breakdown: getExecutionException() returns an Optional<Throwable>: empty means success. getRequiredTestInstance() gives the test class object, and the HasDriver interface lets you pull the driver out of it without reflection. The cast to TakesScreenshot is needed because WebDriver itself lacks that method. You register the extension with @ExtendWith(ScreenshotOnFailure.class).
💡 Простими словами: Фотографуй місце аварії, поки його не прибрали: скріншот має бути знятий до того, як тест закриє браузер.
💡 In plain words: Photograph the crash site before it is cleaned up: the screenshot has to be taken before the test closes the browser.
🗣 Як читати вголос У мене скріншот при падінні робить JUnit-розширення на AfterTestExecutionCallback. Я обрав саме його, бо воно спрацьовує до AfterEach, коли браузер ще живий. Колись я робив це в TestWatcher і отримував помилку замість картинки, бо драйвер уже був закритий. Разом із PNG я зберігаю HTML сторінки, і в CI ці файли прикріплюються до звіту. Ще я завжди задаю розмір вікна в headless, інакше знімок виходить обрізаним.
🗣 How to say it out loud In my framework a JUnit extension built on AfterTestExecutionCallback takes the failure screenshot. I chose that callback because it fires before AfterEach, while the browser is still alive. I once did it in a TestWatcher and got an exception instead of an image, because the driver was already gone. Along with the PNG I store the page HTML, and CI attaches both files to the report. I also always set a window size in headless mode, otherwise the image comes out cropped.
35 Як працювати з cookies через Selenium і як з їхньою допомогою пропустити логін через UI? How do you work with cookies in Selenium, and how can they help you skip logging in through the UI?
КонтекстиContexts Middle ▶

Що це. Cookies поточного домену доступні через driver.manage(): addCookie, getCookies, getCookieNamed, deleteCookieNamed і deleteAllCookies. Об’єкт Cookie має ім’я, значення, домен, шлях, термін дії та прапорці secure і HttpOnly.

What it is. Cookies of the current domain are available through driver.manage(): addCookie, getCookies, getCookieNamed, deleteCookieNamed and deleteAllCookies. A Cookie object has a name, value, domain, path, expiry and the secure and HttpOnly flags.

Навіщо. Найцінніше застосування — швидкий вхід. Замість того щоб у кожному тесті проходити форму логіну, отримуємо токен сесії через API і кладемо його в браузер як cookie. Тест стартує одразу з потрібної сторінки, а форму логіну перевіряє один окремий тест. Також cookies допомагають перевірити, що після логауту сесійна cookie справді зникла.

Why it matters. The most valuable use is a fast login. Instead of filling in the login form in every test, you get a session token from the API and put it into the browser as a cookie. The test starts right on the page it needs, and one dedicated test covers the login form. Cookies also let you check that the session cookie really disappears after logout.

Приклад. Тест викликає POST /api/login, бере значення SESSION, відкриває будь-яку легку сторінку домену, наприклад /robots.txt, додає cookie, робить refresh() і переходить у кабінет уже залогіненим.

Example. The test calls POST /api/login, takes the SESSION value, opens any lightweight page of the domain such as /robots.txt, adds the cookie, calls refresh() and moves on to the account area already signed in.

Підводні камені. Додати cookie можна лише для домену, який зараз відкритий: виклик на about:blank або іншому сайті дає InvalidCookieDomainException. Прапорець secure вимагає HTTPS. deleteAllCookies() чистить лише поточний домен, а не весь профіль. Якщо застосунок тримає сесію не в cookie, а в localStorage з JWT, такий трюк не спрацює, і токен треба класти через JavaScript.

Pitfalls. You can only add a cookie for the domain that is currently open: doing it on about:blank or another site raises InvalidCookieDomainException. The secure flag requires HTTPS. deleteAllCookies() clears only the current domain, not the whole profile. If the app keeps the session in localStorage as a JWT instead of a cookie, this trick fails and the token has to be set through JavaScript.

Уточнення. Чи бачить Selenium HttpOnly-cookies? Так: заборона HttpOnly стосується document.cookie у JavaScript сторінки, а WebDriver читає і пише їх через протокол.

Follow-up. Can Selenium see HttpOnly cookies? Yes: the HttpOnly restriction applies to document.cookie in page JavaScript, while WebDriver reads and writes them through the protocol.

Логін через cookie сесії з API
Logging in with a session cookie from the API
String token = authApi.login("buyer@example.com", "s3cret"); // сесія через API

driver.get("https://shop.example.com/robots.txt"); // спершу відкрити домен
Cookie session = new Cookie.Builder("SESSION", token)
        .domain("shop.example.com")
        .path("/")
        .isSecure(true)
        .isHttpOnly(true)
        .build();
driver.manage().addCookie(session);
driver.navigate().refresh();

driver.get("https://shop.example.com/account");
assertThat(driver.findElement(By.id("user-name")).getText()).isEqualTo("Buyer");

// після логауту cookie має зникнути
driver.findElement(By.id("logout")).click();
assertThat(driver.manage().getCookieNamed("SESSION")).isNull();
String token = authApi.login("buyer@example.com", "s3cret"); // session via the API

driver.get("https://shop.example.com/robots.txt"); // open the domain first
Cookie session = new Cookie.Builder("SESSION", token)
        .domain("shop.example.com")
        .path("/")
        .isSecure(true)
        .isHttpOnly(true)
        .build();
driver.manage().addCookie(session);
driver.navigate().refresh();

driver.get("https://shop.example.com/account");
assertThat(driver.findElement(By.id("user-name")).getText()).isEqualTo("Buyer");

// after logout the cookie must be gone
driver.findElement(By.id("logout")).click();
assertThat(driver.manage().getCookieNamed("SESSION")).isNull();
🔧 Розбір синтаксису: authApi.login — умовний API-клієнт проєкту, який повертає значення сесії. Cookie.Builder дозволяє задати домен, шлях і прапорці; простіший варіант — new Cookie("SESSION", token). navigate().refresh() перезавантажує сторінку, щоб сервер уже побачив cookie. getCookieNamed повертає null, якщо cookie немає.
🔧 Syntax breakdown: authApi.login stands for the project API client that returns the session value. Cookie.Builder lets you set domain, path and flags; the simpler form is new Cookie("SESSION", token). navigate().refresh() reloads the page so the server sees the cookie. getCookieNamed returns null when there is no such cookie.
💡 Простими словами: Cookie сесії — це перепустка. Можна щоразу стояти в черзі на прохідній, а можна отримати перепустку через API і зайти одразу.
💡 In plain words: A session cookie is a pass. You can queue at the front desk every time, or get the pass through the API and walk straight in.
🗣 Як читати вголос Cookies я використовую передусім для швидкого входу. Токен сесії беру з API, потім відкриваю будь-яку легку сторінку потрібного домену, бо без цього Selenium не дасть додати cookie, кладу її і оновлюю сторінку. Так кожен тест економить кілька секунд, а форму логіну перевіряє окремий тест. Ще перевіряю логаут через getCookieNamed. І пам’ятаю, що HttpOnly мені не заважає, а от якщо сесія в localStorage, підхід доведеться змінити.
🗣 How to say it out loud I use cookies mainly for a quick login. I take the session token from the API, then open any light page on the right domain, because Selenium will not let me add a cookie otherwise, set the cookie and refresh. Each test saves a few seconds that way, and a separate test covers the login form. I also verify logout with getCookieNamed. And I keep in mind that HttpOnly does not get in my way, but if the session lives in localStorage I need a different approach.
▶ 7 4
36 Що таке Page Object і як виглядає повноцінний клас LoginPage разом із тестом? What is a Page Object, and what does a complete LoginPage class look like together with a test?
Page ObjectPage Object Junior ▶

Що це. Page Object — клас, який описує одну сторінку або великий компонент: зберігає її локатори і надає методи-дії мовою предметної області, наприклад loginAs(email, password). Тест викликає ці методи і робить перевірки, а про CSS-селектори й очікування нічого не знає.

What it is. A Page Object is a class that describes one page or a large component: it holds that page's locators and offers action methods in domain language, such as loginAs(email, password). The test calls those methods and makes the assertions, knowing nothing about CSS selectors or waits.

Навіщо. Коли розробники перейменують id поля пароля, виправити доведеться один рядок у LoginPage, а не сорок тестів. Тести читаються як сценарій, а не як набір кліків, і їх легше рев’ювити бізнес-аналітику чи новачку в команді.

Why it matters. When developers rename the id of the password field, you fix one line in LoginPage instead of forty tests. Tests read like a scenario rather than a list of clicks, and a business analyst or a newcomer to the team can review them more easily.

Приклад. LoginPage тримає By-поля, отримує WebDriver у конструкторі і має два методи входу: успішний повертає DashboardPage, а невдалий — ту саму LoginPage, щоб тест міг прочитати текст помилки. Такий стиль називають fluent: кожен метод повертає сторінку, на якій користувач опиниться після дії, і ланцюжок викликів сам документує навігацію.

Example. LoginPage keeps By fields, receives a WebDriver in its constructor and has two login methods: the successful one returns a DashboardPage, while the failing one returns the same LoginPage, so the test can read the error text. This fluent style means each method returns the page the user lands on after the action, and the call chain documents navigation by itself.

Підводні камені. Асерти всередині page object — поширена помилка: сторінка перестає бути перевикористовуваною, бо жорстко вирішує, що вважати правильним. Друга помилка — «божественний» клас на 800 рядків для всього застосунку; краще ділити на сторінки й компоненти на кшталт HeaderComponent. Третя — зберігати знайдені WebElement у полях, що веде до stale-винятків.

Pitfalls. Assertions inside a page object are a common mistake: the page stops being reusable because it decides on its own what counts as correct. The second mistake is a god class of 800 lines for the whole application; splitting into pages and components like HeaderComponent works better. The third is storing found WebElement objects in fields, which leads to stale exceptions.

Уточнення. Часто питають, де мають жити очікування. Розумний варіант — усередині методів page object, бо саме сторінка знає, який стан означає «готова до дії».

Follow-up. A frequent question is where waits should live. A sensible answer is inside page object methods, because the page is what knows which state means ready for the next action.

LoginPage і тест JUnit 5
LoginPage and a JUnit 5 test
public class LoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;
    private final By email = By.id("email");
    private final By password = By.id("password");
    private final By submit = By.cssSelector("[data-testid='login-submit']");
    private final By error = By.cssSelector("[role='alert']");

    public LoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    public LoginPage open(String baseUrl) {
        driver.get(baseUrl + "/login");
        wait.until(ExpectedConditions.visibilityOfElementLocated(email));
        return this;
    }

    public DashboardPage loginAs(String user, String pass) {
        fillAndSubmit(user, pass);
        return new DashboardPage(driver); // наступна сторінка
    }

    public LoginPage loginExpectingError(String user, String pass) {
        fillAndSubmit(user, pass);
        wait.until(ExpectedConditions.visibilityOfElementLocated(error));
        return this; // лишаємось на сторінці входу
    }

    public String errorText() {
        return driver.findElement(error).getText();
    }

    private void fillAndSubmit(String user, String pass) {
        driver.findElement(email).sendKeys(user);
        driver.findElement(password).sendKeys(pass);
        driver.findElement(submit).click();
    }
}

// Тест: перевірки живуть тут, а не в сторінці
@Test
void wrongPasswordShowsError() {
    String message = new LoginPage(driver)
        .open(BASE_URL)
        .loginExpectingError("anna@example.com", "wrong-pass")
        .errorText();
    assertEquals("Invalid email or password", message);
}
public class LoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;
    private final By email = By.id("email");
    private final By password = By.id("password");
    private final By submit = By.cssSelector("[data-testid='login-submit']");
    private final By error = By.cssSelector("[role='alert']");

    public LoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    public LoginPage open(String baseUrl) {
        driver.get(baseUrl + "/login");
        wait.until(ExpectedConditions.visibilityOfElementLocated(email));
        return this;
    }

    public DashboardPage loginAs(String user, String pass) {
        fillAndSubmit(user, pass);
        return new DashboardPage(driver); // the next page
    }

    public LoginPage loginExpectingError(String user, String pass) {
        fillAndSubmit(user, pass);
        wait.until(ExpectedConditions.visibilityOfElementLocated(error));
        return this; // we stay on the login page
    }

    public String errorText() {
        return driver.findElement(error).getText();
    }

    private void fillAndSubmit(String user, String pass) {
        driver.findElement(email).sendKeys(user);
        driver.findElement(password).sendKeys(pass);
        driver.findElement(submit).click();
    }
}

// Test: assertions live here, not in the page
@Test
void wrongPasswordShowsError() {
    String message = new LoginPage(driver)
        .open(BASE_URL)
        .loginExpectingError("anna@example.com", "wrong-pass")
        .errorText();
    assertEquals("Invalid email or password", message);
}
🔧 Розбір синтаксису: private final By — локатори як незмінні рецепти пошуку, приховані від тестів. Конструктор приймає драйвер ззовні, тож сторінка не створює і не закриває браузер. Публічні методи повертають об’єкт наступної сторінки або this, що дає ланцюжок .open(...).loginExpectingError(...).errorText(). Приватний fillAndSubmit прибирає дублювання між двома сценаріями входу, а assertEquals з JUnit 5 стоїть лише в тесті.
🔧 Syntax breakdown: private final By keeps locators as immutable search recipes hidden from tests. The constructor takes the driver from outside, so the page never creates or closes a browser. Public methods return the next page object or this, which enables the chain .open(...).loginExpectingError(...).errorText(). The private fillAndSubmit removes duplication between the two login scenarios, and JUnit 5 assertEquals appears only in the test.
💡 Простими словами: Сторінка знає, як щось зробити, тест знає, що має вийти. Не міняй їх ролями.
💡 In plain words: The page knows how to do things, the test knows what the outcome should be. Do not swap those roles.
🗣 Як читати вголос Page Object я пояснюю як шар між тестом і HTML. Кожна сторінка — клас із приватними локаторами і методами на кшталт loginAs, а тест читається як сценарій. Методи в мене повертають наступну сторінку: успішний вхід дає DashboardPage, невдалий лишає LoginPage, звідки я беру текст помилки. Асертів у сторінках не тримаю, щоб їх можна було перевикористати в різних перевірках. А коли змінюється верстка, я правлю один клас замість десятків тестів.
🗣 How to say it out loud I explain Page Object as a layer between the test and the HTML. Each page is a class with private locators and methods like loginAs, and the test reads like a scenario. My methods return the next page: a successful login gives a DashboardPage, a failed one keeps the LoginPage, where I read the error text. I keep assertions out of pages so they can be reused by different checks. And when the markup changes, I edit one class instead of dozens of tests.
37 Як працюють PageFactory і @FindBy і чому їх більше не радять використовувати? How do PageFactory and @FindBy work, and why are they no longer recommended?
Page ObjectPage Object Middle ▶

Що це. PageFactory з модуля selenium-support читає поля класу з анотаціями @FindBy і під час PageFactory.initElements(driver, this) підставляє в них проксі-об’єкти. Реального пошуку в цей момент немає: проксі шукає елемент лише тоді, коли ти викликаєш на ньому метод, і за замовчуванням робить це знову при кожному виклику.

What it is. PageFactory from the selenium-support module reads class fields annotated with @FindBy and, during PageFactory.initElements(driver, this), puts proxy objects into them. No real lookup happens at that moment: the proxy searches for the element only when you call a method on it, and by default it searches again on every call.

Навіщо знати. PageFactory досі трапляється в старих фреймворках, і на співбесіді перевіряють, чи розумієш ти його «магію». Проблема в тому, що вона ховає таймінг: у коді немає видимого моменту пошуку, тож незрозуміло, коли саме впаде NoSuchElementException і чому. Команда Selenium неодноразово радила не використовувати PageFactory, а в офіційній документації про Page Object приклади написано з By.

Why it matters. PageFactory still shows up in older frameworks, and interviewers check whether you understand its magic. The problem is that it hides timing: there is no visible point where the search happens, so it is unclear when and why NoSuchElementException will fire. The Selenium team has repeatedly advised against PageFactory, and the official Page Object documentation writes its examples with By.

Приклад. Поле @FindBy(id = "email") WebElement email виглядає компактно, але не має очікування. AjaxElementLocatorFactory з таймаутом змушує проксі повторювати пошук кілька секунд, проте чекає лише присутності в DOM, а не видимості чи клікабельності. Анотації також не приймають змінних, тож локатор рядка таблиці з динамічним id так не описати.

Example. A field @FindBy(id = "email") WebElement email looks compact but has no waiting at all. AjaxElementLocatorFactory with a timeout makes the proxy retry the lookup for a few seconds, yet it waits only for presence in the DOM, not for visibility or clickability. Annotations also cannot take variables, so a locator for a table row with a dynamic id cannot be expressed this way.

Підводні камені. @CacheLookup кешує знайдений елемент і на сторінках, що перемальовуються, дає stale-винятки. Поля-проксі погано поєднуються з ExpectedConditions, які очікують By, тож команди змішують два стилі. Юніт-тестам та рефакторингу теж заважає рефлексія.

Pitfalls. @CacheLookup caches the found element and causes stale exceptions on pages that re-render. Proxy fields fit poorly with ExpectedConditions, which expect a By, so teams end up mixing two styles. The reflection also gets in the way of refactoring and IDE navigation.

Уточнення. Що замість: звичайні By-поля плюс явні очікування в методах сторінки. Код стає трохи довшим, зате кожен пошук і кожне очікування видно.

Follow-up. What to use instead: plain By fields plus explicit waits inside page methods. The code gets slightly longer, but every lookup and every wait is visible.

Той самий фрагмент: PageFactory і явний стиль
The same fragment: PageFactory versus the explicit style
// Було: проксі без очікувань, пошук прихований
public class SearchPage {
    @FindBy(id = "query")
    private WebElement query;

    @FindBy(css = "[data-testid='result']")
    private List<WebElement> results;

    public SearchPage(WebDriver driver) {
        PageFactory.initElements(new AjaxElementLocatorFactory(driver, 5), this);
    }
}

// Стало: By-поля, явне очікування, динамічний локатор
public class SearchPageExplicit {
    private final WebDriverWait wait;
    private final By query = By.id("query");

    public SearchPageExplicit(WebDriver driver) {
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    public WebElement result(String sku) {
        By row = By.cssSelector("[data-sku='" + sku + "']");
        return wait.until(ExpectedConditions.visibilityOfElementLocated(row));
    }
}
// Before: proxies with no waits, lookup is hidden
public class SearchPage {
    @FindBy(id = "query")
    private WebElement query;

    @FindBy(css = "[data-testid='result']")
    private List<WebElement> results;

    public SearchPage(WebDriver driver) {
        PageFactory.initElements(new AjaxElementLocatorFactory(driver, 5), this);
    }
}

// After: By fields, explicit wait, dynamic locator
public class SearchPageExplicit {
    private final WebDriverWait wait;
    private final By query = By.id("query");

    public SearchPageExplicit(WebDriver driver) {
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    public WebElement result(String sku) {
        By row = By.cssSelector("[data-sku='" + sku + "']");
        return wait.until(ExpectedConditions.visibilityOfElementLocated(row));
    }
}
🔧 Розбір синтаксису: @FindBy(id = ...) і @FindBy(css = ...) описують локатор константою в анотації; поле типу List<WebElement> отримує проксі на список. new AjaxElementLocatorFactory(driver, 5) — фабрика локаторів, яка повторює пошук до 5 секунд. У другому класі метод result(String sku) будує By з параметра, чого анотація зробити не може.
🔧 Syntax breakdown: @FindBy(id = ...) and @FindBy(css = ...) describe a locator as a constant in an annotation; a List<WebElement> field receives a proxy for a list. new AjaxElementLocatorFactory(driver, 5) is a locator factory that retries the lookup for up to 5 seconds. In the second class the method result(String sku) builds a By from a parameter, which an annotation cannot do.
💡 Простими словами: PageFactory економить кілька рядків, але ховає, коли й як шукається елемент. У нестабільних тестах саме це й треба бачити.
💡 In plain words: PageFactory saves a few lines but hides when and how an element is found. In flaky tests that is exactly what you need to see.
🗣 Як читати вголос Я знаю, як PageFactory працює: initElements підставляє в поля з @FindBy проксі, і пошук відбувається при кожному зверненні до поля. Але в нових проєктах я його не беру. Він не чекає нічого, крім присутності навіть з AjaxElementLocatorFactory, анотації не приймають змінних, а @CacheLookup породжує stale. Команда Selenium теж радить обходитися без нього. Тому я пишу By-поля і явні очікування в методах сторінки: трохи довше, але прозоро.
🗣 How to say it out loud I know how PageFactory works: initElements puts proxies into the @FindBy fields and the lookup happens on every access to the field. But I do not pick it for new projects. It waits for nothing beyond presence even with AjaxElementLocatorFactory, the annotations cannot take variables, and @CacheLookup breeds stale references. The Selenium team also advises doing without it. So I write By fields and explicit waits in page methods: a bit longer, but transparent.
38 Як готувати тестові дані для UI-тестів Selenium, щоб вони були швидкими й не заважали одне одному в паралелі? How do you prepare test data for Selenium UI tests so they are fast and do not interfere with each other in parallel?
Page ObjectPage Object Senior ▶

Що це. Підготовка даних — усе, що має існувати до перевірки: користувач, його замовлення, залишки на складі, авторизована сесія. Принцип такий: через UI проходимо лише той сценарій, який перевіряємо, а передумови створюємо через API, тестові ендпоїнти або напряму в базі.

What it is. Data setup is everything that must exist before the check: a user, their orders, stock levels, an authenticated session. The principle is to go through the UI only for the scenario under test and to create the preconditions through the API, test endpoints or directly in the database.

Навіщо. Реєстрація через форму займає 15–20 секунд і сама по собі може впасти, тож тест історії замовлень падатиме через баг у реєстрації, хоча перевіряє інше. Виклик API займає десятки мілісекунд. На наборі з 400 тестів різниця вимірюється годинами, а звіт стає чеснішим: кожен тест падає лише зі своєї причини.

Why it matters. Signing up through the form takes 15 to 20 seconds and can fail on its own, so an order history test would fail because of a registration bug while checking something else. An API call takes tens of milliseconds. Across a suite of 400 tests the difference is measured in hours, and the report becomes more honest: each test fails only for its own reason.

Приклад. У @BeforeEach клієнт тестового API створює унікального покупця з email на основі UUID і замовлення для нього, а потім повертає токен сесії. Тест відкриває будь-яку легку сторінку домену, додає cookie сесії і одразу переходить до історії замовлень, пропускаючи форму входу. У @AfterEach дані видаляються тим самим API.

Example. In @BeforeEach a test data API client creates a unique buyer with a UUID-based email and an order for them, then returns a session token. The test opens any lightweight page on the domain, adds the session cookie and goes straight to order history, skipping the login form. In @AfterEach the data is removed through the same API.

Підводні камені. Спільний акаунт test@company.com на весь набір — головне джерело конфліктів у паралелі: один тест скасовує замовлення, яке інший якраз перевіряє. Прямий запис у базу обходить бізнес-логіку і може створити стан, неможливий у продакшені, тому API надійніший. Тестові ендпоїнти мають бути закриті на продакшен-середовищі. І хоча б один тест має проходити реєстрацію та вхід саме через UI.

Pitfalls. One shared test@company.com account for the whole suite is the main source of conflicts in parallel runs: one test cancels an order that another is checking at that moment. Writing straight to the database bypasses business logic and can create a state that is impossible in production, so the API is safer. Test endpoints must be disabled in production. And at least one test should still go through sign-up and login in the UI.

Уточнення. Якщо прибрання не вдалося, допомагають префікси або теги в даних і нічне чищення за віком запису, щоб середовище не заростало сміттям.

Follow-up. If cleanup fails, prefixes or tags on the data plus a nightly job that deletes records by age keep the environment from filling up with junk.

Дані через API, сесія через cookie
Data through the API, session through a cookie
class OrderHistoryTest extends BaseUiTest {
    private final TestDataApi api = new TestDataApi(API_URL);
    private TestUser buyer;

    @BeforeEach
    void createData() {
        // Унікальний покупець: паралельні тести не перетинаються
        buyer = api.createUser("buyer-" + UUID.randomUUID() + "@test.example");
        api.createOrder(buyer.id(), "SKU-42", 2);
    }

    @Test
    void newOrderIsShownInHistory() {
        driver.get(BASE_URL + "/robots.txt"); // cookie можна додати лише на своєму домені
        driver.manage().addCookie(new Cookie("SESSION", api.sessionToken(buyer)));
        driver.get(BASE_URL + "/account/orders");

        assertEquals(1, new OrdersPage(driver).rowsFor("SKU-42"));
    }

    @AfterEach
    void cleanUp() {
        api.deleteUser(buyer.id()); // разом із замовленнями
    }
}
class OrderHistoryTest extends BaseUiTest {
    private final TestDataApi api = new TestDataApi(API_URL);
    private TestUser buyer;

    @BeforeEach
    void createData() {
        // A unique buyer: parallel tests never overlap
        buyer = api.createUser("buyer-" + UUID.randomUUID() + "@test.example");
        api.createOrder(buyer.id(), "SKU-42", 2);
    }

    @Test
    void newOrderIsShownInHistory() {
        driver.get(BASE_URL + "/robots.txt"); // a cookie can be added only on its own domain
        driver.manage().addCookie(new Cookie("SESSION", api.sessionToken(buyer)));
        driver.get(BASE_URL + "/account/orders");

        assertEquals(1, new OrdersPage(driver).rowsFor("SKU-42"));
    }

    @AfterEach
    void cleanUp() {
        api.deleteUser(buyer.id()); // together with the orders
    }
}
🔧 Розбір синтаксису: TestDataApi і TestUser — власні класи фреймворку поверх HTTP-клієнта, наприклад REST Assured або java.net.http.HttpClient. UUID.randomUUID() дає унікальний email на кожен запуск. driver.manage().addCookie(new Cookie(name, value)) додає cookie для поточного домену, тому спершу відкривається легка сторінка цього домену. @BeforeEach і @AfterEach з JUnit 5 обгортають кожен тест створенням і прибиранням.
🔧 Syntax breakdown: TestDataApi and TestUser are the framework's own classes on top of an HTTP client such as REST Assured or java.net.http.HttpClient. UUID.randomUUID() gives a unique email for every run. driver.manage().addCookie(new Cookie(name, value)) adds a cookie for the current domain, which is why a lightweight page of that domain is opened first. JUnit 5 @BeforeEach and @AfterEach wrap every test with creation and cleanup.
💡 Простими словами: Браузер — для того, що перевіряєш. Усе, що має просто існувати до перевірки, створюй швидшими каналами.
💡 In plain words: The browser is for what you are checking. Everything that simply has to exist beforehand should come through faster channels.
🗣 Як читати вголос Моє правило: через UI я проходжу лише сценарій, який перевіряю, а передумови створюю через API. Кожен тест у @BeforeEach отримує власного користувача з унікальним email, тож паралельні потоки не ламають дані один одному. Щоб не проходити форму входу щоразу, я беру токен з API і кладу його в cookie. Після тесту дані видаляю, а на випадок збою є нічне чищення за префіксом. Окремо лишаю кілька тестів, що перевіряють сам вхід і реєстрацію через інтерфейс.
🗣 How to say it out loud My rule is that I use the UI only for the scenario under test and create preconditions through the API. Every test gets its own user with a unique email in @BeforeEach, so parallel threads never break each other's data. To avoid the login form every time I take a token from the API and put it into a cookie. After the test I delete the data, and a nightly cleanup by prefix covers crashes. I still keep a few tests that check sign-up and login themselves through the interface.
39 Як ви організуєте створення, налаштування і закриття WebDriver у фреймворку, щоб той самий тест ішов локально і на Grid? How do you organise creating, configuring and closing WebDriver in a framework so the same test runs locally and on Grid?
Page ObjectPage Object Senior ▶

Що це. Життєвий цикл драйвера — це відповіді на три питання: хто створює браузер, звідки береться конфігурація і хто гарантовано його закриває. У зрілому фреймворку це одне місце: DriverFactory для створення і розширення JUnit 5 або базовий клас для керування часом життя.

What it is. The driver lifecycle comes down to three questions: who creates the browser, where the configuration comes from and who is guaranteed to close it. In a mature framework that is one place: a DriverFactory for creation and a JUnit 5 extension or a base class for managing its lifetime.

Навіщо. Якщо new ChromeDriver() розкидано по тестах, перехід на Grid чи додавання Firefox означає правити сотню файлів. Коли фабрика читає браузер із системної властивості, а адресу Grid зі змінної середовища, той самий код іде на ноутбуці, в CI і на хмарному провайдері без жодної правки тестів.

Why it matters. If new ChromeDriver() is scattered across tests, moving to Grid or adding Firefox means editing a hundred files. When the factory reads the browser from a system property and the Grid address from an environment variable, the same code runs on a laptop, in CI and at a cloud provider without touching a single test.

Приклад. mvn test -Dbrowser=firefox запускає Firefox локально. У CI задано GRID_URL, і фабрика створює RemoteWebDriver з тими самими опціями. Headless, розмір вікна і стратегія завантаження сторінки налаштовуються в одному методі. Розширення кладе драйвер у ExtensionContext.Store перед тестом, передає його параметром і викликає quit() після тесту, навіть якщо той упав.

Example. mvn test -Dbrowser=firefox starts Firefox locally. In CI GRID_URL is set, and the factory creates a RemoteWebDriver with the same options. Headless mode, window size and page load strategy are configured in one method. The extension puts the driver into the ExtensionContext.Store before a test, passes it in as a parameter and calls quit() afterwards, even when the test failed.

Підводні камені. Статичне поле static WebDriver driver ламає паралельний запуск: потоки ділять один браузер. Сінглтон, що живе весь прогін, накопичує cookies і стан між тестами. Якщо quit() викликається в кінці тіла тесту, а не в afterEach, при падінні сесія на Grid висить до таймауту і забирає слот.

Pitfalls. A static WebDriver driver field breaks parallel runs because threads share one browser. A singleton living for the whole run accumulates cookies and state between tests. If quit() is called at the end of the test body rather than in an afterEach hook, a failure leaves the Grid session hanging until timeout and holding a slot.

Уточнення. Питання про швидкість: один браузер на клас замість одного на тест економить час запуску, але вимагає чистити cookies і сховище між тестами, а ізоляція слабшає. Я обираю за замовчуванням браузер на тест.

Follow-up. On speed: one browser per class instead of one per test saves startup time but requires clearing cookies and storage between tests, and isolation gets weaker. My default is a browser per test.

DriverFactory і розширення JUnit 5
DriverFactory and a JUnit 5 extension
public final class DriverFactory {
    public static WebDriver create() throws MalformedURLException {
        String browser = System.getProperty("browser", "chrome");
        String grid = System.getenv("GRID_URL"); // задано лише в CI
        if ("firefox".equals(browser)) {
            FirefoxOptions ff = new FirefoxOptions();
            ff.addArguments("-headless");
            return grid == null ? new FirefoxDriver(ff)
                : new RemoteWebDriver(URI.create(grid).toURL(), ff);
        }
        ChromeOptions ch = new ChromeOptions();
        ch.addArguments("--headless=new", "--window-size=1920,1080");
        return grid == null ? new ChromeDriver(ch)
            : new RemoteWebDriver(URI.create(grid).toURL(), ch);
    }
}

public class DriverExtension
        implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
    private static final ExtensionContext.Namespace NS =
        ExtensionContext.Namespace.create(DriverExtension.class);

    @Override
    public void beforeEach(ExtensionContext ctx) throws Exception {
        ctx.getStore(NS).put("driver", DriverFactory.create());
    }

    @Override
    public void afterEach(ExtensionContext ctx) {
        WebDriver driver = ctx.getStore(NS).remove("driver", WebDriver.class);
        if (driver != null) driver.quit(); // навіть якщо тест упав
    }

    @Override
    public boolean supportsParameter(ParameterContext p, ExtensionContext e) {
        return p.getParameter().getType() == WebDriver.class;
    }

    @Override
    public Object resolveParameter(ParameterContext p, ExtensionContext e) {
        return e.getStore(NS).get("driver", WebDriver.class);
    }
}
public final class DriverFactory {
    public static WebDriver create() throws MalformedURLException {
        String browser = System.getProperty("browser", "chrome");
        String grid = System.getenv("GRID_URL"); // set only in CI
        if ("firefox".equals(browser)) {
            FirefoxOptions ff = new FirefoxOptions();
            ff.addArguments("-headless");
            return grid == null ? new FirefoxDriver(ff)
                : new RemoteWebDriver(URI.create(grid).toURL(), ff);
        }
        ChromeOptions ch = new ChromeOptions();
        ch.addArguments("--headless=new", "--window-size=1920,1080");
        return grid == null ? new ChromeDriver(ch)
            : new RemoteWebDriver(URI.create(grid).toURL(), ch);
    }
}

public class DriverExtension
        implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
    private static final ExtensionContext.Namespace NS =
        ExtensionContext.Namespace.create(DriverExtension.class);

    @Override
    public void beforeEach(ExtensionContext ctx) throws Exception {
        ctx.getStore(NS).put("driver", DriverFactory.create());
    }

    @Override
    public void afterEach(ExtensionContext ctx) {
        WebDriver driver = ctx.getStore(NS).remove("driver", WebDriver.class);
        if (driver != null) driver.quit(); // even when the test failed
    }

    @Override
    public boolean supportsParameter(ParameterContext p, ExtensionContext e) {
        return p.getParameter().getType() == WebDriver.class;
    }

    @Override
    public Object resolveParameter(ParameterContext p, ExtensionContext e) {
        return e.getStore(NS).get("driver", WebDriver.class);
    }
}
🔧 Розбір синтаксису: System.getProperty("browser", "chrome") читає -Dbrowser з Maven чи Gradle зі значенням за замовчуванням. ChromeDriver і FirefoxDriver успадковуються від RemoteWebDriver, тому тернарний оператор повертає спільний тип. ExtensionContext.Store з власним Namespace зберігає драйвер окремо для кожного тесту, ParameterResolver дозволяє писати @Test void t(WebDriver driver), а клас тесту підключає все через @ExtendWith(DriverExtension.class).
🔧 Syntax breakdown: System.getProperty("browser", "chrome") reads -Dbrowser from Maven or Gradle with a default value. ChromeDriver and FirefoxDriver extend RemoteWebDriver, so the ternary returns a common type. ExtensionContext.Store with its own Namespace keeps the driver separately for each test, ParameterResolver lets you write @Test void t(WebDriver driver), and the test class wires everything with @ExtendWith(DriverExtension.class).
💡 Простими словами: Тест просить браузер і не питає, звідки той узявся. Де його запустити і коли закрити, вирішує одне місце у фреймворку.
💡 In plain words: A test asks for a browser and does not care where it came from. One place in the framework decides where to start it and when to close it.
🗣 Як читати вголос У моїх фреймворках тести ніколи не пишуть new ChromeDriver. Є DriverFactory: браузер вона бере з системної властивості, а якщо в середовищі є адреса Grid, створює RemoteWebDriver з тими самими опціями. Час життя веде розширення JUnit 5: створює драйвер перед тестом, передає його параметром і гарантовано викликає quit після, навіть при падінні. Статичних драйверів не тримаю, бо вони ламають паралельний запуск. Так перехід з ноутбука на Grid — це одна змінна середовища.
🗣 How to say it out loud In my frameworks tests never call new ChromeDriver. There is a DriverFactory: it takes the browser from a system property, and if the environment has a Grid address it creates a RemoteWebDriver with the same options. A JUnit 5 extension owns the lifetime: it creates the driver before the test, injects it as a parameter and always calls quit afterwards, even on failure. I avoid static drivers because they break parallel runs. Moving from a laptop to Grid is then a single environment variable.
▶ 8 6
40 З яких компонентів складається Selenium Grid 4 і що робить нода? What components make up Selenium Grid 4, and what does a node do?
GridGrid Middle ▶

Що це. Grid 4 — це набір сервісів, які приймають W3C-запити на нову сесію і розподіляють їх по машинах з браузерами. Router — єдина точка входу на порту 4444. New Session Queue тримає запити на створення сесій, доки не знайдеться вільний слот. Distributor знає всі ноди та їхні слоти і вирішує, куди віддати сесію. Session Map зберігає відповідність «id сесії → нода», щоб Router пересилав наступні команди куди треба. Event Bus — внутрішній канал повідомлень між компонентами. Node реєструється через Event Bus, оголошує слоти (наприклад, чотири Chrome і два Firefox) і запускає браузери з драйверами.

What it is. Grid 4 is a set of services that accept W3C new-session requests and spread them across machines with browsers. The Router is the single entry point on port 4444. The New Session Queue holds session requests until a free slot appears. The Distributor knows every node and its slots and decides where a session goes. The Session Map keeps the "session id to node" mapping so the Router can forward later commands to the right place. The Event Bus is the internal message channel between components. A Node registers over the Event Bus, advertises its slots (say four Chrome and two Firefox) and starts browsers with their drivers.

Навіщо. Grid дає паралельний і кросбраузерний запуск без того, щоб кожен агент CI мав усі браузери. Тест лише знає URL Router, а решту вирішує інфраструктура.

Why it matters. Grid gives parallel and cross-browser runs without installing every browser on every CI agent. The test only knows the Router URL, and the infrastructure decides the rest.

Приклад. Для локальної перевірки достатньо режиму standalone: усі компоненти в одному процесі. Для команди зазвичай беруть hub + кілька node: хаб містить Router, Distributor, Session Map, чергу і Event Bus. Режим distributed, де кожен компонент — окремий процес, потрібен великим інсталяціям на Kubernetes.

Example. For a quick local check standalone mode is enough: every component lives in one process. Teams usually run hub plus several node processes, where the hub bundles the Router, Distributor, Session Map, queue and Event Bus. The distributed mode, with each component as its own process, is for large installations on Kubernetes.

Підводні камені. Нода приймає лише ті запити, чиї капабіліті збігаються з її слотами: попросиш platformName=linux і Safari — запит просто чекатиме в черзі. Ще одна пастка — сприймати Grid як Selenium 3 з хабом: у четвертій версії реєстрація йде через Event Bus на портах 4442 і 4443, і закриті фаєрволом порти дають ноду, яку хаб не бачить.

Pitfalls. A node accepts only requests whose capabilities match its slots: ask for platformName=linux with Safari and the request just sits in the queue. Another trap is treating Grid like the Selenium 3 hub: version 4 registers nodes through the Event Bus on ports 4442 and 4443, and a firewall that blocks them leaves a node the hub never sees.

Уточнення. Стан Grid видно в UI на /ui та через GET /status, який повертає ноди, слоти і зайняті сесії у JSON.

Follow-up. Grid state is visible in the UI at /ui and through GET /status, which returns nodes, slots and busy sessions as JSON.

Три способи підняти Grid 4 з одного jar
Three ways to start Grid 4 from one jar
# усе в одному процесі: зручно локально
java -jar selenium-server-4.x.jar standalone --max-sessions 4

# хаб: Router, Distributor, Session Map, черга, Event Bus
java -jar selenium-server-4.x.jar hub

# нода на іншій машині реєструється через Event Bus (4442/4443)
java -jar selenium-server-4.x.jar node --hub http://grid-hub:4444

# перевірити, які ноди і слоти бачить Grid
curl http://grid-hub:4444/status
# everything in one process: handy locally
java -jar selenium-server-4.x.jar standalone --max-sessions 4

# hub: Router, Distributor, Session Map, queue, Event Bus
java -jar selenium-server-4.x.jar hub

# a node on another machine registers via the Event Bus (4442/4443)
java -jar selenium-server-4.x.jar node --hub http://grid-hub:4444

# check which nodes and slots the Grid can see
curl http://grid-hub:4444/status
🔧 Розбір синтаксису: Перший аргумент після jar — роль процесу: standalone, hub або node. --max-sessions обмежує кількість одночасних сесій на цьому процесі. --hub вказує ноді адресу хаба, з якої вона виводить адреси Event Bus. curl .../status повертає JSON зі станом нод і слотів.
🔧 Syntax breakdown: The first argument after the jar is the process role: standalone, hub or node. --max-sessions caps how many sessions this process runs at once. --hub gives the node the hub address, from which it derives the Event Bus addresses. curl .../status returns JSON with the state of nodes and slots.
💡 Простими словами: Router — рецепція, черга — зал очікування, Distributor — диспетчер, ноди — кабінети з браузерами.
💡 In plain words: The Router is the front desk, the queue is the waiting room, the Distributor is the dispatcher and the nodes are the rooms with browsers.
🗣 Як читати вголос Я розкладаю Grid 4 на ролі. Мій тест стукає лише в Router на 4444. Запит на нову сесію потрапляє в чергу, Distributor шукає ноду з вільним слотом під потрібні капабіліті, а Session Map запам’ятовує, де живе сесія, щоб наступні команди йшли туди ж. Ноди реєструються через Event Bus. Локально я запускаю standalone, у команді — хаб і кілька нод, а повністю розподілений режим лишаю для Kubernetes. Якщо тест висить на старті, я першим ділом дивлюсь /status: чи взагалі є слот під мій браузер.
🗣 How to say it out loud I break Grid 4 down into roles. My test only talks to the Router on 4444. A new-session request lands in the queue, the Distributor looks for a node with a free slot that fits the capabilities, and the Session Map remembers where the session lives so later commands go to the same place. Nodes register through the Event Bus. Locally I run standalone, for a team a hub plus a few nodes, and I keep the fully distributed mode for Kubernetes. If a test hangs at startup, the first thing I open is /status to see whether any slot fits my browser at all.
41 Як працює RemoteWebDriver і як тест підключається до Grid? How does RemoteWebDriver work, and how does a test connect to Grid?
GridGrid Middle ▶

Що це. RemoteWebDriver — реалізація інтерфейсу WebDriver, яка шле ті самі W3C-команди не локальному chromedriver, а на довільну адресу: Grid, standalone-контейнер або хмарний сервіс. ChromeDriver фактично теж його нащадок, тільки сам запускає драйвер поруч. Конструктор приймає URL і об’єкт опцій, наприклад ChromeOptions, з якого формуються капабіліті запиту.

What it is. RemoteWebDriver is an implementation of the WebDriver interface that sends the same W3C commands not to a local chromedriver but to any address: a Grid, a standalone container or a cloud service. ChromeDriver is actually a subclass of it that just starts the driver next to you. The constructor takes a URL and an options object, for example ChromeOptions, from which the request capabilities are built.

Навіщо. Код тестів лишається однаковим, змінюється лише фабрика драйвера: локально — new ChromeDriver(), у CI — new RemoteWebDriver(gridUrl, options). Так легко перемикатись між машиною розробника, Docker і хмарою.

Why it matters. Test code stays the same and only the driver factory changes: locally new ChromeDriver(), in CI new RemoteWebDriver(gridUrl, options). That makes switching between a developer laptop, Docker and a cloud provider cheap.

Приклад. Distributor порівнює капабіліті запиту зі слотами нод: browserName, browserVersion, platformName і префіксовані розширення на кшталт se:. Якщо тест хоче browserVersion=126, а ноди мають лише 127, сесія не стартує. Файли для завантаження теж потребують уваги: шлях у sendKeys вказує на диск машини з тестом, тож потрібен LocalFileDetector, який передає файл на ноду.

Example. The Distributor compares request capabilities with node slots: browserName, browserVersion, platformName and prefixed extensions such as se:. If the test wants browserVersion=126 but the nodes only offer 127, no session starts. File uploads need care too: the path in sendKeys points to the disk of the machine running the test, so you need a LocalFileDetector, which ships the file to the node.

Підводні камені. Без quit() у finally чи @AfterEach сесія висить на ноді до тайм-ауту бездіяльності й займає слот. Інша помилка — забути, що кожна команда тепер іде мережею: цикл із сотнею getText() через далекий Grid помітно гальмує, краще зчитати таблицю одним запитом або через API.

Pitfalls. Without quit() in a finally block or @AfterEach, the session stays on the node until the idle timeout and holds a slot. Another mistake is forgetting that every command now crosses the network: a loop with a hundred getText() calls against a distant Grid gets slow, so read the table in one call or through the API.

Уточнення. Питають і про RemoteWebDriver.builder() — сучасніший спосіб зібрати віддалений драйвер з опціями та адресою, але звичайний конструктор теж цілком коректний.

Follow-up. You may be asked about RemoteWebDriver.builder(), a newer way to assemble a remote driver from options and an address, but the plain constructor is perfectly valid too.

Підключення до Grid з передачею файлу на ноду
Connecting to Grid and shipping a file to the node
@Test
void uploadsAvatarOnGrid() throws Exception {
    ChromeOptions options = new ChromeOptions();
    options.setPlatformName("linux");          // капабіліті, за якими шукають слот

    RemoteWebDriver driver = new RemoteWebDriver(
            new URL("http://grid-hub:4444"), options);
    try {
        driver.setFileDetector(new LocalFileDetector()); // файл їде з машини тесту на ноду
        driver.get("https://shop.example.com/profile");
        driver.findElement(By.cssSelector("input[type=file]"))
              .sendKeys(Path.of("src/test/resources/avatar.png").toAbsolutePath().toString());
    } finally {
        driver.quit();                          // звільняє слот навіть при падінні
    }
}
@Test
void uploadsAvatarOnGrid() throws Exception {
    ChromeOptions options = new ChromeOptions();
    options.setPlatformName("linux");          // capabilities used to find a slot

    RemoteWebDriver driver = new RemoteWebDriver(
            new URL("http://grid-hub:4444"), options);
    try {
        driver.setFileDetector(new LocalFileDetector()); // file travels from test machine to node
        driver.get("https://shop.example.com/profile");
        driver.findElement(By.cssSelector("input[type=file]"))
              .sendKeys(Path.of("src/test/resources/avatar.png").toAbsolutePath().toString());
    } finally {
        driver.quit();                          // frees the slot even on failure
    }
}
🔧 Розбір синтаксису: new RemoteWebDriver(URL, options) відкриває сесію на Grid за адресою Router. setPlatformName додає капабіліті platformName. setFileDetector(new LocalFileDetector()) змушує клієнт упізнавати локальні шляхи в sendKeys і завантажувати файл на ноду. Блок try/finally гарантує quit() за будь-якого результату.
🔧 Syntax breakdown: new RemoteWebDriver(URL, options) opens a session on Grid through the Router address. setPlatformName adds the platformName capability. setFileDetector(new LocalFileDetector()) makes the client recognise local paths in sendKeys and upload the file to the node. The try/finally block guarantees quit() whatever the outcome.
💡 Простими словами: RemoteWebDriver — той самий пульт керування, тільки браузер стоїть в іншій кімнаті.
💡 In plain words: RemoteWebDriver is the same remote control, only the browser sits in another room.
🗣 Як читати вголос Для мене RemoteWebDriver — це той самий WebDriver, тільки з адресою сервера. Я тримаю одну фабрику драйверів: локально вона створює ChromeDriver, а в CI — RemoteWebDriver з URL хаба і ChromeOptions. Grid підбирає ноду за капабіліті, тому я не прошу версію браузера, якої там немає. Коли тест вантажить файл, я вмикаю LocalFileDetector, бо інакше нода шукає шлях у себе на диску. І quit у мене завжди у finally або в @AfterEach, щоб не лишати зайнятих слотів.
🗣 How to say it out loud To me RemoteWebDriver is the same WebDriver with a server address attached. I keep one driver factory: locally it builds ChromeDriver, in CI it builds RemoteWebDriver with the hub URL and ChromeOptions. Grid picks a node by capabilities, so I never ask for a browser version it does not have. When a test uploads a file I turn on LocalFileDetector, otherwise the node looks for that path on its own disk. And my quit always sits in finally or in @AfterEach, so no slot stays busy.
42 Що відбувається, коли версії браузера і драйвера не збігаються, і як цього уникнути? What happens when the browser and driver versions do not match, and how do you prevent it?
GridGrid Middle ▶

Що це. Драйвер Chromium-браузера зібраний під конкретну мажорну версію. Якщо на агенті Chrome 128, а в PATH лежить chromedriver 126, сесія не створюється: тест падає ще на конструкторі з SessionNotCreatedException і повідомленням на кшталт This version of ChromeDriver only supports Chrome version 126. Current browser version is 128. Firefox з geckodriver толерантніший, але й там є мінімальні версії.

What it is. A Chromium driver is built for one major browser version. If the agent has Chrome 128 and chromedriver 126 sits in PATH, no session is created: the test fails right in the constructor with SessionNotCreatedException and a message like This version of ChromeDriver only supports Chrome version 126. Current browser version is 128. Firefox with geckodriver is more tolerant, but it still has minimum versions.

Навіщо. Це одна з найчастіших причин «вчора все працювало»: браузер на агенті тихо оновився сам, а драйвер лишився старим. Падіння масове й раптове, тож його треба впізнавати за хвилину, а не шукати баг у застосунку.

Why it matters. This is one of the most common causes of "it worked yesterday": the browser on the agent quietly updated itself while the driver stayed old. The failure is sudden and hits every test, so you want to recognise it within a minute instead of hunting for an application bug.

Приклад. З Selenium 4.6 проблему здебільшого знімає Selenium Manager: він визначає встановлений браузер і завантажує відповідний драйвер у кеш ~/.cache/selenium. Для відтворюваності версію краще зафіксувати через options.setBrowserVersion("stable") або конкретне число — тоді Manager підтягне Chrome for Testing саме цієї версії, незалежно від того, що встановлено в системі.

Example. Since Selenium 4.6, Selenium Manager removes most of the pain: it detects the installed browser and downloads the matching driver into the ~/.cache/selenium cache. For reproducible runs, pin the version with options.setBrowserVersion("stable") or a specific number, and Manager fetches that exact Chrome for Testing build regardless of what the system has installed.

Підводні камені. Старий chromedriver, покладений вручну в PATH або прописаний через webdriver.chrome.driver, має пріоритет над Selenium Manager і повертає проблему. Закриті корпоративним проксі агенти не можуть завантажити драйвер, тож потрібен дзеркальний репозиторій або готовий образ. На Grid у Docker пари браузер-драйвер уже узгоджені в образі selenium/node-chrome з конкретним тегом, тому тег варто фіксувати, а не брати latest.

Pitfalls. An old chromedriver dropped into PATH by hand, or set through webdriver.chrome.driver, wins over Selenium Manager and brings the problem back. Agents behind a corporate proxy cannot download a driver, so they need a mirror or a prebuilt image. On a Docker Grid the browser and driver pair is already matched inside a selenium/node-chrome image with a specific tag, which is why you pin the tag instead of using latest.

Уточнення. Якщо спитають, як швидко діагностувати, — дивлюсь капабіліті browserVersion у відповіді сесії та версію драйвера в лозі старту.

Follow-up. If asked how to diagnose it quickly, I check the browserVersion capability returned by the session and the driver version in the startup log.

Фіксована версія браузера замість випадкового оновлення
A pinned browser version instead of a surprise update
ChromeOptions options = new ChromeOptions();
options.setBrowserVersion("127");     // Selenium Manager завантажить Chrome for Testing 127 і драйвер до нього

WebDriver driver = new ChromeDriver(options);
Capabilities caps = ((HasCapabilities) driver).getCapabilities();
// у лог CI: яку версію реально отримали
System.out.println("browser " + caps.getBrowserVersion());
ChromeOptions options = new ChromeOptions();
options.setBrowserVersion("127");     // Selenium Manager fetches Chrome for Testing 127 and its driver

WebDriver driver = new ChromeDriver(options);
Capabilities caps = ((HasCapabilities) driver).getCapabilities();
// to the CI log: which version we actually got
System.out.println("browser " + caps.getBrowserVersion());
🔧 Розбір синтаксису: setBrowserVersion записує капабіліті browserVersion; значення може бути числом або каналом (stable, beta). Коли драйвер не вказано вручну, Selenium Manager підбирає і браузер, і драйвер. HasCapabilities дає доступ до капабіліті, які повернув драйвер, а getBrowserVersion() читає фактичну версію.
🔧 Syntax breakdown: setBrowserVersion sets the browserVersion capability; the value can be a number or a channel (stable, beta). When no driver is configured by hand, Selenium Manager resolves both the browser and the driver. HasCapabilities exposes the capabilities the driver returned, and getBrowserVersion() reads the actual version.
💡 Простими словами: Браузер і драйвер — як замок і ключ: оновив замок, а ключ лишив старий — двері не відчиняться.
💡 In plain words: The browser and the driver are a lock and a key: change the lock but keep the old key and the door stays shut.
🗣 Як читати вголос Коли весь прогін раптом червоний на старті з SessionNotCreatedException, я першою підозрюю розбіжність версій: браузер на агенті оновився, а драйвер ні. Текст помилки прямо каже, яку версію драйвер підтримує і яка стоїть. Лікую так: прибираю ручний chromedriver з PATH і даю роботу Selenium Manager, а для стабільності фіксую browserVersion, щоб отримувати Chrome for Testing потрібної версії. На Grid я просто закріплюю тег Docker-образу ноди, бо там пара вже узгоджена.
🗣 How to say it out loud When the whole run suddenly goes red at startup with SessionNotCreatedException, my first suspect is a version mismatch: the browser on the agent updated and the driver did not. The error text says exactly which version the driver supports and which one is installed. My fix is to remove the hand-placed chromedriver from PATH and let Selenium Manager do its job, and to pin browserVersion so I get a known Chrome for Testing build. On Grid I simply pin the node image tag, because the pair inside is already matched.
43 Як запускати Selenium-тести паралельно і навіщо тут ThreadLocal<WebDriver>? How do you run Selenium tests in parallel, and why use ThreadLocal<WebDriver>?
GridGrid Middle ▶

Що це. Паралельний запуск означає, що кілька тестів одночасно виконуються в різних потоках, і кожен має працювати зі своєю сесією браузера. WebDriver не потокобезпечний: якщо два потоки ділять одне статичне поле driver, кліки одного тесту потрапляють у вікно іншого. ThreadLocal<WebDriver> дає кожному потоку власну копію змінної, тож Page Object-и, які беруть драйвер через DriverHolder.get(), автоматично отримують «свій» браузер.

What it is. Parallel execution means several tests run at the same time on different threads, and each one must work with its own browser session. WebDriver is not thread-safe: if two threads share one static driver field, clicks from one test land in the other test's window. ThreadLocal<WebDriver> gives every thread its own copy of the variable, so page objects that get the driver through DriverHolder.get() automatically receive "their" browser.

Навіщо. Набір на 400 UI-тестів у одному потоці йде годину; на восьми потоках через Grid — близько десяти хвилин. Без ізоляції драйверів і даних ця економія перетворюється на хаотичні падіння.

Why it matters. A suite of 400 UI tests on one thread takes an hour; on eight threads against a Grid it takes about ten minutes. Without isolated drivers and data those savings turn into random failures.

Приклад. У JUnit 5 паралельність вмикається у junit-platform.properties: junit.jupiter.execution.parallel.enabled=true, режим junit.jupiter.execution.parallel.mode.default=concurrent і кількість потоків через стратегію fixed. У TestNG те саме задають у testng.xml: parallel="methods" і thread-count="8". Драйвер створюється в @BeforeEach, закривається в @AfterEach.

Example. In JUnit 5 you enable it in junit-platform.properties: junit.jupiter.execution.parallel.enabled=true, the mode junit.jupiter.execution.parallel.mode.default=concurrent, and the thread count through the fixed strategy. In TestNG the same lives in testng.xml: parallel="methods" and thread-count="8". The driver is created in @BeforeEach and closed in @AfterEach.

Підводні камені. Після quit() обов’язково викликати remove(): потоки пулу перевикористовуються, і наступний тест інакше отримає мертву сесію. Друга пастка — спільні дані: два потоки, що логіняться тим самим користувачем чи редагують той самий кошик, заважають один одному, тож кожен тест створює власні дані з унікальними іменами. Кількість потоків не повинна перевищувати кількість слотів Grid, інакше запити стоять у черзі.

Pitfalls. Call remove() after quit(): pool threads are reused, and otherwise the next test gets a dead session. The second trap is shared data: two threads logging in as the same user or editing the same cart get in each other's way, so each test creates its own data with unique names. The thread count should not exceed the Grid slots, or requests wait in the queue.

Уточнення. Часто питають про статичні Page Object-и: їх теж не можна ділити між потоками, якщо вони тримають драйвер у полі.

Follow-up. A common follow-up is about static page objects: they cannot be shared between threads either if they keep the driver in a field.

Драйвер на потік для JUnit 5 і налаштування паралельності
One driver per thread for JUnit 5, plus the parallel settings
public final class DriverHolder {
    private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();

    public static void start() { DRIVER.set(new ChromeDriver()); }
    public static WebDriver get() { return DRIVER.get(); }

    public static void stop() {
        WebDriver driver = DRIVER.get();
        if (driver != null) {
            driver.quit();
        }
        DRIVER.remove();   // потік із пулу не отримає мертву сесію
    }
}

abstract class BaseUiTest {
    @BeforeEach void openBrowser() { DriverHolder.start(); }
    @AfterEach  void closeBrowser() { DriverHolder.stop(); }
}

// 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=8
public final class DriverHolder {
    private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();

    public static void start() { DRIVER.set(new ChromeDriver()); }
    public static WebDriver get() { return DRIVER.get(); }

    public static void stop() {
        WebDriver driver = DRIVER.get();
        if (driver != null) {
            driver.quit();
        }
        DRIVER.remove();   // a pooled thread will not inherit a dead session
    }
}

abstract class BaseUiTest {
    @BeforeEach void openBrowser() { DriverHolder.start(); }
    @AfterEach  void closeBrowser() { DriverHolder.stop(); }
}

// 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=8
🔧 Розбір синтаксису: ThreadLocal<WebDriver> зберігає окреме значення для кожного потоку; set, get і remove працюють лише з копією поточного потоку. @BeforeEach та @AfterEach виконуються в тому ж потоці, що й сам тест. Властивості в коментарі вмикають паралельне виконання, режим concurrent і фіксований пул із восьми потоків.
🔧 Syntax breakdown: ThreadLocal<WebDriver> stores a separate value per thread; set, get and remove only touch the current thread's copy. @BeforeEach and @AfterEach run on the same thread as the test itself. The properties in the comment turn on parallel execution, the concurrent mode and a fixed pool of eight threads.
💡 Простими словами: Кожен потік — окремий тестувальник за окремим комп’ютером. Спільна клавіатура на двох гарантує плутанину.
💡 In plain words: Each thread is a separate tester at a separate computer. Two people sharing one keyboard is guaranteed chaos.
🗣 Як читати вголос Паралельність я будую на двох правилах: один потік — одна сесія браузера, і ніяких спільних даних. Драйвер тримаю в ThreadLocal, створюю його в BeforeEach, а в AfterEach викликаю quit і обов’язково remove, бо потоки пулу живуть довше за тест. У JUnit 5 вмикаю паралельність у junit-platform.properties, у TestNG — через parallel і thread-count у testng.xml. Кількість потоків я підганяю під кількість слотів на Grid. А дані кожен тест створює собі сам, з унікальними іменами.
🗣 How to say it out loud I build parallel runs on two rules: one thread, one browser session, and no shared data. I keep the driver in a ThreadLocal, create it in BeforeEach, and in AfterEach I call quit and always remove, because pool threads outlive a test. In JUnit 5 I enable parallelism in junit-platform.properties, in TestNG through parallel and thread-count in testng.xml. I match the thread count to the number of Grid slots. And each test creates its own data with unique names.
44 Як підняти Selenium Grid у Docker і запускати його в CI? How do you run Selenium Grid in Docker and use it in CI?
GridGrid Middle ▶

Що це. Проєкт docker-selenium публікує готові образи: selenium/hub, selenium/node-chrome, selenium/node-firefox, selenium/standalone-chrome, а також selenium/node-docker для динамічного Grid і selenium/video для запису екрана. У кожному образі браузер і драйвер уже узгоджені, тож окремо керувати версіями не треба.

What it is. The docker-selenium project publishes ready images: selenium/hub, selenium/node-chrome, selenium/node-firefox, selenium/standalone-chrome, plus selenium/node-docker for a dynamic Grid and selenium/video for screen recording. Every image ships a browser and driver that already match, so versions need no separate management.

Навіщо. Контейнери роблять середовище однаковим на ноутбуці й в CI: той самий тег образу означає той самий браузер. Нода піднімається за секунди і не залежить від того, що встановлено на агенті.

Why it matters. Containers make the environment identical on a laptop and in CI: the same image tag means the same browser. A node comes up in seconds and does not care what is installed on the agent.

Приклад. Для CI часто вистачає одного selenium/standalone-chrome як сервісу поруч із джобою: тести ходять на http://localhost:4444. Для команди — docker compose з хабом і нодами; ноді передають адресу Event Bus змінними SE_EVENT_BUS_HOST, SE_EVENT_BUS_PUBLISH_PORT і SE_EVENT_BUS_SUBSCRIBE_PORT. Динамічний Grid на node-docker стартує новий контейнер з браузером під кожну сесію і прибирає його після quit(), тож сесії не впливають одна на одну.

Example. In CI a single selenium/standalone-chrome running as a service next to the job is often enough, and tests go to http://localhost:4444. For a team there is a docker compose file with a hub and nodes; each node gets the Event Bus address through SE_EVENT_BUS_HOST, SE_EVENT_BUS_PUBLISH_PORT and SE_EVENT_BUS_SUBSCRIBE_PORT. A dynamic Grid built on node-docker starts a fresh browser container for every session and removes it after quit(), so sessions never affect each other.

Підводні камені. Chrome у контейнері активно використовує /dev/shm, а за замовчуванням Docker дає лише 64 МБ — вкладки падають з «Aw, Snap!». Тому ноді задають shm_size: 2gb. Тег latest тихо змінює версію браузера між прогонами; краще фіксувати конкретний тег. Для динамічного Grid потрібен доступ до Docker-сокета, що в спільному CI є питанням безпеки.

Pitfalls. Chrome in a container leans heavily on /dev/shm, and Docker gives only 64 MB by default, so tabs crash with "Aw, Snap!". That is why a node gets shm_size: 2gb. The latest tag silently changes the browser version between runs, so pin a specific tag. A dynamic Grid needs access to the Docker socket, which is a security question on shared CI runners.

Уточнення. Питають, як дивитись на тест, що впав у контейнері: образи мають noVNC на порту 7900, а контейнер selenium/video записує відео сесії.

Follow-up. People ask how to watch a test that failed inside a container: the images expose noVNC on port 7900, and a selenium/video container records the session.

docker-compose.yml: хаб і дві ноди
docker-compose.yml: a hub and two nodes
services:
  selenium-hub:
    image: selenium/hub:4.25.0          # фіксований тег, не latest
    ports:
      - "4444:4444"                     # Router: сюди ходять тести

  chrome:
    image: selenium/node-chrome:4.25.0
    shm_size: 2gb                       # інакше вкладки Chrome падають
    depends_on: [selenium-hub]
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=2

  firefox:
    image: selenium/node-firefox:4.25.0
    shm_size: 2gb
    depends_on: [selenium-hub]
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
services:
  selenium-hub:
    image: selenium/hub:4.25.0          # a pinned tag, not latest
    ports:
      - "4444:4444"                     # Router: tests connect here

  chrome:
    image: selenium/node-chrome:4.25.0
    shm_size: 2gb                       # otherwise Chrome tabs crash
    depends_on: [selenium-hub]
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=2

  firefox:
    image: selenium/node-firefox:4.25.0
    shm_size: 2gb
    depends_on: [selenium-hub]
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
🔧 Розбір синтаксису: services описує три контейнери в одній мережі, тож нода звертається до хаба за іменем selenium-hub. ports відкриває 4444 назовні для тестів. Змінні SE_EVENT_BUS_* кажуть ноді, де Event Bus, а SE_NODE_MAX_SESSIONS задає кількість слотів. shm_size збільшує спільну пам’ять контейнера.
🔧 Syntax breakdown: services declares three containers on one network, so a node reaches the hub by the name selenium-hub. ports exposes 4444 to the tests. The SE_EVENT_BUS_* variables tell the node where the Event Bus is, and SE_NODE_MAX_SESSIONS sets the number of slots. shm_size enlarges the container's shared memory.
💡 Простими словами: Docker-образ — це браузер разом із драйвером у запечатаній коробці: відкрив, запустив, викинув.
💡 In plain words: A Docker image is a browser and its driver in a sealed box: open it, run it, throw it away.
🗣 Як читати вголос Grid я піднімаю з офіційних образів docker-selenium, бо там браузер і драйвер уже узгоджені. У CI зазвичай вистачає standalone-chrome як сервісу, і тести йдуть на localhost 4444. Для спільного Grid пишу docker compose з хабом і нодами, передаю їм адресу Event Bus через змінні SE_EVENT_BUS. Завжди ставлю shm_size два гігабайти і фіксую тег образу. Якщо потрібна повна ізоляція, беру динамічний Grid, де кожна сесія отримує свіжий контейнер, а для розбору падінь вмикаю запис відео.
🗣 How to say it out loud I start Grid from the official docker-selenium images, because the browser and driver inside already match. In CI a standalone-chrome service is usually enough, and tests go to localhost 4444. For a shared Grid I write a docker compose file with a hub and nodes and pass them the Event Bus address through the SE_EVENT_BUS variables. I always set shm_size to two gigabytes and pin the image tag. When I need full isolation I use a dynamic Grid where each session gets a fresh container, and I turn on video recording to investigate failures.
45 Як розрахувати ємність Grid і що робити, коли запити на сесії застрягають у черзі? How do you size Grid capacity, and what do you do when session requests get stuck in the queue?
GridGrid Senior ▶

Що це. Кожен запит на нову сесію спершу потрапляє в New Session Queue і чекає, доки Distributor знайде вільний слот з потрібними капабіліті. Якщо слот не звільнився за --session-request-timeout (за замовчуванням 300 секунд), тест отримує SessionNotCreatedException з текстом про тайм-аут запиту нової сесії. У звіті це виглядає як «впав на старті через п’ять хвилин», хоча застосунок ні до чого.

What it is. Every new-session request first lands in the New Session Queue and waits until the Distributor finds a free slot with matching capabilities. If no slot frees up within --session-request-timeout (300 seconds by default), the test gets a SessionNotCreatedException saying the new session request timed out. In the report it reads as "failed at startup after five minutes", even though the application has nothing to do with it.

Навіщо. Помилка в розрахунку ємності дає або черги й тайм-аути, або перевантажені ноди з повільними, флакі тестами. Обидва сценарії виглядають як «нестабільні тести», тож senior має вміти відрізнити їх від проблем коду.

Why it matters. Getting capacity wrong gives you either queues and timeouts, or overloaded nodes with slow and flaky tests. Both look like "unstable tests", so a senior engineer must tell them apart from real code problems.

Приклад. За замовчуванням нода виставляє стільки слотів, скільки має ядер CPU. Робоча оцінка — одна сесія Chrome на ядро і приблизно 1–2 ГБ пам’яті. Якщо CI запускає три джоби по вісім потоків, а Grid має 16 слотів, вісім запитів постійно чекають. Розв’язки: зменшити паралельність до кількості слотів, додати ноди або масштабувати їх автоматично — на Kubernetes для цього є скейлер KEDA для Selenium Grid, який дивиться на довжину черги.

Example. By default a node offers as many slots as it has CPU cores. A working estimate is one Chrome session per core and about 1–2 GB of memory. If CI runs three jobs with eight threads each against a Grid with 16 slots, eight requests are always waiting. The fixes: cut parallelism to the slot count, add nodes, or scale them automatically; on Kubernetes the KEDA scaler for Selenium Grid does this by watching the queue length.

Підводні камені. --override-max-sessions дозволяє виставити слотів більше, ніж ядер, але тоді тести стають повільнішими й нестабільнішими, а не швидшими. Ще гірше — витоки сесій: тест без quit() тримає слот до --session-timeout бездіяльності (теж 300 секунд), і черга росте, хоча реально нічого не виконується.

Pitfalls. --override-max-sessions lets a node advertise more slots than cores, but tests then get slower and flakier, not faster. Session leaks are worse: a test without quit() holds a slot until the --session-timeout idle limit (also 300 seconds), and the queue grows while nothing is actually running.

Уточнення. Спитають, що моніторити: довжину черги, кількість вільних слотів і тривалість старту сесії — усе це доступно через /status і GraphQL-ендпоінт /graphql.

Follow-up. You may be asked what to monitor: queue length, free slots and session start time, all available through /status and the /graphql endpoint.

Скільки слотів зайнято і скільки запитів чекає
How many slots are busy and how many requests are waiting
// Перевірка перед великим прогоном: чи вистачить слотів
HttpClient http = HttpClient.newHttpClient();
String query = "{\"query\": \"{ grid { maxSession sessionCount sessionQueueSize } }\"}";

HttpRequest request = HttpRequest.newBuilder(URI.create("http://grid-hub:4444/graphql"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(query))
        .build();

String body = http.send(request, HttpResponse.BodyHandlers.ofString()).body();
// напр.: {"data":{"grid":{"maxSession":16,"sessionCount":16,"sessionQueueSize":8}}}
System.out.println(body);
// Check before a big run: are there enough slots?
HttpClient http = HttpClient.newHttpClient();
String query = "{\"query\": \"{ grid { maxSession sessionCount sessionQueueSize } }\"}";

HttpRequest request = HttpRequest.newBuilder(URI.create("http://grid-hub:4444/graphql"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(query))
        .build();

String body = http.send(request, HttpResponse.BodyHandlers.ofString()).body();
// e.g.: {"data":{"grid":{"maxSession":16,"sessionCount":16,"sessionQueueSize":8}}}
System.out.println(body);
🔧 Розбір синтаксису: Це звичайний java.net.http.HttpClient, без Selenium. GraphQL-запит до /graphql повертає поля maxSession (усього слотів), sessionCount (зайнято) і sessionQueueSize (запитів у черзі). Лапки всередині JSON екрановано через \", бо тіло — рядковий літерал Java.
🔧 Syntax breakdown: This is a plain java.net.http.HttpClient, no Selenium involved. The GraphQL query to /graphql returns maxSession (total slots), sessionCount (busy) and sessionQueueSize (requests waiting). Quotes inside the JSON are escaped as \" because the body is a Java string literal.
💡 Простими словами: Слоти — це каси в супермаркеті. Відкрити більше кас, ніж є касирів, не прискорить чергу, а забута відкрита каса без покупця її лише подовжить.
💡 In plain words: Slots are checkout lanes in a supermarket. Opening more lanes than you have cashiers will not speed up the line, and a lane left open with nobody at it only makes the line longer.
🗣 Як читати вголос Ємність Grid я рахую від заліза: приблизно одна сесія Chrome на ядро і гігабайт-два пам’яті на сесію. Потім зводжу це з паралельністю CI: якщо три джоби по вісім потоків, мені потрібно двадцять чотири слоти, інакше запити сидять у черзі й падають через п’ять хвилин з тайм-аутом нової сесії. Слоти понад кількість ядер не вмикаю, бо тести стають флакі. Моніторю довжину черги, на Kubernetes віддаю масштабування KEDA, а витоки сесій ловлю тим, що quit стоїть у кожному AfterEach.
🗣 How to say it out loud I size a Grid from the hardware: roughly one Chrome session per core and a gigabyte or two of memory per session. Then I compare that with CI parallelism: three jobs with eight threads means I need twenty-four slots, otherwise requests sit in the queue and fail after five minutes with a new session timeout. I do not push slots past the core count, because tests turn flaky. I monitor queue length, leave scaling to KEDA on Kubernetes, and stop session leaks by having quit in every AfterEach.
▶ 9 6
46 Як зібрати логи консолі браузера в Selenium-тесті і навіщо падати на помилках JavaScript? How do you collect browser console logs in a Selenium test, and why fail on JavaScript errors?
СтратегіяStrategy Middle ▶

Що це. Логи консолі — це повідомлення, які сторінка пише через console.error, а також неперехоплені винятки JavaScript і помилки завантаження ресурсів. У Chrome їх можна отримати через класичний механізм логування: у капабіліті goog:loggingPrefs передати LoggingPreferences з рівнем для LogType.BROWSER, а після дії викликати driver.manage().logs().get(LogType.BROWSER).

What it is. Console logs are the messages a page writes through console.error, plus uncaught JavaScript exceptions and failed resource loads. In Chrome you read them through the classic logging mechanism: pass LoggingPreferences with a level for LogType.BROWSER in the goog:loggingPrefs capability, then call driver.manage().logs().get(LogType.BROWSER) after an action.

Навіщо. Багато клієнтських багів не видно на екрані одразу: кнопка є, але обробник упав з TypeError, і наступний крок тесту згодом зламається з незрозумілим таймаутом. Якщо після ключових кроків перевіряти, що в консолі немає записів рівня SEVERE, тест падає ближче до причини, а в звіті одразу є стек помилки.

Why it matters. Plenty of client-side bugs are not visible right away: the button is there, but its handler died with a TypeError, and a later step fails with a puzzling timeout. If you check after key steps that the console has no SEVERE entries, the test fails close to the cause and the report already holds the stack trace.

Приклад. На сторінці кошика фронтенд звертається до поля undefined.price. Візуально сторінка майже ціла, але підсумок не перераховується. Тест з перевіркою консолі падає з повідомленням Console errors: [SEVERE ... Cannot read properties of undefined] — це вже готовий опис для розробника.

Example. On the cart page the frontend reads undefined.price. The page looks almost fine, yet the total never recalculates. A test with a console check fails with Console errors: [SEVERE ... Cannot read properties of undefined], which is a ready-made description for the developer.

Підводні камені. Цей механізм не входить до стандарту W3C: він працює в Chrome і Edge, а geckodriver ендпойнт логів не підтримує. ChromeDriver віддає буфер і очищає його, тож другий виклик поверне лише нові записи. Сторонні скрипти аналітики теж пишуть помилки, тому потрібен allowlist відомого шуму, інакше команда почне ігнорувати червоні тести.

Pitfalls. This mechanism is not part of the W3C standard: it works in Chrome and Edge, while geckodriver does not support the logs endpoint. ChromeDriver hands over its buffer and clears it, so a second call returns only new entries. Third-party analytics scripts log errors too, so you need an allowlist of known noise, or the team will learn to ignore red builds.

Уточнення. Кросбраузерний напрям — WebDriver BiDi: з капабіліті webSocketUrl браузер сам надсилає події консолі через WebSocket, і так можна підписатись на них у Chrome і Firefox, а не опитувати буфер після факту.

Follow-up. The cross-browser direction is WebDriver BiDi: with the webSocketUrl capability the browser pushes console events over a WebSocket, so you can subscribe to them in Chrome and Firefox instead of polling a buffer after the fact.

Увімкнути логи Chrome і впасти на SEVERE
Enable Chrome logs and fail on SEVERE
ChromeOptions options = new ChromeOptions();
LoggingPreferences prefs = new LoggingPreferences();
prefs.enable(LogType.BROWSER, Level.ALL);          // java.util.logging.Level
options.setCapability("goog:loggingPrefs", prefs);
WebDriver driver = new ChromeDriver(options);

driver.get("https://shop.example.com/cart");

// Забираємо буфер консолі і лишаємо тільки помилки
List<LogEntry> errors = driver.manage().logs().get(LogType.BROWSER)
        .getAll().stream()
        .filter(e -> e.getLevel() == Level.SEVERE)
        .filter(e -> !e.getMessage().contains("analytics")) // відомий шум
        .toList();

assertTrue(errors.isEmpty(), () -> "Console errors: " + errors);
ChromeOptions options = new ChromeOptions();
LoggingPreferences prefs = new LoggingPreferences();
prefs.enable(LogType.BROWSER, Level.ALL);          // java.util.logging.Level
options.setCapability("goog:loggingPrefs", prefs);
WebDriver driver = new ChromeDriver(options);

driver.get("https://shop.example.com/cart");

// Drain the console buffer and keep only errors
List<LogEntry> errors = driver.manage().logs().get(LogType.BROWSER)
        .getAll().stream()
        .filter(e -> e.getLevel() == Level.SEVERE)
        .filter(e -> !e.getMessage().contains("analytics")) // known noise
        .toList();

assertTrue(errors.isEmpty(), () -> "Console errors: " + errors);
🔧 Розбір синтаксису: LoggingPreferences.enable задає, які типи логів і з якого рівня збирати. logs().get(...) повертає LogEntries, а getAll() — список LogEntry з рівнем, часом і текстом. Stream-фільтри відкидають усе, крім SEVERE, і відомий шум. Третій аргумент assertTrue — лямбда-постачальник повідомлення, тож рядок будується лише при падінні.
🔧 Syntax breakdown: LoggingPreferences.enable says which log types to collect and from which level. logs().get(...) returns LogEntries, and getAll() gives a list of LogEntry objects with a level, timestamp and message. The stream filters drop everything except SEVERE and known noise. The last argument of assertTrue is a message supplier lambda, so the string is built only when the check fails.
💡 Простими словами: Консоль браузера — це чорна скринька фронтенду. Якщо там червоне, тест має про це сказати сам.
💡 In plain words: The browser console is the frontend's black box. If there is red in it, the test should say so on its own.
🗣 Як читати вголос Я вмикаю збір логів консолі в Chrome через goog:loggingPrefs і після важливих кроків забираю записи типу BROWSER. Помилки рівня SEVERE валять тест, крім невеликого списку відомого шуму від аналітики. Це дає мені падіння біля справжньої причини, а не через хвилину на таймауті. Я пам’ятаю, що це Chrome-специфічна штука, Firefox її не підтримує, і буфер очищається після читання. Для кросбраузерного варіанту я б дивився в бік BiDi, де браузер сам шле події консолі.
🗣 How to say it out loud I turn on console collection in Chrome through goog:loggingPrefs and, after the important steps, I pull the BROWSER entries. SEVERE errors fail the test, apart from a short list of known analytics noise. That way the failure lands next to the real cause instead of a minute later on some timeout. I keep in mind it is Chrome-only, Firefox does not support it, and the buffer is emptied once read. For a cross-browser option I would look at BiDi, where the browser pushes console events itself.
47 Що дають CDP і BiDi у Selenium 4 і як підмінити відповідь API, щоб перевірити стан помилки в UI? What do CDP and BiDi give you in Selenium 4, and how do you stub an API response to test an error state in the UI?
СтратегіяStrategy Senior ▶

Що це. Chrome DevTools Protocol — внутрішній протокол Chromium, через який працює сам DevTools. Selenium 4 відкриває до нього доступ: драйвер, що реалізує HasDevTools, дає DevTools, у якому викликають createSession() і шлють команди доменів Network, Emulation, Log. Поверх цього є високорівневі класи, наприклад NetworkInterceptor, що перехоплює запити за правилами Route. WebDriver BiDi — стандартизований W3C-наступник тієї ж ідеї, який працює і в Firefox.

What it is. The Chrome DevTools Protocol is Chromium's internal protocol, the one DevTools itself runs on. Selenium 4 exposes it: a driver that implements HasDevTools gives you a DevTools object, you call createSession() and send commands from domains such as Network, Emulation or Log. On top of that sit higher-level helpers like NetworkInterceptor, which intercepts requests according to Route rules. WebDriver BiDi is the standardised W3C successor of the same idea and also works in Firefox.

Навіщо. Стани помилок — 500 від сервісу цін, повільна відповідь, порожній список — складно відтворити на реальному стенді. Перехоплення дозволяє віддати браузеру штучну відповідь і детерміновано перевірити, що UI показує банер, а не вічний спінер.

Why it matters. Error states such as a 500 from the pricing service, a slow response or an empty list are hard to reproduce on a real environment. Interception lets you hand the browser a fake response and check deterministically that the UI shows a banner rather than an endless spinner.

Приклад. Правило в коді нижче ловить усі запити до /api/prices і повертає 500 з JSON-тілом. Тест відкриває каталог і чекає на банер з текстом про недоступні ціни. Коли блок try закінчується, перехоплення знімається.

Example. The rule in the code below catches every request to /api/prices and returns a 500 with a JSON body. The test opens the catalogue and waits for the banner saying prices are unavailable. When the try block ends, the interception is removed.

Підводні камені. CDP є лише в Chrome та Edge, а Java-класи згенеровані під конкретну версію протоколу: пакети org.openqa.selenium.devtools.v1NN треба оновлювати разом із браузером, і на свіжому Chrome старий артефакт може не знайтись. На Grid CDP іде через WebSocket ноди, тож мережа між тестом і нодою має це пропускати. Перехоплення змінює поведінку браузера — тримай такі тести окремо від end-to-end перевірок реальної інтеграції.

Pitfalls. CDP exists only in Chrome and Edge, and the Java classes are generated for a specific protocol version: the org.openqa.selenium.devtools.v1NN packages must be upgraded together with the browser, and a fresh Chrome may have no matching artefact in an old Selenium. On Grid, CDP travels over the node's WebSocket, so the network between the test and the node must allow it. Interception changes browser behaviour, so keep these tests apart from end-to-end checks of the real integration.

Уточнення. Питання «CDP чи BiDi для нового коду?» я закриваю так: для кросбраузерних речей — BiDi, бо це стандарт і Selenium поступово переносить туди логи, мережу та скрипти; CDP лишається для Chromium-специфіки на кшталт емуляції мережі.

Follow-up. When asked "CDP or BiDi for new code?", I answer: BiDi for cross-browser needs, because it is the standard and Selenium is moving logs, network and scripting onto it; CDP stays for Chromium-specific work such as network throttling.

Підміна відповіді API через NetworkInterceptor
Stubbing an API response with NetworkInterceptor
ChromeDriver driver = new ChromeDriver();

// Усі запити до /api/prices отримують 500 замість реальної відповіді
Route pricesDown = Route.matching(req -> req.getUri().contains("/api/prices"))
        .to(() -> req -> new HttpResponse()
                .setStatus(500)
                .addHeader("Content-Type", "application/json")
                .setContent(Contents.utf8String("{\"error\":\"unavailable\"}")));

try (NetworkInterceptor interceptor = new NetworkInterceptor(driver, pricesDown)) {
    driver.get("https://shop.example.com/catalog");
    WebElement banner = new WebDriverWait(driver, Duration.ofSeconds(5))
            .until(ExpectedConditions.visibilityOfElementLocated(
                    By.cssSelector("[data-testid='prices-error']")));
    assertEquals("Prices are temporarily unavailable", banner.getText());
} // тут перехоплення знімається
ChromeDriver driver = new ChromeDriver();

// Every request to /api/prices gets a 500 instead of the real response
Route pricesDown = Route.matching(req -> req.getUri().contains("/api/prices"))
        .to(() -> req -> new HttpResponse()
                .setStatus(500)
                .addHeader("Content-Type", "application/json")
                .setContent(Contents.utf8String("{\"error\":\"unavailable\"}")));

try (NetworkInterceptor interceptor = new NetworkInterceptor(driver, pricesDown)) {
    driver.get("https://shop.example.com/catalog");
    WebElement banner = new WebDriverWait(driver, Duration.ofSeconds(5))
            .until(ExpectedConditions.visibilityOfElementLocated(
                    By.cssSelector("[data-testid='prices-error']")));
    assertEquals("Prices are temporarily unavailable", banner.getText());
} // interception is removed here
🔧 Розбір синтаксису: Route.matching приймає предикат над HttpRequest, а to — постачальника обробника, який з запиту робить HttpResponse. Contents.utf8String пакує рядок у тіло відповіді. NetworkInterceptor реалізує Closeable, тому try-with-resources гарантує, що правило зникне навіть при падінні асерту.
🔧 Syntax breakdown: Route.matching takes a predicate over HttpRequest, and to takes a supplier of a handler that turns the request into an HttpResponse. Contents.utf8String wraps a string as the response body. NetworkInterceptor implements Closeable, so try-with-resources guarantees the rule is gone even when the assertion fails.
💡 Простими словами: CDP — службовий вхід у Chrome з ключем під конкретну версію. BiDi — стандартні двері, які поступово ставлять у всі браузери.
💡 In plain words: CDP is a service entrance into Chrome with a key cut for one version. BiDi is the standard door that is gradually being fitted to every browser.
🗣 Як читати вголос Для станів помилок я не чекаю, поки на стенді впаде сервіс, а підміняю відповідь прямо в браузері. У Selenium 4 є NetworkInterceptor: я описую маршрут, наприклад усе на /api/prices, і повертаю п’ятсоту. Далі тест перевіряє, що користувач бачить зрозумілий банер. Я чесно кажу про обмеження: це CDP, отже лише Chromium, і пакети прив’язані до версії браузера. Тому для нового кросбраузерного коду я дивлюсь на BiDi, а CDP лишаю для Chrome-специфічних речей.
🗣 How to say it out loud For error states I do not wait for a service to go down on the environment; I replace the response inside the browser. Selenium 4 has NetworkInterceptor: I describe a route, say everything under /api/prices, and return a five hundred. Then the test checks that the user sees a clear banner. I am upfront about the limits: this is CDP, so Chromium only, and the packages are tied to the browser version. That is why for new cross-browser code I look at BiDi and keep CDP for Chrome-specific work.
48 Один Selenium-тест падає раз на двадцять запусків. Як ти знайдеш причину, а не просто додаш retry? One Selenium test fails once in twenty runs. How do you find the cause instead of just adding a retry?
СтратегіяStrategy Senior ▶

Що це. Флейкі-тест дає різний результат на тому самому коді. Це симптом, а не діагноз: причина може бути в тесті, у застосунку або в середовищі, і задача — довести, де саме.

What it is. A flaky test gives different results on the same code. That is a symptom, not a diagnosis: the cause may be in the test, the application or the environment, and the job is to prove which.

Навіщо. Retry ховає проблему, а іноді й реальний баг: гонка в UI, яку ловить тест, так само трапляється в користувачів. Крім того, кожен повтор подовжує пайплайн і вчить команду не довіряти червоному.

Why it matters. A retry hides the problem and sometimes hides a real bug: a UI race that the test hits also happens to users. On top of that, every rerun makes the pipeline longer and teaches the team not to trust a red build.

Приклад. Спершу відтворюю: запускаю тест 50 разів через @RepeatedTest, у тому ж режимі, що й CI — headless, той самий розмір вікна, на Grid, якщо там падає. Потім збираю артефакти кожного падіння: скріншот, page source, логи консолі, відео сесії з Grid, таймінги запитів. Далі класифікую: таймінг (читаємо стан до завершення XHR), дані (товар розпродав сусідній тест), порядок (тест живе на cookie попереднього), середовище (повільний стенд, інша локаль). У прикладі нижче причина — лічильник кошика читається до відповіді сервера, і виправлення — чекати саме цього стану.

Example. First I reproduce it: run the test 50 times with @RepeatedTest in the same mode as CI, headless, same window size, on Grid if that is where it fails. Then I collect artefacts for each failure: screenshot, page source, console logs, the Grid session video and request timings. Next I classify: timing (we read the state before the XHR finished), data (a neighbouring test bought the last item), order (the test relies on a cookie from the previous one), environment (slow stage, different locale). In the example below the cart counter is read before the server responds, and the fix is to wait for exactly that state.

Підводні камені. Локально тест часто зелений, бо машина швидша; не роби висновок «у мене працює», поки не повториш умови CI. Збільшити таймаут — не фікс, якщо умова очікування неправильна. І не лагодь кілька гіпотез одночасно: інакше не дізнаєшся, що допомогло.

Pitfalls. Locally the test is often green because the machine is faster, so do not conclude "works on my machine" until you have recreated CI conditions. Raising the timeout is not a fix when the wait condition is wrong. And never try several hypotheses at once, or you will not know which one helped.

Уточнення. Якщо причина в застосунку, наприклад подвійний сабміт через гонку, я заводжу баг з відео й логами і не маскую його в тесті.

Follow-up. If the cause is in the product, for example a double submit caused by a race, I file a bug with the video and logs and do not mask it in the test.

Відтворення в циклі і виправлення кореневої причини
Reproducing in a loop and fixing the root cause
// Крок 1: відтворити в умовах CI (headless, той самий розмір вікна)
@RepeatedTest(50)
void cartBadgeShowsOneItem() {
    catalogPage.open().addToCart("sku-123");

    // Флейкі-версія: читає лічильник до завершення XHR
    // assertEquals("1", driver.findElement(By.id("cart-count")).getText());

    // Виправлення: чекаємо стан, якого чекає і користувач
    new WebDriverWait(driver, Duration.ofSeconds(10))
            .until(ExpectedConditions.textToBe(By.id("cart-count"), "1"));
}
// Step 1: reproduce under CI conditions (headless, same window size)
@RepeatedTest(50)
void cartBadgeShowsOneItem() {
    catalogPage.open().addToCart("sku-123");

    // Flaky version: reads the counter before the XHR has finished
    // assertEquals("1", driver.findElement(By.id("cart-count")).getText());

    // Fix: wait for the state the user is waiting for too
    new WebDriverWait(driver, Duration.ofSeconds(10))
            .until(ExpectedConditions.textToBe(By.id("cart-count"), "1"));
}
🔧 Розбір синтаксису: @RepeatedTest(50) з JUnit 5 запускає метод 50 разів як окремі тести, тож у звіті видно частоту падінь. Закоментований рядок — типовий флейк: getText() знімає значення миттєво. ExpectedConditions.textToBe повторює перевірку тексту, доки він не стане «1» або не мине таймаут.
🔧 Syntax breakdown: @RepeatedTest(50) from JUnit 5 runs the method 50 times as separate tests, so the report shows the failure rate. The commented-out line is a typical flake: getText() takes the value instantly. ExpectedConditions.textToBe keeps re-checking the text until it becomes "1" or the timeout runs out.
💡 Простими словами: Спершу зроби флейк стабільно відтворюваним, потім доведи причину артефактами, і лише тоді змінюй код.
💡 In plain words: First make the flake reproducible, then prove the cause with artefacts, and only then change code.
🗣 Як читати вголос Я не додаю retry першим кроком, бо це ховає і проблему тесту, і можливий баг продукту. Спочатку я відтворюю падіння: ганяю тест у циклі в тих самих умовах, що й CI. На кожному падінні збираю скріншот, DOM, консоль і відео з Grid. Потім відношу причину до однієї з груп: таймінг, дані, порядок тестів або середовище. У типовому випадку тест читає значення до відповіді сервера, і я міняю перевірку на очікування конкретного стану. Якщо ж винна гонка в самому застосунку, я заводжу баг.
🗣 How to say it out loud I do not reach for a retry first, because it hides both the test problem and a possible product bug. I start by reproducing the failure: I run the test in a loop under the same conditions as CI. For every failure I capture a screenshot, the DOM, the console and the Grid video. Then I put the cause into one bucket: timing, data, test order or environment. In the common case the test reads a value before the server has answered, so I replace the read with a wait for that exact state. If the race is in the application itself, I file a bug.
49 У регресі 15% тестів падають нестабільно, і команда перестала вірити CI. Як ти стабілізуєш набір як процес? Fifteen percent of the regression suite fails intermittently and the team has stopped trusting CI. How do you stabilise the suite as a process?
СтратегіяStrategy Senior ▶

Що це. Це вже не питання одного тесту, а проблема довіри до сигналу. Лагодити 60 тестів навмання довго; потрібні вимірювання, правила і власники.

What it is. At this point it is not about one test but about trust in the signal. Fixing sixty tests at random takes forever; you need measurement, rules and owners.

Навіщо. Поки червоний білд нічого не означає, ніхто не дивиться на справжні регресії, і автотести перетворюються на витрати без користі. Мета — щоб падіння знову означало «щось зламалось».

Why it matters. While a red build means nothing, nobody looks at real regressions and the automation becomes cost without value. The goal is for a failure to mean "something broke" again.

Приклад. Крок 1 — метрика: з історії CI рахую частку флейків для кожного тесту за два тижні і сортую за шкодою. Крок 2 — карантин: тести з частотою флейку понад поріг отримують тег і біжать окремою джобою, яка не блокує мердж, але кожен має тікет, власника і дедлайн. Крок 3 — retry тільки один і тільки як сигнал: тест, що пройшов з другої спроби, позначається як flaky у звіті, а не як зелений. Крок 4 — системні фікси, які прибирають цілі класи проблем: видалити Thread.sleep і implicit wait, перейти на явні очікування, створювати дані через API з унікальними іменами, дати кожному потоку власний драйвер і користувача.

Example. Step 1 is a metric: from CI history I compute each test's flake rate over two weeks and sort by damage. Step 2 is quarantine: tests above a threshold get a tag and run in a separate job that does not block merges, but each has a ticket, an owner and a deadline. Step 3 is at most one retry, used as a signal: a test that passed on the second attempt is shown as flaky in the report, not as green. Step 4 is systemic fixes that remove whole classes of problems: delete Thread.sleep and implicit waits, move to explicit waits, create data through the API with unique names, and give each thread its own driver and user.

Підводні камені. Карантин без дедлайну стає кладовищем, де тести тихо гниють. Масові retry дають гарну статистику і приховують справжні баги. Паралельність, увімкнена до ізоляції даних, сама породжує нові флейки.

Pitfalls. Quarantine without a deadline turns into a graveyard where tests quietly rot. Blanket retries produce nice statistics and hide real bugs. Turning on parallel runs before data is isolated breeds a fresh wave of flakes.

Уточнення. Успіх я міряю не кількістю виправлених тестів, а часткою флейків у загальних прогонах і тим, як часто розробники перезапускають пайплайн вручну.

Follow-up. I measure success not by the number of tests fixed but by the flake share across all runs and by how often developers rerun the pipeline by hand.

Карантин з власником і дані через API
Quarantine with an owner, and data through the API
// Тест у карантині: окрема джоба, тікет, власник і дедлайн
@Tag("quarantine") // FLAKY-412, власник: команда payments, до 2026-10-15
@Test
void refundAppearsInHistory() {
    // Дані створюються через API з унікальним ім'ям, а не кліками в UI
    Order order = api.createPaidOrder("user-" + UUID.randomUUID());
    historyPage.openFor(order.id());

    new WebDriverWait(driver(), Duration.ofSeconds(10))
            .until(ExpectedConditions.textToBePresentInElementLocated(
                    By.cssSelector("[data-testid='order-status']"), "Refunded"));
}

// driver() повертає драйвер поточного потоку з ThreadLocal
// Quarantined test: separate job, ticket, owner and deadline
@Tag("quarantine") // FLAKY-412, owner: payments team, until 2026-10-15
@Test
void refundAppearsInHistory() {
    // Data is created via the API with a unique name, not by clicking through the UI
    Order order = api.createPaidOrder("user-" + UUID.randomUUID());
    historyPage.openFor(order.id());

    new WebDriverWait(driver(), Duration.ofSeconds(10))
            .until(ExpectedConditions.textToBePresentInElementLocated(
                    By.cssSelector("[data-testid='order-status']"), "Refunded"));
}

// driver() returns the current thread's driver from a ThreadLocal
🔧 Розбір синтаксису: @Tag("quarantine") — мітка JUnit 5, за якою збірка виключає тест з основної джоби (у Maven Surefire це excludedGroups) і запускає окремо. UUID.randomUUID() робить дані унікальними для кожного прогону й потоку. textToBePresentInElementLocated чекає, поки в елементі з’явиться потрібний текст, замість фіксованої паузи.
🔧 Syntax breakdown: @Tag("quarantine") is a JUnit 5 label the build uses to exclude the test from the main job (in Maven Surefire that is excludedGroups) and run it separately. UUID.randomUUID() makes the data unique per run and per thread. textToBePresentInElementLocated waits for the expected text to appear in the element instead of pausing for a fixed time.
💡 Простими словами: Спершу поверни довіру до червоного кольору, потім прибирай причини цілими класами, а не по одному тесту.
💡 In plain words: First make red mean something again, then remove causes by whole classes rather than one test at a time.
🗣 Як читати вголос Коли флейків п’ятнадцять відсотків, я вважаю, що зламаний не тест, а сигнал. Спершу рахую статистику по кожному тесту з історії CI і беру найшкідливіші. Нестабільні йдуть у карантин, але з тікетом, власником і датою, інакше це кладовище. Один повтор я допускаю, але в звіті такий тест позначений як нестабільний. Далі системні речі: прибрати sleep та implicit wait, дані через API, окремий драйвер і користувач на потік. Прогрес міряю часткою флейків і тим, як часто люди перезапускають пайплайн.
🗣 How to say it out loud When fifteen percent of runs are flaky, I treat it as a broken signal rather than a broken test. I start with per-test statistics from CI history and pick the most harmful ones. Unstable tests go into quarantine, but with a ticket, an owner and a date, otherwise it becomes a graveyard. I allow one rerun, yet the report marks such a test as unstable. Then come systemic changes: remove sleeps and implicit waits, set up data via the API, one driver and one user per thread. I track progress by the flake share and how often people rerun the pipeline.
50 Selenium, Playwright чи Selenide: як обрати інструмент для нового проєкту і коли лишатися на Selenium? Selenium, Playwright or Selenide: how do you choose a tool for a new project, and when do you stay on Selenium?
СтратегіяStrategy Senior ▶

Що це. Selenium — реалізація стандарту W3C WebDriver з клієнтами для Java, Python, C#, JavaScript і Ruby, Grid для розподілу сесій та підтримкою всіх основних браузерів, включно зі справжнім Safari через safaridriver. Playwright керує браузерами власним протоколом, має автоочікування перед кожною дією, трейсинг, контроль мережі та ізольовані контексти з коробки, але працює зі своїми збірками Chromium, Firefox і WebKit. Selenide — Java-обгортка над Selenium з лаконічним API та вбудованими очікуваннями в should і діях.

What it is. Selenium implements the W3C WebDriver standard, with clients for Java, Python, C#, JavaScript and Ruby, Grid for distributing sessions, and support for every major browser, including real Safari through safaridriver. Playwright drives browsers over its own protocol and ships auto-waiting before each action, tracing, network control and isolated contexts out of the box, but it runs its own builds of Chromium, Firefox and WebKit. Selenide is a Java wrapper over Selenium with a concise API and waits built into should and into actions.

Навіщо. Вибір впливає на стабільність, швидкість написання тестів і на те, хто зможе їх підтримувати. Питання на співбесіді перевіряє, чи ти зважуєш обмеження проєкту, а не повторюєш моду.

Why it matters. The choice affects stability, how fast tests get written and who can maintain them. The interview question checks whether you weigh the project's constraints rather than repeating what is fashionable.

Приклад. Новий вебпродукт, команда на TypeScript, потрібні швидкий фідбек і мок мережі — Playwright. Велика Java-кодова база, тисячі тестів на Selenium, вимога прогону в реальному Safari та на хмарній фермі — лишаюсь на Selenium, а щоб прибрати ручні очікування, можу поступово додати Selenide, бо він працює поверх того ж драйвера. Код нижче показує одну дію в трьох інструментах.

Example. A new web product, a TypeScript team, a need for fast feedback and network mocks: Playwright. A large Java codebase with thousands of Selenium tests, a requirement to run on real Safari and on a cloud device farm: I stay on Selenium, and to get rid of hand-written waits I can bring in Selenide gradually, because it runs on the same driver. The code below shows one action in all three tools.

Підводні камені. WebKit у Playwright — не те саме, що Safari на macOS, і для деяких клієнтів це критично. Міграція тисяч тестів коштує місяців, і нестабільність частіше йде від поганих очікувань, ніж від інструмента. Selenide ховає очікування, але не лагодить погані локатори чи залежні тести.

Pitfalls. Playwright's WebKit is not the same as Safari on macOS, and for some clients that matters a lot. Migrating thousands of tests costs months, and flakiness more often comes from poor waiting than from the tool. Selenide hides waits but does not fix bad locators or interdependent tests.

Уточнення. Мій критерій: мова команди, обов’язкові браузери та інфраструктура, потреба в мережевих моках і трейсингу, вартість переходу. Selenium 4 з BiDi частково скорочує розрив у роботі з мережею та логами.

Follow-up. My criteria are the team's language, the required browsers and infrastructure, the need for network mocks and tracing, and the cost of switching. Selenium 4 with BiDi partly closes the gap on network and log access.

Одна дія в трьох інструментах
One action in three tools
// Selenium: очікування — відповідальність автора тесту
new WebDriverWait(driver, Duration.ofSeconds(10))
        .until(ExpectedConditions.elementToBeClickable(By.cssSelector("[data-testid='pay']")))
        .click();

// Selenide: той самий WebDriver під капотом, очікування вбудовані
$("[data-testid='pay']").shouldBe(visible, enabled).click();

// Playwright for Java: перевірки actionability перед кожною дією
page.getByTestId("pay").click();
// Selenium: waiting is the test author's job
new WebDriverWait(driver, Duration.ofSeconds(10))
        .until(ExpectedConditions.elementToBeClickable(By.cssSelector("[data-testid='pay']")))
        .click();

// Selenide: the same WebDriver underneath, waits are built in
$("[data-testid='pay']").shouldBe(visible, enabled).click();

// Playwright for Java: actionability checks before every action
page.getByTestId("pay").click();
🔧 Розбір синтаксису: У Selenium until повертає елемент, коли умова виконалась, і на ньому викликається click(). У Selenide $ — статичний метод, що створює лінивий елемент, а shouldBe(visible, enabled) повторює перевірку умов до таймауту з конфігурації. У Playwright getByTestId створює локатор, а click() сам чекає, поки елемент стане видимим, стабільним і доступним.
🔧 Syntax breakdown: In Selenium, until returns the element once the condition holds, and click() is called on it. In Selenide, $ is a static method that builds a lazy element, and shouldBe(visible, enabled) keeps re-checking the conditions up to the configured timeout. In Playwright, getByTestId builds a locator and click() itself waits until the element is visible, stable and enabled.
💡 Простими словами: Selenium — стандарт і ширина браузерів, Playwright — зручність і швидкість з коробки, Selenide — Selenium без ручних очікувань.
💡 In plain words: Selenium is the standard and browser breadth, Playwright is convenience and speed out of the box, Selenide is Selenium without hand-written waits.
🗣 Як читати вголос Я не вважаю, що є найкращий інструмент взагалі. Якщо проєкт новий, команда пише на TypeScript і потрібні моки мережі та трейси, я візьму Playwright. Якщо ж є великий Java-набір, обов’язковий реальний Safari і хмарна ферма, я лишуся на Selenium, бо це стандарт W3C і Grid. Щоб позбутися ручних очікувань без переписування, можна поступово додати Selenide поверх того ж драйвера. І я пам’ятаю, що флейки частіше дають погані очікування, ніж сам інструмент, тож міграція заради стабільності — не завжди виграш.
🗣 How to say it out loud I do not think there is one best tool in general. For a new project with a TypeScript team that needs network mocks and traces, I would take Playwright. With a large Java suite, a hard requirement for real Safari and a cloud farm, I would stay on Selenium, because it is the W3C standard and has Grid. To drop hand-written waits without a rewrite, I can phase in Selenide on top of the same driver. I also remember that flakiness usually comes from poor waiting rather than the tool, so migrating for stability alone is not always a win.
51 Як ти спланував би міграцію великого набору тестів із Selenium 3 на Selenium 4? How would you plan migrating a large test suite from Selenium 3 to Selenium 4?
СтратегіяStrategy Senior ▶

Що це. Selenium 4 працює лише за W3C-протоколом і прибрав частину старого API. DesiredCapabilities.chrome() замінили класи ChromeOptions, FirefoxOptions та інші. Методи findElementById і подібні стали deprecated у 4.0 і пізніше зникли — лишився findElement(By...). Таймаути приймають Duration: new WebDriverWait(driver, Duration.ofSeconds(10)), implicitlyWait(Duration). Grid 3 hub/node замінив Grid 4 з новими компонентами, і старі ноди до нього не підключаються.

What it is. Selenium 4 speaks only the W3C protocol and removed parts of the old API. DesiredCapabilities.chrome() gave way to classes like ChromeOptions and FirefoxOptions. Methods such as findElementById were deprecated in 4.0 and removed later, leaving findElement(By...). Timeouts take a Duration: new WebDriverWait(driver, Duration.ofSeconds(10)) and implicitlyWait(Duration). Grid 3 hub and node were replaced by Grid 4 with new components, and old nodes cannot register with it.

Навіщо. Selenium 3 давно не підтримується, нові браузери з ним поводяться непередбачувано, а Selenium Manager, відносні локатори, getShadowRoot, newWindow і BiDi є лише в четвертій версії.

Why it matters. Selenium 3 has long been unsupported, new browsers behave unpredictably with it, and Selenium Manager, relative locators, getShadowRoot, newWindow and BiDi exist only in version 4.

Приклад. План на тисячу тестів: 1) інвентаризація — пошук deprecated викликів компілятором і grep по DesiredCapabilities, findElementBy, TimeUnit; 2) спершу оновити бібліотеку, лишивши старий Grid, і виправити компіляцію в одній DriverFactory, а не в кожному тесті; 3) замінити WebDriverManager на вбудований Selenium Manager; 4) підняти Grid 4 поруч зі старим і перевести джоби частинами; 5) порівнювати частку падінь до і після на тих самих тестах. Останні свіжі 4.x релізи вимагають Java 11+, тож перевір і JDK.

Example. A plan for a thousand tests: 1) inventory, using compiler deprecation warnings and a grep for DesiredCapabilities, findElementBy and TimeUnit; 2) upgrade the library first while keeping the old Grid, fixing compilation in one DriverFactory rather than in every test; 3) swap WebDriverManager for the built-in Selenium Manager; 4) run Grid 4 next to the old one and move jobs over in batches; 5) compare failure rates before and after on the same tests. Recent 4.x releases need Java 11 or newer, so check the JDK as well.

Підводні камені. Власні капабіліті без префікса вендора, як-от "build" для хмарних ферм, W3C-сервер відкидає — їх переносять у "vendor:options". Великий «вибуховий» мердж змішує помилки міграції з флейками і ускладнює відкат.

Pitfalls. A W3C server rejects custom capabilities without a vendor prefix, such as "build" for cloud farms, so they move into "vendor:options". One big-bang merge mixes migration errors with existing flakes and makes a rollback painful.

Уточнення. Корисно одночасно прибрати implicit wait, якщо він був, бо поведінка очікувань змінюється разом із драйверами, і міграція — зручний момент почати з чистого аркуша.

Follow-up. It pays to drop implicit waits at the same time if you had them, since wait behaviour shifts along with the drivers, and a migration is a convenient moment to start clean.

Було в Selenium 3 і стало в Selenium 4
Selenium 3 before and Selenium 4 after
// Selenium 3 (більше не компілюється):
// DesiredCapabilities caps = DesiredCapabilities.chrome();
// WebDriver driver = new RemoteWebDriver(new URL(hubUrl), caps);
// driver.findElementById("login").click();
// new WebDriverWait(driver, 10);
// driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

// Selenium 4: Options замість DesiredCapabilities, By замість findElementBy*
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
WebDriver driver = new RemoteWebDriver(new URL(gridUrl), options);

driver.findElement(By.id("login")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// Selenium 3 (no longer compiles):
// DesiredCapabilities caps = DesiredCapabilities.chrome();
// WebDriver driver = new RemoteWebDriver(new URL(hubUrl), caps);
// driver.findElementById("login").click();
// new WebDriverWait(driver, 10);
// driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

// Selenium 4: Options instead of DesiredCapabilities, By instead of findElementBy*
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
WebDriver driver = new RemoteWebDriver(new URL(gridUrl), options);

driver.findElement(By.id("login")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
🔧 Розбір синтаксису: Закоментований блок — типовий код Selenium 3, який треба знайти й замінити. RemoteWebDriver у Selenium 4 приймає ChromeOptions напряму, бо Options самі є капабіліті. --headless=new вмикає новий headless-режим Chrome. Duration.ofSeconds(10) замінює пару «число плюс TimeUnit».
🔧 Syntax breakdown: The commented-out block is typical Selenium 3 code that you have to find and replace. In Selenium 4, RemoteWebDriver takes ChromeOptions directly, because Options are capabilities themselves. --headless=new turns on Chrome's new headless mode. Duration.ofSeconds(10) replaces the pair of a number and a TimeUnit.
💡 Простими словами: Мігруй шарами: бібліотека, фабрика драйвера, керування драйверами, Grid — і міряй падіння після кожного шару.
💡 In plain words: Migrate in layers: library, driver factory, driver management, Grid, and measure failures after each layer.
🗣 Як читати вголос Я б не робив міграцію одним великим мерджем. Спочатку інвентаризація: компілятор і пошук по DesiredCapabilities, findElementBy і старих таймаутах. Потім оновлюю бібліотеку, а всі зміни створення драйвера концентрую в одній фабриці. Далі прибираю WebDriverManager, бо в четвертій версії є Selenium Manager. Grid 4 піднімаю поруч зі старим і переводжу джоби частинами, порівнюючи частку падінь. Окремо перевіряю кастомні капабіліті хмарної ферми — без префікса вендора W3C їх не прийме.
🗣 How to say it out loud I would not do the migration as one big merge. First an inventory: the compiler plus a search for DesiredCapabilities, findElementBy and old-style timeouts. Then I upgrade the library and keep all driver creation changes inside one factory. Next I drop WebDriverManager, since version four has Selenium Manager. I bring up Grid 4 alongside the old grid and move jobs across in batches, comparing failure rates. Separately I check the cloud farm's custom capabilities, because W3C rejects them without a vendor prefix.