Формы: ввод, клики, выпадающие списки
Форма логина, форма оформления заказа, форма фильтров — там, где пользователь что-то вводит и выбирает, автотесты падают чаще всего. Разберём, как заполнять формы так, чтобы тест был честным и стабильным.
Взаимодействие — это команда WebDriver, которая просит браузер сделать с элементом то же, что сделал бы живой человек: навести курсор, нажать кнопку мыши, отправить нажатия клавиш. Именно «как человек» — а не «как JavaScript».
Зачем это нужно на практике
Найти элемент — половина дела. Настоящая ценность автотеста в том, что он воспроизводит сценарий пользователя: ввёл почту, ввёл пароль, поставил галочку «согласен», выбрал город из списка, нажал «Войти». Каждый из этих шагов в Selenium выглядит как одна строка кода — и каждый умеет ломаться по-своему. Поле не очистилось, кнопку перекрыл баннер, выпадающий список оказался не тегом select, а самодельным виджетом на div. Разберём это по порядку, чтобы не собирать грабли в проде.
Ввод текста: send_keys и clear
Базовый сценарий выглядит так:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example.com/login")
wait = WebDriverWait(driver, 10)
email = wait.until(EC.element_to_be_clickable((By.ID, "email")))
email.clear()
email.send_keys("qa@codechick.io")
password = driver.find_element(By.NAME, "password")
password.send_keys("S3cret-pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
wait.until(EC.url_contains("/dashboard"))
print(driver.current_url)
driver.quit()
Что здесь важно построчно:
element_to_be_clickable, а неpresence_of_element_located: поле может уже быть в DOM, но быть перекрыто прелоадером или отключено атрибутомdisabled. Ждём именно готовности к работе.clear()обязателен, если в поле уже что-то есть:send_keysне заменяет содержимое, а дописывает в конец. Классический баг: тест ввёл «qa@codechick.io» в поле, где браузер автоподставил старую почту, и на сервер ушла склейка из двух адресов.send_keysпринимает не только текст, но и спецклавиши изKeys:Keys.ENTER,Keys.TAB,Keys.ESCAPE. Отправить форму часто проще черезsend_keys(Keys.ENTER)в последнем поле, чем искать кнопку.
Отдельная боль — поля React/Vue с масками (телефон, карта, дата). Там clear() иногда «очищает» значение в DOM, но фреймворк об этом не узнаёт и при первом же ререндере возвращает старый текст. Надёжнее чистить поле так, как это сделал бы человек:
from selenium.webdriver.common.keys import Keys
phone = driver.find_element(By.ID, "phone")
phone.click()
phone.send_keys(Keys.CONTROL, "a") # на macOS — Keys.COMMAND
phone.send_keys(Keys.DELETE)
phone.send_keys("9991234567")
# проверяем то, что реально в поле, а не то, что мы "как бы" ввели
assert phone.get_attribute("value") == "+7 (999) 123-45-67"
Бонус: загрузка файла — это тоже send_keys. Не пытайтесь кликать по кнопке «Выбрать файл» — откроется системное окно, которым WebDriver не управляет. Вместо этого отправьте абсолютный путь прямо в input:
import os
upload = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
upload.send_keys(os.path.abspath("fixtures/avatar.png"))
Клик и его подводные камни
Метод click() — самый простой и самый капризный. Типичное падение:
selenium.common.exceptions.ElementClickInterceptedException:
Message: element click intercepted: Element <button id="submit">...</button>
is not clickable at point (640, 812). Other element would receive the click:
<div class="cookie-banner">...</div>
Драйвер честно сообщает: я прицелился в точку (640, 812), но клик получил бы другой элемент — плашка про куки. Виноваты обычно три вещи: баннер согласия, «липкая» шапка (position: sticky) и модалка, которая ещё доигрывает анимацию исчезновения. Правильная реакция — устранить причину, а не обойти симптом: закрыть баннер в фикстуре, дождаться закрытия модалки, доскроллить элемент в центр окна.
from selenium.common.exceptions import ElementClickInterceptedException
def safe_click(driver, element):
try:
element.click()
except ElementClickInterceptedException:
# доводим элемент в центр вьюпорта: из-под sticky-шапки и футера
driver.execute_script(
"arguments[0].scrollIntoView({block: 'center', inline: 'center'});",
element,
)
element.click() # вторая попытка — уже настоящим кликом мыши
А вот driver.execute_script("arguments[0].click();", element) — это не «надёжный клик», это выключение проверки. JS-клик проходит сквозь невидимые, отключённые и перекрытые элементы: тест позеленеет там, где живой пользователь не смог бы нажать кнопку. Держите его как крайнюю меру и обязательно оставляйте рядом комментарий, почему нельзя иначе.
Выпадающие списки: класс Select
Если в разметке настоящий тег select, в Selenium для него есть отдельный помощник:
from selenium.webdriver.support.select import Select
city = Select(driver.find_element(By.ID, "city"))
city.select_by_visible_text("Казань") # по видимому тексту — самый читаемый способ
city.select_by_value("kzn") # по атрибуту value — самый устойчивый
city.select_by_index(2) # по номеру — самый хрупкий
print(city.first_selected_option.text)
print([o.text for o in city.options])
# мультиселект
tags = Select(driver.find_element(By.ID, "tags"))
print(tags.is_multiple) # True
tags.select_by_value("qa")
tags.select_by_value("python")
tags.deselect_by_value("qa")
print([o.text for o in tags.all_selected_options])
Правило выбора метода: select_by_value — когда value осмысленный и стабильный; select_by_visible_text — когда тест должен читаться как ТЗ («выбрать Казань»); select_by_index — почти никогда, потому что порядок опций меняется от сортировки и локали.
Главная ловушка: Select работает только с тегом select. На дизайнерском дропдауне из div (react-select, кастомный автокомплит) он бросит UnexpectedTagNameException. Такой список — обычная двухшаговая история «кликнуть, дождаться меню, кликнуть по пункту»:
driver.find_element(By.CSS_SELECTOR, ".select__control").click()
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".select__menu")))
driver.find_element(
By.XPATH, "//div[contains(@class,'select__option')][normalize-space()='Казань']"
).click()
assert driver.find_element(By.CSS_SELECTOR, ".select__value").text == "Казань"
Чекбоксы и радиокнопки
Ключевой метод — is_selected(). Никогда не кликайте по чекбоксу вслепую: если он уже отмечен (например, браузер восстановил состояние формы), клик его снимет, и тест проверит ровно обратное тому, что задумано.
def set_checkbox(element, value: bool) -> None:
"""Приводит чекбокс к нужному состоянию, а не переключает вслепую."""
if element.is_selected() != value:
element.click()
agree = driver.find_element(By.ID, "agree")
set_checkbox(agree, True)
assert agree.is_selected()
# радиокнопки: снять выбор нельзя, только перевести на другую опцию
pro = driver.find_element(By.CSS_SELECTOR, "input[name='plan'][value='pro']")
pro.click()
assert pro.is_selected()
Ещё одна частая беда: дизайнеры прячут настоящий input (нулевой размер или opacity: 0) и рисуют галочку через label. Тогда input.click() упадёт с ElementNotInteractableException — кликать надо по label, а состояние читать у скрытого input:
checkbox = driver.find_element(By.ID, "agree") # настоящий input, невидимый
label = driver.find_element(By.CSS_SELECTOR, "label[for='agree']") # то, что видит человек
if not checkbox.is_selected():
label.click()
assert checkbox.is_selected()
Как это работает
Понимание механики снимает половину вопросов. По спецификации W3C WebDriver команда Element Click выполняется не «мысленно», а по шагам: браузер скроллит элемент во вьюпорт, вычисляет его in-view center point (центр видимой части) и делает hit-test — по сути спрашивает у DOM, какой элемент лежит в этой точке. Если сверху оказался кто-то другой (баннер, оверлей, шапка), команда обязана вернуть ошибку element click intercepted. Это не каприз Selenium, это защита: реальный пользователь в эту точку тоже не попал бы.
Команда Element Send Keys точно так же не присваивает value напрямую, а генерирует настоящие события клавиатуры (keydown, keypress, input, keyup). Поэтому маски, валидация на лету и React-состояние работают ровно так, как у человека, — и поэтому же send_keys дописывает текст, а не заменяет его: клавиатура ведь не умеет «заменять».
А Select — не команда протокола, а обычная обёртка на Python: внутри она сама ищет теги option, сравнивает их текст и value и кликает по нужному. Никакой магии — просто аккуратно написанный за вас код.
Частые ошибки
- Забыли
clear()— в поле склейка старого и нового значения, а тест «не понимает», почему бэкенд ругается на невалидную почту. - Лечат
ElementClickInterceptedExceptionJS-кликом. Симптом исчез, баг (кнопка реально недоступна пользователю) — остался, и тест теперь его не поймает. - Заворачивают клик в
time.sleep(3). На быстром стенде это потерянные секунды на каждом тесте, на медленном — всё равно падение. Ждать нужно условие, а не время. Selectна кастомном дропдауне —UnexpectedTagNameException. Сначала посмотрите в DevTools, что там за тег на самом деле.- Клик по чекбоксу без
is_selected()— тест-«качели»: при повторном прогоне на той же сессии галочка снимается. - Проверка «ввелось ли» по своей же переменной. Ассерт должен читать
get_attribute("value")из браузера — только это доказывает, что поле приняло текст.
Итоги
send_keysдописывает текст — перед вводом чистите полеclear(), а для капризных масок используйте Ctrl+A и Delete.- Загрузка файла — это
send_keysс абсолютным путём вinput[type='file'], а не клик по кнопке. ElementClickInterceptedException— сообщение о реальной проблеме UI: чините причину (баннер, оверлей, sticky-шапка), JS-клик — крайняя мера.Select— только для тегаselect; предпочитайтеselect_by_valueиselect_by_visible_text, забудьте про индексы.- Чекбоксы и радио: сначала
is_selected(), потом решение кликать. Скрытыйinputкликается через егоlabel.