Формы: ввод, клики, выпадающие списки

Форма логина, форма оформления заказа, форма фильтров — там, где пользователь что-то вводит и выбирает, автотесты падают чаще всего. Разберём, как заполнять формы так, чтобы тест был честным и стабильным.

Взаимодействие — это команда 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() — в поле склейка старого и нового значения, а тест «не понимает», почему бэкенд ругается на невалидную почту.
  • Лечат ElementClickInterceptedException JS-кликом. Симптом исчез, баг (кнопка реально недоступна пользователю) — остался, и тест теперь его не поймает.
  • Заворачивают клик в 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.
Проверьте себя
1. Тест падает с ElementClickInterceptedException: «Other element would receive the click: div.cookie-banner». Что это значит и как правильно поступить?
AЭлемент не найден в DOM — нужно поправить локатор кнопки
BКнопку перекрывает баннер куки: закрыть баннер (или доскроллить элемент в центр) и кликнуть настоящим кликом
CБраузер слишком медленный — добавить time.sleep(5) перед кликом
DКнопка отключена атрибутом disabled — включить её через execute_script
2. К какому элементу применим класс Select из selenium.webdriver.support.select?
AК любому выпадающему списку, включая дропдауны на div (react-select и подобные)
BТолько к тегу select — на других тегах он бросит UnexpectedTagNameException
CК любому элементу, у которого есть дочерние option, независимо от тега
DТолько к select с атрибутом multiple