Перейти к основному содержимому

Selenium

Тестировщику Разработчику Аналитику
Теория данных (раздел 3)

Глубокий разбор Selenium. Для новых проектов часто берут Playwright — короче порог входа и стабильнее ожидания. Selenium остаётся must-know — legacy-код, Appium, корпоративные стеки.


Selenium​

"Машины не думают как люди. Но они могут повторять то, что люди делают — и делать это миллионы раз подряд, не уставая, не ошибаясь в рутине, не отвлекаясь на шум за окном".

Единый формат практики в статье:

  • когда использовать Selenium в проекте;
  • минимальный рабочий паттерн запуска и проверки;
  • типичные ошибки и командные правила против flaky.
Когда Selenium выбирать в 2026

Selenium особенно уместен, если у вас enterprise-стек с долгой историей, нужна широкая кросс-браузерность и уже есть зрелая инфраструктура на WebDriver. Для нового проекта часто быстрее стартуют с Playwright, но знание Selenium остаётся обязательным в QA-практике.

Быстрый маршрут по статье:

  1. Подготовить среду и первый запуск браузера.
  2. Освоить локаторы и ожидания.
  3. Закрепить архитектуру тестов (изоляция, данные, отчёты, CI).
  4. Добавить headless и масштабирование под пайплайн.

Перед глубоким разбором полезно зафиксировать контекст выбора:

  • Selenium хорошо подходит для legacy-проектов, enterprise-стеков и задач с широкой браузерной совместимостью;
  • для новых фронтов часто быстрее стартуют с Playwright, особенно если важны встроенная отладка и auto-wait;
  • общая стратегия выбора инструмента описана в автоматизации тестирования и каталоге инструментов.

Если читаете статью впервые, двигайтесь так: базовый запуск -> локаторы -> ожидания -> архитектура тестов -> CI.


Интуитивные действия, которые мы совершаем, даже не замечая​

Когда человек открывает веб-страницу, он вступает в диалог с интерфейсом. Этот диалог идёт через простые, привычные жесты:

  • указатель мыши перемещается к нужной кнопке — курсор меняет форму, зрительная система подтверждает: "да, это кликабельно";
  • палец нажимает клавишу — символ появляется в поле ввода;
  • колесо прокрутки вращается — контент сдвигается, открывая новые блоки;
  • вкладки переключаются — внимание переходит с одного контекста к другому;
  • форма отправляется — страница реагирует — загружается, появляется сообщение, меняется URL.

Эти действия кажутся естественными, потому что они отточены годами повседневного использования. Компьютер при этом остаётся пассивным наблюдателем — он отвечает на события, но не инициирует их, если не получит команду. Браузер — это интерпретатор, который реагирует на сигналы от операционной системы — клик мыши → событие click, нажатие клавиш → событие keydown, прокрутка → событие scroll.

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


Предел человеческой выносливости​

Рассмотрим типичную задачу тестировщика: проверить, как веб-форма реагирует на 100 различных комбинаций логинов и паролей. Вручную это означает:

  • открыть страницу;
  • ввести данные;
  • нажать кнопку;
  • дождаться ответа;
  • зафиксировать результат;
  • очистить форму;
  • повторить — 99 раз.

За первый час внимательность снижается. За второй — начинаются ошибки — пропущенное поле, опечатка в пароле, случайный клик мимо кнопки. За третий — появляется усталость, раздражение, снижение мотивации. Это не недостаток человека. Это физиологическая характеристика человеческого восприятия — мозг устроен так, чтобы искать новизну, а не выполнять идентичные действия. Рутина — не его среда.

Тем временем, машина:

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

Проблема в одном: машина не знает, что делать — пока ей не опишут последовательность команд на понятном ей языке. Задача инструментов вроде Selenium — дать человеку возможность формализовать поведение пользователя и передать его исполнителю: браузеру.


Selenium​

Selenium — это не одна программа. Это экосистема, объединяющая стандарты, протоколы, драйверы и библиотеки для программного управления веб-браузерами. Его главная задача — позволить разработчику писать код, который:

  • запускает браузер как отдельный процесс;
  • открывает в нём страницы по URL;
  • находит элементы интерфейса (кнопки, поля, ссылки) по их признакам;
  • имитирует действия пользователя — клики, ввод текста, наведение, прокрутку;
  • считывает состояние страницы — заголовок, URL, тексты, атрибуты, cookies;
  • реагирует на изменения — ожидает появления элемента, проверяет текст сообщения, перехватывает всплывающие окна.

Selenium стал стандартом де-факто в индустрии автоматизированного тестирования веб-приложений. Его поддерживают все основные браузеры — Chrome, Firefox, Safari, Edge. Он интегрируется с современными фреймворками тестирования (pytest, JUnit, TestNG), системами непрерывной интеграции (Jenkins, GitLab CI, GitHub Actions) и облачными платформами (Sauce Labs, BrowserStack, LambdaTest). Его используют крупнейшие технологические компании — от Google и Microsoft до Яндекса и Сбера.

Это мост между логикой программы и живым поведением пользователя в браузере.


Краткая история​

История Selenium началась в 2004 году. Джейсон Хаггинс, инженер в компании ThoughtWorks, разрабатывал веб-приложение и столкнулся с рутиной ручного тестирования. Чтобы ускорить процесс, он создал внутренний скрипт на JavaScript, который внедрялся в страницу и управлял ею "изнутри" — это был Selenium Core.

Позже, поняв масштаб проблемы, он добавил серверную часть — Selenium RC (Remote Control), который позволял управлять браузером из внешней программы на любом языке. Это был прорыв: тесты перестали быть привязанными к JavaScript и браузеру, в котором они выполнялись.

В 2009 году проект объединился с инструментом WebDriver (разрабатывавшимся Саймоном Стюартом в Google), чтобы преодолеть ограничения архитектуры RC. Результатом стала Selenium WebDriver — более надёжная, прямая и безопасная модель взаимодействия с браузером через нативные API. Именно она легла в основу современного Selenium.

В 2018 году W3C утвердил WebDriver как официальный стандарт для автоматизации браузеров. Selenium стал его наиболее полной и зрелой реализацией.

Эта эволюция — от хака до стандарта — отражает фундаментальную потребность индустрии — иметь единый, открытый, независимый способ управлять браузером. Selenium её удовлетворил.


Где применяется Selenium​

Основная и самая зрелая область применения — автоматизированное тестирование веб-приложений. Здесь Selenium проверяет:

  • корректность отображения элементов и макетов;
  • логику взаимодействия — кнопки реагируют, формы валидируются, переходы работают;
  • интеграцию с бэкендом: данные сохраняются, ошибки обрабатываются;
  • регрессии: изменения в коде не сломали уже работающий функционал.

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

Вторая важная сфера — сбор данных с динамических веб-сайтов. Многие современные приложения построены как SPA (Single Page Applications) — React, Angular, Vue. В них контент не приходит в виде готового HTML, а формируется на клиенте — в браузере — с помощью JavaScript. Классические методы парсинга (например, requests + BeautifulSoup) получают "пустую оболочку", в то время как Selenium:

  • дожидается полной инициализации приложения;
  • прокручивает страницу, чтобы активировать lazy-loading;
  • нажимает кнопки "Загрузить ещё";
  • извлекает данные уже после их появления в DOM.

Это позволяет собирать информацию с маркетплейсов, новостных агрегаторов, карт, календарей — везде, где данные подгружаются после события.

Третья группа задач — имитация пользовательского поведения:

  • нагрузочное тестирование — запустить 10, 100, 1000 сессий, каждая из которых ведёт себя как реальный пользователь (в сочетании с инструментами вроде Selenium Grid или Selenoid);
  • демонстрационные скрипты: автоматическая запись прохождения тура по продукту для презентаций;
  • аудит доступности: проверка, как интерфейс ведёт себя при навигации с клавиатуры;
  • мониторинг — регулярная проверка, доступен ли сайт, корректно ли отображается критический элемент, изменилась ли цена.

Четвёртая, более деликатная область — персональная автоматизация:

  • заполнение часто повторяющихся форм (анкеты, отчёты, заявки);
  • авторизация в личных кабинетах (при условии соблюдения законов и условий использования).

Здесь важна ответственность: автоматизация личных задач — это удобство, но только если она не нарушает правила сервиса и не ставит под угрозу безопасность аккаунта.


Что Selenium НЕ умеет делать​

Selenium работает исключительно с веб-браузерами. Он не взаимодействует с десктопными приложениями (Word, Photoshop, 1С), мобильными нативными приложениями (если только они не встроены в WebView и не управляются через Appium), терминальными утилитами.

Selenium не предназначен для обхода систем защиты. Такие технологии, как Cloudflare, reCAPTCHA, hCaptcha, анализируют поведение пользователя — скорость ввода, траекторию мыши, отпечаток браузера. Selenium в базовой конфигурации легко детектируется, потому что:

  • у него отсутствует естественная "дрожь" мыши;
  • ввод текста мгновенный, без пауз;
  • в DOM присутствует свойство navigator.webdriver = true;
  • отсутствуют события mousemove, mousedown, mouseup между click.

Обход таких систем требует дополнительных мер — и часто выходит за рамки легального использования.

Selenium не заменяет API. Если у сервиса есть публичный API — это всегда предпочтительный способ взаимодействия. API быстрее, стабильнее, менее ресурсоёмок, и его поведение документировано. Парсинг через Selenium — это вторичный, резервный путь, когда API отсутствует или недостаточен.


Архитектура Selenium​

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


Браузер​

Современный браузер — это целый многопроцессный оркестр:

  • браузерный процесс управляет окнами, вкладками, сетью;
  • процесс рендеринга (по одному на вкладку или домен) — строит DOM, применяет CSS, выполняет JavaScript;
  • процесс GPU — отвечает за анимации и отрисовку;
  • процесс утилит — работает с файлами, прокси, шифрованием.

Эта изоляция повышает безопасность и стабильность: если одна вкладка зависнет — остальные продолжат работу. Но она же блокирует прямой доступ извне: внешняя программа не может просто "нажать кнопку" в чужом процессе — это нарушение модели безопасности операционной системы.


Протокол WebDriver​

Чтобы решить эту проблему, был разработан WebDriver — стандарт, описывающий, как управлять пользовательским агентом через HTTP-запросы. Это не привязка к конкретному языку или браузеру. Это спецификация — какие команды существуют (get, findElement, click, sendKeys), какие параметры они принимают, какие ответы возвращают.

Каждая команда — это HTTP-запрос в формате JSON к локальному серверу. Например, открытие страницы:

POST /session/{session_id}/url
{
"url": "https://example.com"
}

Ответ — статус выполнения и, при успехе, пустое тело.

Такой подход делает систему расширяемой: любой браузер может реализовать сервер WebDriver, и любой клиент может отправлять ему команды.


Драйвер браузера​

Драйвер (например, chromedriver) — это небольшая программа-сервер, разрабатываемая командой браузера (Google для Chrome, Mozilla для Firefox). Она:

  • запускается отдельным процессом на той же машине;
  • открывает TCP-порт и принимает HTTP-запросы по протоколу WebDriver;
  • переводит эти запросы в нативные вызовы браузера через внутренние API (DevTools Protocol для Chrome, Marionette для Firefox);
  • возвращает результаты в формате JSON.

Драйвер — это "легитимный агент". Он работает с полными правами, потому что запускается явно пользователем (или тестовой системой), а не внедряется извне. Это решение балансирует между безопасностью и функциональностью.


Библиотека клиента​

Код на Python, Java, C# не должен формировать JSON-запросы вручную. Библиотека selenium берёт на себя:

  • создание HTTP-запросов;
  • обработку ответов;
  • преобразование данных (например, JSON-описание элемента → объект WebElement);
  • управление сессиями и ошибками.

Класс webdriver.Chrome() — это фабрика, которая:

  1. запускает chromedriver как дочерний процесс;
  2. устанавливает соединение с его HTTP-сервером;
  3. создаёт объект WebDriver, инкапсулирующий соединение.

Всё это скрыто от пользователя. Разработчик видит прозрачный интерфейс: driver.get(url), driver.find_element(...), element.click().


Схема взаимодействия​

[ Ваш код на Python ]
↓
[ Библиотека selenium — формирует команду ]
↓
[ HTTP-запрос по WebDriver-протоколу → localhost:port ]
↓
[ chromedriver — получает запрос, проверяет его ]
↓
[ Вызов Chrome DevTools Protocol через внутренние API ]
↓
[ Движок Chrome — изменяет DOM, генерирует события, перерисовывает ]
↓
[ Ответ от chromedriver → JSON-статус ]
↓
[ Библиотека selenium — создаёт объект WebElement / возвращает данные ]
↓
[ Ваш код получает результат и продолжает работу ]

Эта архитектура объясняет, почему:

  • драйвер должен быть той же версии, что и браузер (он использует внутренние API, которые могут меняться);
  • Selenium не может работать без запущенного драйвера;
  • каждый экземпляр webdriver.Chrome() — это полноценный браузер, со своими cookies, кэшем и сессией.

Подготовка среды​

Автоматизация начинается не с кода, а с доверия к инфраструктуре. Если драйвер не той версии, если библиотека устарела, если браузер обновился без уведомления — скрипт упадёт на первом же шаге. Чтобы этого не произошло, нужна устойчивая, самодостаточная и легко воспроизводимая среда.

Практический чек-лист подготовки:

  • зафиксируйте версии Python, Selenium и браузера в документации проекта;
  • убедитесь, что локальный запуск и запуск в CI повторяют одну и ту же конфигурацию;
  • сразу сохраните "эталонный" минимальный тест smoke, который подтверждает здоровье окружения.

Минимальный smoke для проверки среды:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

with webdriver.Chrome(service=Service(ChromeDriverManager().install())) as driver:
driver.get("https://example.com")
assert "Example" in driver.title

Базовый стек​

Для работы с Selenium в 2025 году минимальный набор выглядит так:

  • Python 3.8 или новее — язык с чётким синтаксисом, богатой экосистемой и отличной поддержкой Selenium. Версии ниже 3.8 уже не поддерживаются сообществом (например, webdriver-manager требует Python ≥ 3.7, но 3.8 — более безопасный выбор).
  • pip — стандартный пакетный менеджер Python. Он идёт в комплекте с официальной сборкой интерпретатора.
  • Браузер — Chrome или Firefox. Они имеют наиболее стабильные и быстро обновляемые драйверы. Safari и Edge тоже поддерживаются, но с оговорками: Safari требует включения "Разрешить удалённую автоматизацию" в настройках, Edge — дополнительной настройки политик в корпоративной среде.
  • Интернет-соединение — для автоматической загрузки драйверов.

Всё остальное (selenium, webdriver-manager, драйверы) устанавливается программно:

pip install selenium webdriver-manager

Установка библиотек​

Первый шаг — установка клиентской библиотеки Selenium. Выполняем в терминале:

pip install selenium

Эта команда загружает и устанавливает последнюю стабильную версию пакета selenium из PyPI (Python Package Index). На момент 2025 года это — Selenium 4.x, в котором:

  • полная поддержка стандарта W3C WebDriver;
  • встроенная поддержка относительной навигации (driver.back(), driver.forward());
  • улучшенный API для работы с iframe и вкладками;
  • нативные методы для прокрутки (scroll_by_amount, scroll_to_element);
  • более строгая типизация (совместимость с mypy).

Проверить установку можно так:

python -c "import selenium; print(selenium.__version__)"

Если выводится версия (например, 4.24.0) — всё в порядке.


Проблема совместимости​

Драйвер — это не "универсальный ключ". Это точный переводчик между протоколом WebDriver и внутренним API конкретной версии браузера. Chrome 131 работает с новыми фичами DevTools Protocol, которых нет в Chrome 128. chromedriver 131 знает об этих фичах. chromedriver 128 — нет. Попытка использовать несовместимые версии приводит к одной из самых частых ошибок:

SessionNotCreatedException: session not created: This version of ChromeDriver
only supports Chrome version 128
Current browser version is 131.0.6778.85

Раньше разработчики решали это вручную:

  1. Открыть chrome://version → скопировать версию (например, 131.0.6778.85);
  2. Перейти на https://chromedriver.chromium.org;
  3. Найти архив, соответствующий мажорной версии (131);
  4. Скачать, распаковать, переместить chromedriver в проект или в PATH;
  5. Повторить при каждом обновлении браузера.

Это утомительно, подвержено ошибкам и не масштабируется. Особенно в командах или CI-средах.


webdriver-manager​

webdriver-manager — это вспомогательная библиотека, которая:

  • определяет текущую версию установленного браузера;
  • находит подходящую версию драйвера на официальных зеркалах (Google, Mozilla);
  • скачивает её в кэш (обычно ~/.wdm/drivers/);
  • возвращает путь к исполняемому файлу.

Установка:

pip install webdriver-manager

После этого код инициализации браузера становится устойчивым к обновлениям:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

# Создаём сервис, который управляет жизненным циклом chromedriver
service = Service(ChromeDriverManager().install())

# Запускаем браузер
driver = webdriver.Chrome(service=service)

Что происходит внутри:

  1. ChromeDriverManager().install():
    • определяет версию установленного браузера:
google-chrome --version
# Google Chrome 131.0.6778.85
  • извлекает мажорную версию 131;
  • проверяет локальный кэш: есть ли chromedriver для версии 131;
  • если нет — скачивает с https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-Тестирование/...;
  • распаковывает и возвращает путь: /home/user/.wdm/drivers/chromedriver/linux64/131/chromedriver.
  1. Service(...) оборачивает этот путь в объект, управляющий запуском и остановкой драйвера.

  2. webdriver.Chrome(service=service):

    • запускает chromedriver как дочерний процесс;
    • устанавливает HTTP-соединение с ним;
    • создаёт сессию и возвращает управление.

Этот подход закрывает большинство типовых проблем с окружением (версия драйвера, путь к браузеру, рассинхрон с Chrome). Он работает на Windows, macOS, Linux, в Docker-контейнерах (при условии наличия браузера) и в CI/CD-пайплайнах. Для нестандартных корпоративных политик и старых браузеров всё равно может понадобиться ручная настройка — это нормально.

Примечание для Linux (Arch, Ubuntu и др.)
Если Chrome установлен через пакетный менеджер, его версия обновляется вместе с системой. webdriver-manager корректно определяет её через google-chrome --version.
Для Firefox — аналогично: geckodriver управляется через GeckoDriverManager().
В headless-средах (например, сервер без GUI) важно установить xvfb или использовать аргумент --headless=new — об этом позже.

# Arch
yay -S google-chrome
# Ubuntu/Debian
sudo apt install google-chrome-stable
google-chrome --version

Явное указание пути​

В редких случаях (например, в закрытой сети без интернета, или при строгих требованиях к версиям) используется ручная установка драйвера. Алгоритм:

  1. Скачать драйвер вручную и положить, например, в папку project/drivers/;
  2. Указать путь явно:
service = Service("./drivers/chromedriver")
driver = webdriver.Chrome(service=service)

Этот способ даёт полный контроль, но требует ручного сопровождения. Лучше применять его только в исключительных обстоятельствах — например, когда политика безопасности запрещает внешние HTTP-запросы.


Добавление в PATH​

Раньше драйвер копировали в системную папку (/usr/local/bin на Linux, C:\Windows\System32 на Windows), чтобы он был доступен "из любого места". Тогда инициализация упрощалась:

driver = webdriver.Chrome() # без Service — ищет chromedriver в PATH

Сейчас это не рекомендуется, потому что:

  • обновление браузера может сломать совместимость без предупреждения;
  • в проекте может быть несколько тестов с разными требованиями к версиям;
  • в CI-средах сложно гарантировать чистоту PATH.

Лучше держать драйверы в изолированном кэше (~/.wdm/) и управлять ими через webdriver-manager.


Hello, Browser​

Теперь — самый простой, но критически важный скрипт:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

with webdriver.Chrome(service=Service(ChromeDriverManager().install())) as driver:
driver.get("https://example.com")
print("Заголовок страницы:", driver.title)

Разберём каждую строчку.


Конструкция with​

with — это контекстный менеджер. Он автоматически вызывает driver.quit() при выходе из блока — даже если произошла ошибка. Это важно, потому что:

  • каждый webdriver.Chrome() запускает полноценный браузер — процесс, потребляющий 300–500 МБ ОЗУ;
  • при аварийном завершении (ошибка, Ctrl+C) процесс может остаться "висеть";
  • накопление таких "зомби" приводит к перегрузке памяти, особенно при запуске сотен тестов.

Без with пришлось бы писать:

driver = webdriver.Chrome(...)
try:
# ... работа
finally:
driver.quit() # обязательно!

with делает это неявно, надёжно и элегантно.


Метод driver.get(url)​

get() — команда "открой страницу и дождись базовой загрузки":

driver.get("https://example.com/login")
assert "Login" in driver.title

Под "полностью" понимается:

  • HTML-документ построен;
  • все синхронные скрипты выполнены;
  • DOM готов к взаимодействию.

Он не ждёт асинхронных операций — AJAX-запросов, динамической подгрузки контента, анимаций. Это частый источник заблуждений: после get() элемент может ещё отсутствовать. Об этом — в разделе про ожидания.


Свойство driver.title​

title возвращает содержимое тега <title> из <head>. Это самый простой способ убедиться, что:

  • страница действительно загрузилась;
  • мы находимся в правильном месте (например, не попали на страницу ошибки 404);
  • браузер отвечает на команды.

Вывод:

Заголовок страницы: Example Domain

Если вместо этого — ошибка или пустая строка — значит, что-то пошло не так — сетевая проблема, блокировка, таймаут. Это наш первый сигнал о неисправности.


Что происходит при первом запуске​

Проследим за жизненным циклом:

  1. Интерпретатор Python запускает скрипт;
  2. ChromeDriverManager().install() — определяет версию Chrome, скачивает драйвер (если нужно), возвращает путь;
  3. Service(...) создаёт объект, управляющий драйвером;
  4. webdriver.Chrome(...):
    • запускает chromedriver как отдельный процесс;
    • chromedriver стартует локальный HTTP-сервер (например, на порту 54321);
    • создаёт сессию, получает session_id;
    • открывает новое окно браузера (с чистым профилем, без расширений, без истории);
  5. driver.get("https://example.com") → HTTP-POST на /session/{id}/url;
  6. Chrome:
    • устанавливает соединение с example.com;
    • получает HTML-ответ;
    • строит DOM-дерево;
    • выполняет скрипты (на этом сайте их нет);
    • обновляет заголовок окна;
  7. driver.title → HTTP-GET /session/{id}/title → получает "Example Domain".

Всё это происходит за 2–4 секунды — и в этом времени заключена вся мощь интеграции: от Python-кода до пикселей на экране.


Поиск элементов​

Когда человек смотрит на страницу, его мозг мгновенно распознаёт объекты:
"Вот поле ввода email — оно подписано "Электронная почта", имеет рамку, курсор мигает внутри".
Машина этого не умеет. Для неё страница — это древовидная структура HTML-элементов, каждый со своими атрибутами, текстом и положением в иерархии.


DOM​

DOM (Document Object Model) — это программная модель HTML-документа. Браузер строит его по мере загрузки:

  • <html> — корень;
  • внутри <head> и <body>;
  • <body> содержит <div>, <form>, <input>, <button> и так далее — с вложенностью до десятков уровней.

Каждый элемент — это объект со свойствами:

  • tagName — имя тега (input, button, a);
  • id, class, name, placeholder, type — атрибуты;
  • textContent — видимый текст;
  • children — дочерние элементы;
  • parentElement — родитель.

Selenium не "смотрит на экран". Он запрашивает у браузера описание элементов по заданным критериям. Это как запрос в базу данных: "Найди все <input> с name="password"".


Локаторы​

Selenium предлагает семь стратегий поиска — от самых простых до самых мощных. Порядок ниже — не случайный. Это иерархия надёжности и предпочтительности.

Командное правило, которое сильно снижает flakiness:

  1. Сначала id и name.
  2. Затем устойчивые css по data-* атрибутам.
  3. XPath оставляем как резерв для сложных случаев.

Если у вас frontend-команда рядом, договоритесь о data-testid для критичных элементов (логин, оплата, подтверждение заказа). Это окупается уже на первом крупном редизайне.


By.ID​

Атрибут id в HTML должен быть уникальным на странице. Это требование спецификации. Хорошо спроектированный интерфейс использует id для ключевых элементов — форм, кнопок отправки, основных блоков.

Пример HTML:

<input type="email" id="user-email" placeholder="Введите email">

Код:

email_field = driver.find_element(By.ID, "user-email")

Преимущества:

  • однозначность: если id задан верно — найдётся ровно один элемент;
  • скорость: браузер использует хеш-таблицу для поиска по id, это O(1);
  • читаемость: имя user-email сразу говорит о назначении.

Рекомендация: если у элемента есть id — используйте его. Это первый и главный выбор.


By.NAME​

Атрибут name широко используется в формах, особенно в legacy-коде. Он не обязан быть уникальным, но часто уникален в пределах одной формы.

Пример:

<input type="password" name="pass">

Код:

password_field = driver.find_element(By.NAME, "pass")

Когда name подходит:

  • элемент входит в форму, и name соответствует полю в данных отправки (application/x-www-form-urlencoded);
  • id не задан, но name стабилен и осмыслен.

Осторожно: если на странице несколько элементов с одинаковым name — find_element() вернёт первый. Это может быть не тот элемент, что ожидается.


By.CLASS_NAME​

Классы (class) используются для стилизации и поведения. Один элемент может иметь несколько классов: class="btn btn-primary submit".

Пример:

<button class="btn btn-large confirm">Подтвердить</button>

Код:

button = driver.find_element(By.CLASS_NAME, "confirm")

Важные нюансы:

  • By.CLASS_NAME ищет ровно один класс. Если написать "btn btn-large" — поиск не сработает (пробел интерпретируется как часть имени).
  • если класс динамический (например, loading_abc123), он может меняться при каждом обновлении — такой локатор хрупок.
  • классы часто дублируются: кнопка "Отмена" и "Подтвердить" могут иметь class="btn".

Лучше использовать By.CLASS_NAME, только если:

  • класс уникален и семантичен (confirm, delete-warning);
  • он не генерируется автоматически (например, не sc-fjdhpX из CSS-in-JS).

By.TAG_NAME​

Ищет по имени HTML-тега — a, button, input, h1.

Пример:

first_link = driver.find_element(By.TAG_NAME, "a")

Применение:

  • получение всех ссылок на странице (find_elements(By.TAG_NAME, "a"));
  • работа с уникальными тегами — <title>, <meta>, <base>;
  • быстрый доступ к <body> для прокрутки или ввода глобальных горячих клавиш.

Ограничение: почти никогда не подходит для точечного взаимодействия. На странице десятки <div> и <span> — какой из них нужен?


Эти стратегии ищут <a>-элементы по видимому тексту.

Полный совпадение:

faq_link = driver.find_element(By.LINK_TEXT, "Часто задаваемые вопросы")

Частичное совпадение:

support_link = driver.find_element(By.PARTIAL_LINK_TEXT, "поддержк")

Особенности:

  • работает только с <a>-тегами;
  • чувствителен к пробелам, регистру, переносам строк;
  • ломается при изменении формулировки: "FAQ" вместо "Часто задаваемые вопросы".

Применяйте, когда текст стабилен и семантичен (например, в навигационном меню), но избегайте в динамических интерфейсах.


By.CSS_SELECTOR​

CSS-селекторы — язык, изначально созданный для стилизации, но идеально подходящий для поиска. Они читаются слева направо, от общего к частному, и поддерживают:

  • поиск по id: #user-email;
  • по классу: .btn;
  • по атрибуту: [type="submit"], [placeholder*="email"];
  • комбинации: form#login input[name="email"];
  • псевдоклассы: :first-child, :nth-of-type(2).

Примеры:

# Кнопка с классом "submit" внутри формы с id="login"
driver.find_element(By.CSS_SELECTOR, "form#login .submit")

# Поле ввода, в placeholder которого есть "email"
driver.find_element(By.CSS_SELECTOR, "input[placeholder*='email']")

# Первая кнопка в теле документа
driver.find_element(By.CSS_SELECTOR, "body button:first-of-type")

Преимущества CSS-селекторов:

  • лаконичность и выразительность;
  • поддержка большинства современных браузеров на уровне движка (очень быстро);
  • не зависят от внутренней структуры DOM так сильно, как XPATH;
  • интуитивно понятны разработчикам фронтенда.

Рекомендация: если id и name недоступны — CSS_SELECTOR — ваш основной инструмент. Он надёжнее XPATH в большинстве случаев.


By.XPATH​

XPATH (XML Path Language) — мощный язык навигации по дереву элементов. Он позволяет:

  • искать по тексту: //button[text()='Войти'];
  • искать по предкам/потомкам: //div[@class='card']//input;
  • использовать функции — contains(), starts-with(), normalize-space().

Примеры:

# Кнопка с текстом "Купить", внутри div с классом "product"
button = driver.find_element(By.XPATH, "//div[contains(@class, 'product')]//button[text()='Купить']")

# Поле ввода, placeholder которого содержит "email"
field = driver.find_element(By.XPATH, "//input[contains(@placeholder, 'email')]")

# Любой элемент с текстом "Успешно"
message = driver.find_element(By.XPATH, "//*[text()='Успешно']")

Почему XPATH хрупок:

  • структура DOM часто меняется — добавляются обёртки <div>, меняются классы, перестраивается вложенность;
  • //div//div//button может найти не ту кнопку, если появится второй блок;
  • абсолютные пути (/html/body/div[2]/form/input[1]) ломаются при малейшем изменении разметки.

Когда XPATH неизбежен:

  • элемент не имеет id, name, устойчивого класса;
  • нужно найти по точному тексту — это единственный способ;
  • интерфейс построен фреймворком, генерирующим нестабильные class (например, Angular без data-testid).

Правило: используйте XPATH, только когда другие методы не работают. Предпочитайте семантичные выражения (см. пример выше с contains(@class, 'product')), а не позиционные пути вроде div[3]/span[2]/button.


find_element() и find_elements()​

Selenium предоставляет два фундаментально разных метода:

МетодЧто возвращаетЕсли элемент найденЕсли элемент не найден
find_element()один объект WebElementпервый подходящий элементNoSuchElementException
find_elements()список list[WebElement]список из 1+ элементовпустой список []

Когда использовать find_element()​

  • Вы уверены, что элемент должен быть один (например, id="submit").
  • Вы готовы обработать исключение, если элемент отсутствует (например, в тестах: "если кнопки нет — тест упал").
  • Вы работаете с критически важным элементом — форма, основная кнопка, заголовок.

Пример (тест на наличие ошибки):

try:
error_msg = driver.find_element(By.CLASS_NAME, "error")
assert "Неверный пароль" in error_msg.text
except NoSuchElementException:
assert False, "Ожидаемое сообщение об ошибке не появилось"

Когда использовать find_elements()​

  • Элементов может быть ноль, один или много (карточки товаров, пункты меню, чекбоксы).
  • Вы хотите проверить наличие/отсутствие без исключений.
  • Вам нужно пройти по всем элементам (например, собрать все цены).

Пример (сбор списка товаров):

cards = driver.find_elements(By.CSS_SELECTOR, ".product-card")
print(f"Найдено товаров: {len(cards)}")
for card in cards:
title = card.find_element(By.CLASS_NAME, "title").text
price = card.find_element(By.CLASS_NAME, "price").text
print(f"{title} — {price}")

Пример (проверка отсутствия всплывающего окна):

popups = driver.find_elements(By.ID, "ad-banner")
if len(popups) == 0:
print("Рекламный баннер не отображается — хорошо.")

Это гораздо надёжнее, чем try/except для простой проверки существования.


Инструменты разработчика​

Нельзя писать локаторы вслепую. Браузер уже содержит всё необходимое для их отладки.


Как открыть панель Elements​

  • F12 (Windows/Linux) или Cmd+Opt+I (macOS);
  • ПКМ → "Просмотреть код" на любом элементе;
  • через меню: Ещё → Дополнительные инструменты → Инструменты разработчика.

Вкладка Elements показывает текущее DOM-дерево — то, что видит Selenium.


Поиск по локатору внутри панели​

  • Нажмите Ctrl+F (Cmd+F) — появится строка поиска;
  • Введите:
    • #user-email — найдёт по id;
    • .btn — по классу;
    • input[name='email'] — по CSS;
    • //input[@placeholder='Логин'] — по XPATH.

Если найдено — отобразится количество совпадений и подсветка. Это мгновенная проверка перед написанием кода.


Копирование селекторов​

ПКМ на элементе → Copy →

  • Copy selector — генерирует CSS-селектор (часто переусложнённый: body > div:nth-child(3) > form > input#email);
  • Copy XPath — генерирует абсолютный XPATH (ещё хуже: /html/body/div[3]/form/input[1]).

Эти селекторы не стоит использовать "как есть". Они хрупкие. Но они — отличная отправная точка. Возьмите их, упростите, сделайте семантичнее:

Было:

#root > div > div > div:nth-child(2) > form > div:nth-child(1) > input

Стало:

form input[placeholder*='email']

Или:

//form//input[contains(@placeholder, 'email')]

Как выбирать локатор​

Представьте: вам нужно нажать кнопку "Отправить" в форме.

  1. Откройте DevTools → Elements.

  2. Найдите кнопку в дереве (кликните по ней в интерфейсе → браузер подсветит узел).

  3. Проверьте атрибуты:

    • Есть id="submit-btn"? → By.ID;
    • Есть name="submit"? → By.NAME;
    • Есть уникальный класс class="btn-submit"? → By.CLASS_NAME или By.CSS_SELECTOR(".btn-submit");
    • Нет id/name, но есть type="submit" и value="Отправить"? → By.CSS_SELECTOR("input[type='submit'][value='Отправить']");
    • Только текст "Отправить" и тег <button>? → By.XPATH("//button[text()='Отправить']").
  4. Проверьте в строке поиска (Ctrl+F) — сколько совпадений?

    • Если 1 — локатор хороший.
    • Если >1 — уточните: добавьте предка (form button), атрибут (button.primary), текст.
  5. Запишите в код.

Этот алгоритм превращает поиск из угадывания в инженерную задачу.


Взаимодействие с элементами​

Каждый метод объекта WebElement — это не просто вызов функции. Это инициация цепочки событий, идентичной пользовательскому действию. Разберём ключевые операции.


.click()​

Когда человек кликает по кнопке, браузер генерирует последовательность событий:

  1. mousedown — кнопка мыши нажата;
  2. mouseup — кнопка отпущена;
  3. click — полный цикл завершён;
  4. (если элемент — <a> или <form>) — переход или отправка.

Selenium, вызывая element.click(), эмулирует именно эту цепочку — через драйвер и DevTools Protocol. Это важно, потому что:

  • некоторые фреймворки (например, React) слушают не click, а mousedown;
  • кастомные элементы (без <button>, а с <div onclick="...">) могут реагировать только на полный цикл;
  • элемент должен быть кликабельным — видимым, не перекрытым, не заблокированным (disabled).

Если click() вызван на элементе, который:

  • скрыт (display: none);
  • перекрыт другим слоем (z-index);
  • находится вне области просмотра без прокрутки;

— Selenium выбросит исключение ElementClickInterceptedException или ElementNotInteractableException.

Это не ошибка кода. Это сигнал: "Интерфейс изменился, и действие невозможно в текущем состоянии".

Решение — использовать явные ожидания (WebDriverWait), дожидаясь, пока элемент станет кликабельным:

wait = WebDriverWait(driver, 10)
button = wait.until(EC.element_to_be_clickable((By.ID, "submit")))
button.click()

Этот код безопасен: он ждёт до 10 секунд, пока кнопка не станет видимой и не перестанет быть перекрытой.


.send_keys("текст")​

Метод send_keys() имитирует нажатие клавиш на клавиатуре. Он:

  • фокусируется на элементе (вызывает focus);
  • генерирует события keydown, keypress, keyup для каждого символа;
  • вставляет текст в значение поля (value);
  • вызывает события input и change (если текст изменился).

Это важно для SPA: React/Vue следят именно за этими событиями, чтобы обновить внутреннее состояние.


Особенности и подводные камни​

  1. Поле должно быть активным и редактируемым
    — Если input имеет атрибут readonly или disabled, send_keys() вызовет InvalidElementStateException.

  2. Автозаполнение и маски
    — В полях с масками (телефон, дата) ввод send_keys("123") может преобразоваться в +7 (123) ___-__-__. Это ожидаемо и корректно.

  3. Очистка перед вводом
    — send_keys() добавляет текст к текущему значению. Чтобы заменить — сначала вызовите .clear():

field.clear()
field.send_keys("новый текст")

Метод .clear() эмулирует выделение всего текста (Ctrl+A) и нажатие Delete — и тоже генерирует события input/change.

  1. Специальные клавиши
    — send_keys() принимает не только строки, но и константы из selenium.webdriver.common.keys:
from selenium.webdriver.common.keys import Keys

field.send_keys("привет")
field.send_keys(Keys.ENTER) # имитация нажатия Enter
field.send_keys(Keys.TAB) # переход к следующему полю
field.send_keys(Keys.ARROW_DOWN) # стрелка вниз (для списков)

Это позволяет эмулировать навигацию по клавиатуре — критически важно для доступности.


Чтение состояния — .text, .get_attribute(), .is_selected()​

Автоматизация — это не только действия, но и проверка результатов. Selenium позволяет читать:

МетодЧто возвращаетПример применения
element.textВидимый текст внутри элемента (без скрытых, без <script>)Проверка сообщения: assert "Успешно" in message.text
element.get_attribute("атрибут")Значение HTML-атрибута: value, href, class, disabled, checkedinput.get_attribute("value") — текущее содержимое поля
element.is_selected()True/False — выбран ли чекбокс/радиокнопкаПроверка состояния: assert agree_checkbox.is_selected()
element.is_enabled()True/False — доступен ли элемент для взаимодействияКнопка disabled → is_enabled() == False
element.is_displayed()True/False — отображается ли элемент (учитывает display, visibility, размеры)Проверка появления модального окна

Разница между get_attribute("value") и .text:

<input id="email" value="user@example.com">
<span id="label">Email:</span>
email_field = driver.find_element(By.ID, "email")
label = driver.find_element(By.ID, "label")

print(email_field.get_attribute("value")) # → "user@example.com"
print(label.text) # → "Email:"

Для <input>, <textarea>, <select> — значение хранится в атрибуте value. Текст внутри тега — пуст.


Управление формами​

Чекбоксы и радиокнопки​

HTML:

<input type="checkbox" id="agree" name="terms">
<input type="radio" name="plan" value="basic" checked>
<input type="radio" name="plan" value="premium">

Работа с ними:

Код ITЗагрузка примера кода…

Важно:

  • для чекбоксов — click() переключает состояние;
  • для радиокнопок — click() выбирает, другие с той же name автоматически отменяются;
  • не нужно читать checked через get_attribute() — .is_selected() надёжнее.

Выпадающие списки (<select>)​

HTML:

<select id="country">
<option value="ru">Россия</option>
<option value="kz">Казахстан</option>
<option value="by">Беларусь</option>
</select>

Способ 1 — вручную​

dropdown = driver.find_element(By.ID, "country")
# Открыть список (не всегда нужно — можно сразу искать option)
dropdown.click()

# Найти и кликнуть нужный пункт
kazakhstan = driver.find_element(By.XPATH, "//option[@value='kz']")
kazakhstan.click()

Плюсы: полный контроль.
Минусы — хрупкость — если список не открывается кликом (например, через focus), может не сработать.


Способ 2 — через класс Select​

from selenium.webdriver.support.ui import Select

dropdown = Select(driver.find_element(By.ID, "country"))
dropdown.select_by_value("kz") # по value
dropdown.select_by_visible_text("Россия") # по тексту
dropdown.select_by_index(2) # по индексу (с 0)

Преимущества Select:

  • работает со всеми стандартными <select>;
  • автоматически обрабатывает открытие/закрытие списка;
  • проверяет, доступен ли нужный пункт;
  • поддерживает мультивыбор (<select multiple>).

Рекомендация: всегда используйте Select, если работаете с <select>. Это стандартный, проверенный инструмент.


Сложные взаимодействия — ActionChains​

До сих пор мы имитировали базовые действия: клик, ввод. Но пользователь делает больше:

  • наводит курсор, чтобы открыть подменю;
  • тянет ползунок или перетаскивает карточку;
  • дважды кликает по слову;
  • прокручивает колесом;
  • зажимает Ctrl и кликает по нескольким элементам.

Для этого — ActionChains. Это строитель цепочек действий:

from selenium.webdriver.common.action_chains import ActionChains

actions = ActionChains(driver)

Наведение курсора (hover)​

menu_item = driver.find_element(By.ID, "products")
submenu = driver.find_element(By.ID, "laptops")

# Навести на "Продукты", подождать появления подменю, кликнуть "Ноутбуки"
actions.move_to_element(menu_item).perform()
wait.until(EC.visibility_of(submenu))
submenu.click()

move_to_element() — ключевой метод для работы с дропдаунами, тултипами, ховер-эффектами.


Перетаскивание (drag-and-drop)​

source = driver.find_element(By.ID, "card-1")
target = driver.find_element(By.ID, "trash")

actions.drag_and_drop(source, target).perform()
# или пошагово —
# actions.click_and_hold(source).move_to_element(target).release().perform()

Двойной клик, правый клик​

text = driver.find_element(By.ID, "document")

actions.double_click(text).perform() # выделение слова
actions.context_click(text).perform() # ПКМ → контекстное меню

Прокрутка колесом (Selenium 4+)​

actions.scroll_by_amount(0, 500).perform() # прокрутить вниз на 500px
actions.scroll_to_element(element).perform() # прокрутить, чтобы element был виден

Это заменяет старые JavaScript-хаки и работает как настоящее движение колеса.


Заполнение реалистичной формы​

Представим: регистрация на сайте с валидацией, чекбоксом и выпадающим списком.

Код ITЗагрузка примера кода…

Этот скрипт:

  • использует WebDriverWait на каждом критическом шаге;
  • проверяет состояние чекбокса вместо слепого клика;
  • читает результат, а не просто "нажимает и надеется";
  • завершает работу через with, гарантируя закрытие браузера.

Навигация​

Браузер — это не статичная страница. Это машина состояний, где каждое действие может изменить URL, историю, DOM. Selenium даёт полный контроль над этим процессом.


Управление историей — back(), forward(), refresh()​

Эти методы эмулируют стандартные кнопки браузера:

driver.get("https://example.com/page1")
driver.get("https://example.com/page2")

driver.back() # → возвращаемся на page1
driver.forward() # → снова на page2
driver.refresh() # → перезагружаем page2

Как это работает:

  • back() и forward() используют историю сессии (session history), а не кэш страниц;
  • refresh() отправляет повторный запрос на текущий URL, с теми же заголовками и cookies;
  • после refresh() все WebElement-объекты становятся недействительными — их нужно искать заново (это StaleElementReferenceException).

Практическое применение:
— Проверка, что кнопка "Назад" не ломает приложение;
— Тестирование SPA: после back() должен восстановиться правильный маршрут;
— Возврат к форме после просмотра справки.


Текущее состояние — current_url, title, page_source​

Эти свойства позволяют "осмотреться" в текущем состоянии:

СвойствоЧто возвращаетПрименение
driver.current_urlполный URL (с параметрами)Проверка редиректа: assert "success" in driver.current_url
driver.titleтекст <title>Быстрая проверка загрузки страницы
driver.page_sourceисходный HTML после обработки JavaScript (не исходный ответ сервера!)Отладка: поиск элемента, которого "не видно" в DevTools

Важно:

  • page_source — это DOM-представление, сериализованное в строку. Оно содержит изменения, внесённые JavaScript (например, динамически добавленные элементы), но не содержит содержимое <script> или комментарии, удалённые парсером;
  • page_source не обновляется автоматически — его нужно запрашивать после каждого изменения.

Пример проверки редиректа:

driver.get("https://example.com/login")
login_form = driver.find_element(By.ID, "login")
login_form.send_keys("user")
driver.find_element(By.ID, "submit").click()

# Ждём, пока URL изменится
wait.until(lambda d: "dashboard" in d.current_url)
assert "/dashboard" in driver.current_url

Управление окнами и вкладками​

Современный пользователь редко работает с одной вкладкой. Он открывает ссылки в новых вкладках, переключается между задачами, копирует данные из одного окна в другое.

Selenium поддерживает множество окон и вкладок в рамках одной сессии.


Дескрипторы​

Каждое окно или вкладка в браузере имеет уникальный идентификатор — window handle. Это строка вида "CDwindow-1234ABCD".

Основные методы:

МетодНазначение
driver.window_handlesсписок всех дескрипторов (порядок — как в интерфейсе браузера)
driver.current_window_handleдескриптор текущего окна
driver.switch_to.window(handle)переключение фокуса на окно/вкладку

Открытие новой вкладки​

Есть два способа:


Способ 1 — через JavaScript (универсальный)​

driver.execute_script("window.open('https://example.com', '_blank');")

Это создаёт новую вкладку с указанным URL (или пустую, если '').


Способ 2 — через ActionChains + клик (имитация пользователя)​

link = driver.find_element(By.LINK_TEXT, "Справка")
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL).click(link).key_up(Keys.CONTROL).perform()

Это эмулирует Ctrl+клик — стандартный способ открытия ссылки в новой вкладке.


Переключение между вкладками​

Код ITЗагрузка примера кода…

Важно:
— После переключения driver работает только с текущей вкладкой;
— find_element() ищет в активной вкладке;
— Закрытие вкладки: driver.close() → закрывает текущую, фокус переходит на предыдущую;
— Закрытие всего браузера: driver.quit().


Модальные окна — alert, confirm, prompt​

JavaScript позволяет показывать системные диалоги:

  • alert("Сообщение") — просто информирует;
  • confirm("Продолжить?") — "ОК" / "Отмена";
  • prompt("Ваше имя?", "Иван") — поле ввода + "ОК" / "Отмена".

Эти окна блокируют основной поток выполнения — ни клик, ни ввод не работают, пока окно не закрыто.

Selenium предоставляет специальный интерфейс: driver.switch_to.alert.


Работа с alert​

# Вызовем alert через JS
driver.execute_script("alert('Готово!');")

# Переключаемся на окно
alert = driver.switch_to.alert

# Читаем текст
print("Текст:", alert.text) # → "Готово!"

# Закрываем
alert.accept() # нажать "ОК"
# или
alert.dismiss() # нажать "Отмена" (для confirm/prompt)

Работа с prompt​

driver.execute_script("prompt('Введите код:', '');")

alert = driver.switch_to.alert
alert.send_keys("12345") # ввод в поле
alert.accept() # подтвердить

Ожидание появления alert​

Alert может появиться не мгновенно (например, после AJAX-запроса). Используем явное ожидание:

wait = WebDriverWait(driver, 10)
alert = wait.until(EC.alert_is_present())
alert.accept()

Это предотвращает NoAlertPresentException.


Прокрутка​

Элемент может существовать в DOM, но быть скрытым за пределами viewport. Тогда click() или send_keys() вызовут ElementNotInteractableException.

Решение — прокрутить страницу так, чтобы элемент стал видимым.


Три стратегии прокрутки​

Стратегия 1 — JavaScript​

element = driver.find_element(By.ID, "footer")

# Прокрутить, чтобы элемент оказался вверху viewport
driver.execute_script("arguments[0].scrollIntoView(true);", element)

# Или внизу (чтобы не прикрыть хедером) —
driver.execute_script("arguments[0].scrollIntoView(false);", element)

scrollIntoView() — нативный метод DOM, работает во всех браузерах.


Стратегия 2 — ActionChains​

from selenium.webdriver.common.action_chains import ActionChains

actions = ActionChains(driver)
actions.scroll_to_element(element).perform() # Selenium 4.1+
# или
actions.scroll_by_amount(0, 500).perform() # на 500px вниз

Это ближе к реальному поведению пользователя.


Стратегия 3 — Клавиши — для интерактивных элементов​

body = driver.find_element(By.TAG_NAME, "body")
body.send_keys(Keys.PAGE_DOWN) # листать постранично
# или
body.send_keys(Keys.END) # в самый конец

Подходит для SPA, где прокрутка триггерит подгрузку.


Бесконечная прокрутка​

Многие сайты (соцсети, маркетплейсы) подгружают контент при прокрутке вниз. Алгоритм сбора:

Код ITЗагрузка примера кода…

Тонкости:

  • time.sleep() — грубый способ; лучше — WebDriverWait + условие "исчез индикатор загрузки";
  • некоторые сайты ограничивают количество подгрузок (пагинация по API внутри);
  • headless-режим может вести себя иначе — проверяйте визуально.

JavaScript-инъекции​

Selenium — мощный инструмент, но не всесильный. Иногда нужно:

  • прочитать localStorage или sessionStorage;
  • вызвать внутренний метод SPA (например, app.router.push('/admin'));
  • обойти проверку readonly в поле;
  • изменить navigator.webdriver (этично — только для тестов!).

Для этого — driver.execute_script().


Чтение и запись в хранилища​

# Получить всё localStorage как JSON
local_data = driver.execute_script("return JSON.stringify(localStorage);")
data = json.loads(local_data)

# Установить значение
driver.execute_script("localStorage.setItem('theme', 'dark');")

# Удалить
driver.execute_script("localStorage.removeItem('debug');")

Аналогично для sessionStorage.


Обход ограничений (только для тестов!)​

# Снять readonly с поля
driver.execute_script("arguments[0].removeAttribute('readonly');", field)

# Заполнить напрямую (минуя события)
driver.execute_script("arguments[0].value = 'test@example.com';", email_field)

# Перезапустить событие, чтобы SPA "увидел" изменение
driver.execute_script("arguments[0].dispatchEvent(new Event('input'));", email_field)

Важно: такие приёмы нарушают принцип "тестировать как пользователь". Используйте их только когда: — интерфейс не даёт иного способа (например, поле readonly без API-доступа); — это часть теста на безопасность/валидацию; — вы чётко документируете обход.


Полный сценарий​

Соберём заголовки новостей с сайта с бесконечной лентой (например, https://news.yandex.ru):

Код ITЗагрузка примера кода…

Этот скрипт:

  • использует WebDriverWait для старта;
  • эмулирует прокрутку с паузой;
  • собирает данные после загрузки;
  • корректно завершает работу.

Проблема времени​

Рассмотрим типичную последовательность:

driver.get("https://spa-app.com")
button = driver.find_element(By.ID, "load-Data")
button.click()
result = driver.find_element(By.ID, "output")
print(result.text)

Что может пойти не так?

  1. После get() браузер загрузил HTML, но JavaScript ещё не выполнился — кнопки #load-Data нет в DOM → NoSuchElementException.
  2. После click() запущен AJAX-запрос, но ответ ещё не пришёл — элемент #output пуст или отсутствует.
  3. Ответ пришёл, но React ещё не обновил DOM — result.text пуст, хотя через 200 мс там будет "Готово".

Это не ошибки кода. Это асинхронная природа веба. Браузер — оркестр параллельных процессов — сеть, парсинг, рендеринг, выполнение скриптов. Selenium должен уметь слушать, когда нужный момент наступил.


Два типа ожиданий — неявные и явные​

Selenium предлагает два подхода к ожиданию. Они различаются по механизму, гибкости и зоне ответственности.

Практический стандарт команды

Для новых тестов берите только явные ожидания через WebDriverWait и expected_conditions. Неявные ожидания оставляйте только в legacy-наборах, где полный рефакторинг пока невозможен.


Неявные ожидания (implicitly_wait)​

driver.implicitly_wait(10) # секунды

Это настройка по умолчанию для всех операций поиска (find_element, find_elements). Если элемент не найден сразу, Selenium будет повторять попытку каждые 500 мс, пока не пройдёт указанное время.

Пример:

driver.implicitly_wait(10)
element = driver.find_element(By.ID, "delayed-button") # ждёт до 10 сек

Преимущества:

  • простота: одна строка — и все поиски "умные";
  • не нужно оборачивать каждый find_element в try/except.

Ограничения:

  • глобальный эффект: действует на всю сессию. Если один тест требует 30 сек, а другой — 2, приходится сбрасывать (driver.implicitly_wait(0));
  • не поддерживает сложные условия — нельзя дождаться, пока кнопка станет кликабельной, пока текст содержит "Успешно", пока исчезнет индикатор загрузки;
  • работает только для поиска элементов, не для проверки их состояния.

Рекомендация: не используйте implicitly_wait в новых проектах. Он устарел как основной механизм. Его место — в legacy-коде или очень простых скриптах.


Явные ожидания (WebDriverWait)​

Это целевой, декларативный подход: "подожди, пока не выполнится условие X".

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, timeout=10, poll_frequency=0.5)
element = wait.until(EC.presence_of_element_located((By.ID, "submit")))

Как это работает:

  1. WebDriverWait создаёт "наблюдателя" с таймаутом и частотой опроса;
  2. until() запускает цикл: каждые 0.5 сек вызывает переданное условие;
  3. Если условие возвращает истинное значение — возвращает результат;
  4. Если за 10 сек условие так и не выполнилось — TimeoutException.

Преимущества:

  • гибкость — можно ждать любое состояние — видимости, кликабельности, текста, URL, alert’а;
  • локальность: каждое ожидание настраивается под конкретную задачу;
  • читаемость: код сам объясняет что ждёт, а не просто сколько;
  • отладка: при падении видно, какое условие не выполнилось.

Это рекомендуемый способ в 2025 году.


Библиотека условий — expected_conditions​

Модуль selenium.webdriver.support.expected_conditions (сокращённо EC) содержит десятки предопределённых условий. Вот самые важные.


Условия для элементов​

УсловиеКогда полезно
presence_of_element_located(locator)Элемент появился в DOM (но может быть скрыт)
visibility_of_element_located(locator)Элемент видим: не display:none, не visibility:hidden, размеры > 0
element_to_be_clickable(locator)Элемент видим и не перекрыт, не disabled — можно кликать
text_to_be_present_in_element(locator, "текст")В тексте элемента есть подстрока (регистрозависимо)
text_to_be_present_in_element_value(locator, "значение")В value поля ввода есть подстрока (для <input>)

Примеры:

# Ждём, пока кнопка появится и станет кликабельной
button = wait.until(EC.element_to_be_clickable((By.ID, "pay-now")))

# Ждём сообщения об успехе
success = wait.until(
EC.text_to_be_present_in_element((By.CLASS_NAME, "alert"), "Оплата прошла")
)

# Ждём, пока поле загрузится данными
wait.until(
EC.text_to_be_present_in_element_value((By.ID, "user-email"), "@")
)

Условия для страницы​

УсловиеПрименение
title_is("Точное название")Проверка загрузки конкретной страницы
title_contains("часть")Гибкая проверка: "Корзина — Магазин"
url_to_be("https://exact.com")Точный URL
url_contains("/checkout")Часть пути или параметра
url_matches("regex")Полный контроль через регулярные выражения

Пример:

wait.until(EC.url_contains("/order/confirmed"))
assert "Спасибо за заказ" in driver.title

Условия для модальных окон и фреймов​

УсловиеНазначение
alert_is_present()Ждём появления alert, confirm, prompt
frame_to_be_available_and_switch_to_it(locator)Ждём iframe и автоматически переключаемся в него

Пример:

wait.until(EC.alert_is_present())
driver.switch_to.alert.accept()

Пользовательские условия​

Иногда нужно дождаться чего-то уникального:

  • пока в localStorage появится ключ auth_token;
  • пока на странице будет ровно 12 карточек товаров;
  • пока сумма в корзине станет больше 1000.

Для этого — кастомные условия.

Условие — это callable, который принимает driver и возвращает:

  • True / объект — успех;
  • False / None — продолжить ожидание;
  • исключение (кроме NoSuchElementException) — прервать сразу.

Ожидание количества элементов​

def elements_count_to_be(locator, count):
def _predicate(driver):
elements = driver.find_elements(*locator)
return elements if len(elements) >= count else False
return _predicate

# Ждём, пока появится хотя бы 5 товаров
cards = wait.until(elements_count_to_be((By.CLASS_NAME, "product"), 5))
print(f"Загружено {len(cards)} карточек")

Проверка localStorage​

def local_storage_contains_key(key):
def _predicate(driver):
script = f"return localStorage.getItem('{key}') !== null;"
return driver.execute_script(script)
return _predicate

wait.until(local_storage_contains_key("userSession"))

Элемент исчез (например, индикатор загрузки)​

def element_to_disappear(locator):
def _predicate(driver):
try:
element = driver.find_element(*locator)
return not element.is_displayed()
except NoSuchElementException:
return True # элемента нет — условие выполнено
return _predicate

wait.until(element_to_disappear((By.CLASS_NAME, "spinner")))

Стабильный сценарий с ожиданиями на каждом этапе​

Авторизация в SPA с динамической загрузкой:

Код ITЗагрузка примера кода…

Этот скрипт:

  • не использует фиксированные паузы (time.sleep) — ожидания адаптивны через WebDriverWait;
  • каждое действие сопровождается проверкой готовности;
  • устойчив к задержкам сети и инициализации SPA;
  • читаем и самодокументируем.

Типичные ошибки и как их избежать​

ОшибкаПричинаРешение
NoSuchElementExceptionЭлемент ещё не в DOMИспользовать presence_of_element_located или visibility_of...
ElementNotInteractableExceptionЭлемент скрыт, перекрыт, не кликабельныйelement_to_be_clickable + прокрутка
StaleElementReferenceExceptionЭлемент был удалён/пересоздан после поискаИскать элемент непосредственно перед использованием, не кэшировать надолго
Скрипт "зависает" на 10+ секТаймаут завышен, условие никогда не выполнитсяУменьшить timeout, добавить логирование, проверить локатор
Условие "выполняется", но текст пустЖдали presence, а не visibility или text_to_be_presentУточнить условие: текст может появиться позже, чем элемент

Что сделать в проекте, чтобы эти ошибки не возвращались:

  • добавить линтер/ревью-правило "без sleep, только ожидания";
  • хранить локаторы централизованно (Page Object или аналог);
  • после падений собирать скриншот, HTML и лог браузера автоматически;
  • раз в спринт разбирать flaky-тесты отдельным коротким созвоном.

Золотое правило:

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


Cookies​

Cookies — это небольшие фрагменты данных, которые сервер отправляет браузеру через заголовок Set-Cookie, а браузер возвращает при каждом последующем запросе к тому же домену. Они лежат в основе:

  • аутентификации (сессионные cookies, JWT в HttpOnly);
  • персонализации (язык, регион, тема);
  • аналитики (идентификаторы посещений);
  • защиты (CSRF-токены, SameSite).

Чтение cookies в Selenium​

Selenium даёт полный доступ к cookies текущего домена:

# Все cookies как список словарей
cookies = driver.get_cookies()
# [
# {'name' —'sessionid', 'value': 'abc123', 'domain': '.example.com', 'path': '/', 'secure': True, 'httpOnly': True},
# {'name' —'lang', 'value': 'ru', 'domain': '.example.com', 'path': '/', 'secure': False}
# ]

# Одна cookie по имени
session_cookie = driver.get_cookie("sessionid") # или None, если нет

# Конкретное значение
session_id = driver.get_cookie("sessionid")["value"] if driver.get_cookie("sessionid") else None

Каждая cookie содержит:

  • name, value — обязательные;
  • domain, path — область действия;
  • secure — только по HTTPS;
  • httpOnly — недоступна из JavaScript (защита от XSS);
  • expiry — время жизни (Unix timestamp).

Установка и удаление cookies​

Код ITЗагрузка примера кода…

Важно:
— add_cookie() можно вызывать только после открытия страницы на нужном домене (иначе InvalidCookieDomainException);
— httpOnly cookies невозможно прочитать через JavaScript, но Selenium видит их — это преимущество перед клиентскими скриптами.


Восстановление сессии без повторного логина​

Представим: вы тестируете админку, и каждый вход через форму занимает 10 секунд. Можно ли "войти раз — и использовать сессию в 100 тестах"?

Да. Алгоритм:

  1. Один раз — выполнить логин вручную или через скрипт;
  2. Сохранить cookies в файл;
  3. В каждом последующем тесте — загрузить cookies до открытия страницы.

Сохранение сессии​

Код ITЗагрузка примера кода…


Восстановление сессии​

Код ITЗагрузка примера кода…

Преимущества:

  • ускорение тестов в 3–5 раз;
  • обход многофакторной аутентификации (если сессия уже живая);
  • стабильность: не зависит от изменений в форме логина.

Ограничения:

  • cookies имеют срок жизни (обычно 30 минут–24 часа);
  • сервер может привязывать сессию к IP/User-Agent — тогда восстановление с другого устройства не сработает;
  • httpOnly cookies передаются корректно — Selenium умеет с ними работать.

localStorage и sessionStorage​

Современные приложения (React, Vue, Angular) всё чаще хранят состояние не на сервере, а в браузере:

  • localStorage — постоянное, до очистки вручную;
  • sessionStorage — живёт, пока открыта вкладка.

Типичные данные:

  • токены авторизации (если не httpOnly);
  • настройки интерфейса (тема, язык);
  • кэш данных (списки, конфигурации);
  • черновики форм.

Чтение и запись через JavaScript​

Selenium не имеет прямых методов для localStorage, но может выполнить JS:

Код ITЗагрузка примера кода…

Аналогично для sessionStorage — замените localStorage → sessionStorage.


Сохранение состояния SPA​

Допустим, приложение хранит токен в localStorage:

Код ITЗагрузка примера кода…

Важно:
— Такой подход менее безопасен, чем cookies с httpOnly;
— Он уместен только для тестов, где вы контролируете окружение;
— В продакшене никогда не храните секреты в localStorage.


Профили браузера​

Каждый запуск webdriver.Chrome() по умолчанию использует временный профиль — пустой, без истории, cookies, расширений. Это хорошо для изоляции тестов, но плохо, если нужно:

  • сохранить настройки между запусками;
  • использовать расширения (AdBlock, тестовые инструменты);
  • эмулировать "реального пользователя" с историей.

Создание постоянного профиля​

from selenium.webdriver.chrome.options import Options

options = Options()
# Указать папку профиля (если не существует — создастся)
options.add_argument("--user-Data-dir=/home/user/selenium-profile")
# Указать имя профиля (по умолчанию "Default")
options.add_argument("--profile-directory=TestProfile")

driver = webdriver.Chrome(service=Service(...), options=options)

Теперь:

  • все данные (cookies, кэш, localStorage) сохраняются между запусками;
  • можно вручную настроить браузер один раз (войти, выбрать тему, установить расширения);
  • последующие скрипты будут работать в этом контексте.

Использование существующего профиля (например, вашего основного)​

⚠️ Осторожно: не используйте основной профиль в автоматических тестах — можно повредить данные.

# Узнать путь — chrome://version → "Папка профиля"
# Пример для Linux —
options.add_argument("--user-Data-dir=/home/user/.config/google-chrome")
options.add_argument("--profile-directory=Default")

Headless + профиль — для CI​

В CI-средах можно комбинировать:

options.add_argument("--headless=new")
options.add_argument("--user-Data-dir=/tmp/chrome-profile")

Это даёт изолированный, быстрый, но "персонализированный" браузер.


Когда и как использовать сохранение состояния​

СценарийРекомендация
Тестирование логинаНикогда не восстанавливайте сессию — тестируйте полный цикл
Тестирование внутренних страницВосстанавливайте сессию — экономия времени, фокус на логике
Сбор данных с личного кабинетаТолько если вы — владелец аккаунта, и это не нарушает ToS
Мониторинг цен/наличияПредпочтительно — через API; если через браузер — используйте отдельный учётный запись
Обход платных функцийНедопустимо — нарушает условия использования и закон

Юридический контекст (РФ):
— Статья 137 УК РФ (нарушение неприкосновенности частной жизни);
— Федеральный закон №152-ФЗ "О персональных данных";
— Гражданский кодекс (нарушение условий публичной оферты — ToS сайта).

Правило: если сайт требует авторизацию для доступа к данным — это сигнал: эти данные не предназначены для автоматического сбора без явного разрешения.


Комплексный сценарий — мониторинг личного кабинета​

Цель: раз в час проверять баланс на сайте банка (только для своего аккаунта, с согласия).

Код ITЗагрузка примера кода…

Этот скрипт:

  • работает в фоне;
  • не хранит пароль — только cookies;
  • логирует состояние;
  • устойчив к мелким изменениям интерфейса (ожидания).

Headless-режим​

Headless-режим — это запуск браузера без графического интерфейса. Он работает "внутри себя", рисуя страницы в памяти, но не выводя их на экран.

Когда включать headless:

  • в CI и nightly-прогонах, где важны скорость и масштаб;
  • в локальной отладке flaky-сценариев сначала лучше headed-режим, потом повтор в headless;
  • для релизного smoke полезно прогонять оба режима, чтобы видеть различия поведения.

Как включить (Selenium 4.13+, Chrome 109+)​

from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--headless=new") # современный headless (Chromium)
# options.add_argument("--headless") # старый режим (устарел в 2023)

driver = webdriver.Chrome(options=options, service=...)

Важно:
— --headless=new использует ту же архитектуру рендеринга, что и обычный режим — поведение идентично;
— Старый --headless (без =new) эмулирует устаревшую версию и не поддерживается в новых Chrome.


Преимущества headless-режима​

  • Скорость: на 15–30% быстрее из-за отсутствия отрисовки пикселей;
  • Экономия ресурсов: меньше потребление CPU и RAM (на 100–200 МБ на сессию);
  • Совместимость с серверами — работает на Linux без GUI (например, в Docker, VPS, CI);
  • Параллелизм — можно запустить 10, 20, 50 сессий на одном сервере.

Ограничения и подводные камни​

  1. Детектирование сайтом
    Многие ресурсы (особенно с защитой от ботов) проверяют:

    • navigator.webdriver === true;
    • отсутствие window.outerWidth, window.outerHeight;
    • нестандартные значения navigator.plugins, navigator.languages.

    Результат — блокировка, CAPTCHA, пустая страница.

  2. Разное поведение JavaScript
    Некоторые фреймворки (редко) используют requestAnimationFrame или screen.width для адаптивности — в headless эти значения могут отличаться.

  3. Отладка сложнее
    Нельзя "посмотреть глазами". Приходится делать скриншоты:

driver.save_screenshot("debug.png")

Headless в Linux/Arch (без GUI)​

На сервере без графической оболочки нужно:

  1. Установить Chrome и драйвер:
# Arch
yay -S google-chrome chromedriver
# Ubuntu/Debian
sudo apt install -y google-chrome-stable chromium-chromedriver
  1. Убедиться, что установлен xvfb (X Virtual Framebuffer) — не обязательно при --headless=new;
  2. Или использовать официальный Docker-образ (см. ниже).

Современный headless (--headless=new) не требует X11, так как рендеринг идёт через --disable-gpu и --no-sandbox (но sandbox лучше не отключать без необходимости).


Маскировка Selenium​

⚠️ Важно — эти методы не для обхода защиты, а для того, чтобы тесты не падали из-за ложного срабатывания систем обнаружения. Использование в целях обхода платных сервисов, сбора персональных данных или нарушения ToS — недопустимо.


Устранение navigator.webdriver​

Самый простой признак:

driver.get("https://example.com")
print(driver.execute_script("return navigator.webdriver")) # → True

Исправление:

options.add_argument("--disable-blink-features=AutomationControlled")
driver = webdriver.Chrome(options=options, ...)

# После запуска — удалить свойство
driver.execute_script("delete navigator.__proto__.webdriver")

Теперь navigator.webdriver возвращает undefined.


Подмена User-Agent​

User-Agent — строка, идентифицирующая браузер, ОС, устройство.

По умолчанию Selenium использует:

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.6778.85 Safari/537.36

Видно: HeadlessChrome → сигнал тревоги.

Подмена:

ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
options.add_argument(f"--user-agent={ua}")

Лучше брать актуальный UA с whatmyuseragent.com или использовать библиотеку fake-useragent.


Дополнительные аргументы​

options.add_argument("--disable-infobars")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--no-sandbox")
options.add_argument("--disable-gpu") # для некоторых серверов
options.add_experimental_option("excludeSwitches", ["enable-Автоматизация"])
options.add_experimental_option("useAutomationExtension", False)

Эти настройки:

  • убирают панель "Chrome управляется автоматизированным ПО";
  • снижают потребление /dev/shm;
  • отключают sandbox (только в доверенной среде!);
  • удаляют следы расширения ChromeDriver.

Важно: --no-sandbox снижает безопасность. Используйте только в изолированных средах (Docker, CI).


Прокси​

Прокси-сервер выступает посредником между браузером и интернетом. В Selenium его можно настроить:

options.add_argument("--proxy-server=http://192.168.1.10:8080")
# или
options.add_argument("--proxy-server=socks5://user:pass@proxy.example.com:1080")

Прокси в тестах - перехват HTTP-трафика​

  • Обход геоограничений (например, доступ к региональному контенту);
  • Анонимизация (скрытие реального IP);
  • Логирование и отладка трафика (через Charles Proxy, Fiddler);
  • Тестирование в условиях слабого интернета (прокси с искусственной задержкой).

Ограничения и риски​

  • Безопасность: трафик через HTTP-прокси передаётся в открытом виде;
  • Совместимость: не все сайты работают через прокси (особенно с WebSockets);
  • Юридические последствия:
    — В РФ использование прокси для обхода блокировок или платных сервисов может нарушать ст. 273 УК РФ (создание/использование вредоносных программ) и условия хостинга;
    — Использование публичных бесплатных прокси — высокий риск утечки cookies и сессий.

Рекомендация:
— Для тестов — используйте локальный прокси (например, mitmproxy);
— Никогда не передавайте учётные данные через публичные прокси;
— Всегда проверяйте, разрешено ли использование прокси в ToS сайта.


Расширения​

Расширения (.crx, .zip) можно загрузить при запуске:

options.add_extension("/path/to/adblock.crx")
# или распакованное расширение —
options.add_argument("--load-extension=/path/to/unpacked-extension")

Применение​

  • Отключение рекламы — ускоряет загрузку, упрощает парсинг;
  • Тестовые расширения — например, "React Developer Tools" для отладки SPA;
  • Кастомные инструменты — например, расширение для инъекции тестовых данных.

Ограничения​

  • Расширения увеличивают время запуска (на 1–3 сек);
  • Могут конфликтовать с тестируемым приложением;
  • В headless-режиме не все расширения работают (особенно те, что требуют GUI);
  • Для Chrome в CI — лучше использовать --disable-extensions по умолчанию, включая только нужные.

Запуск в Docker​

Docker — стандарт для запуска Selenium в CI/CD и распределённых системах.


Официальный образ Selenium​

Selenium предоставляет готовые образы:

docker run -d -p 4444:4444 --shm-size="2g" selenium/standalone-chrome:latest

После этого можно подключаться удалённо:

driver = webdriver.Remote(
command_executor="http://localhost:4444",
options=Options()
)

Преимущества:

  • одинаковое окружение на всех машинах;
  • параллельный запуск через Selenium Grid;
  • автоматическое управление памятью (--shm-size критичен для Chrome).

Собственный Dockerfile​

Для тонкой настройки:

Код ITЗагрузка примера кода…

Запуск:

docker build -t selenium-job .
docker run --shm-size=2g selenium-job

Важно:
— --shm-size=2g предотвращает падение Chrome из-за нехватки разделяемой памяти;
— В CI (GitHub Actions, GitLab CI) это стандартная практика.


Производительность​

Один экземпляр Chrome в headless-режиме потребляет:

РесурсМинимумТипичноС учётом тяжёлого SPA
RAM200 МБ350 МБ600+ МБ
CPU0.1 ядро0.3 ядра0.8+ ядра (при активности)

На сервере с 8 ГБ RAM можно запустить ~20 параллельных сессий (с запасом под ОС и драйверы).

Оптимизации:

  • использовать --headless=new;
  • закрывать драйвер через with или quit();
  • не хранить WebElement дольше необходимого;
  • избегать page_source в циклах — это сериализация всего DOM.

Навигация по разделу "Тестирование"​


Содержание