Python: Selenium

Теория: Продвинутые пользовательские действия

Класс ActionChains: hover, double-click, drag and drop

В браузере не все действия сводятся к простому click(). Бывают всплывающие меню по наведению, двойной клик, перетаскивание карточек. Для таких случаев в Selenium есть ActionChains. Этот класс строит цепочку низкоуровневых событий мыши и клавиатуры и проигрывает их единым жестом. Создание начинается с chain = ActionChains(driver). Наведение курсора делается через move_to_element(elem), после чего .perform() отправляет реальные события в браузер. Пример раскрытия меню по hover:

from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By

menu = driver.find_element(By.CSS_SELECTOR, ".menu")
item = driver.find_element(By.CSS_SELECTOR, ".menu .item-more")

ActionChains(driver).move_to_element(menu).perform()
item.click()

Двойной клик использует double_click(element). Браузер отличает его от двух обычных кликов таймингом, поэтому важно вызывать именно метод двойного клика. Пример редактирования по дабл-клику:

row = driver.find_element(By.CSS_SELECTOR, "tr[data-id='42']")
ActionChains(driver).double_click(row).perform()
editor = driver.find_element(By.CSS_SELECTOR, "input.editor")
editor.send_keys("Новое значение")

Перетаскивание совмещает захват, перемещение и отпускание. В простом варианте достаточно drag_and_drop(source, target), но в интерфейсах с точной геометрией надёжнее управлять оффсетами и шагами. Базовый перенос карточки в другую колонку выглядит так:

card = driver.find_element(By.CSS_SELECTOR, ".card")
column = driver.find_element(By.CSS_SELECTOR, ".column.done")

ActionChains(driver).drag_and_drop(card, column).perform()

Когда целевой контейнер имеет «горячую» точку не по центру, помогает смещение. Вначале выполняется захват click_and_hold, затем курсор сдвигается и отпускается. Такой жест стабильнее на канбан-досках:

src = driver.find_element(By.CSS_SELECTOR, ".task[data-id='7']")
dst = driver.find_element(By.CSS_SELECTOR, ".column.done")

chain = ActionChains(driver)
chain.click_and_hold(src).move_to_element_with_offset(dst, 10, 10).release().perform()

Если элемент вне вьюпорта, браузер может игнорировать события. Перед жестом стоит прокрутить его в область видимости вызовом element.location_once_scrolled_into_view или driver.execute_script("arguments[0].scrollIntoView({block:'center'})", elem). Это снимает проблемы «элемент вне экрана» и «клик перехвачен шапкой».

Клавиатурные взаимодействия

Клавиатура управляется через send_keys и константы Keys. Прямой ввод в поле:

from selenium.webdriver.common.keys import Keys
inp = driver.find_element(By.CSS_SELECTOR, "input[name='q']")
inp.send_keys("selenium", Keys.ENTER)

Модификаторы удерживаются явным нажатием и отпусканием. В ActionChains это последовательность key_down → действия → key_up. Создание горячих клавиш без фокуса поля удобно выполнять на уровне документа:

body = driver.find_element(By.TAG_NAME, "body")
ActionChains(driver).key_down(Keys.CONTROL).send_keys("s").key_up(Keys.CONTROL).perform()

Комбинации вроде Ctrl+A, Ctrl+C в одном фокусе работают надёжно, если фокус заранее помещён в нужное поле. Пример полного цикла «выделить всё → копировать → очистить → вставить»:

area = driver.find_element(By.CSS_SELECTOR, "textarea#note")
area.click()
ActionChains(driver)
    .key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL)
    .key_down(Keys.CONTROL).send_keys("c").key_up(Keys.CONTROL)
    .send_keys(Keys.DELETE)
    .key_down(Keys.CONTROL).send_keys("v").key_up(Keys.CONTROL)
    .perform()

Системные клавиши Enter, Escape и Tab управляют формами и модалками без мыши. Закрытие диалога выполняется одной строкой:

driver.find_element(By.TAG_NAME, "body").send_keys(Keys.ESCAPE)

Переход по таб-порядку помогает в интерфейсах, где кнопка недоступна обычным локатором, но присутствует в последовательности фокуса. Ввод по шагам выглядит наглядно:

login = driver.find_element(By.ID, "username")
login.send_keys("tomsmith", Keys.TAB, "SuperSecretPassword!", Keys.TAB, Keys.ENTER)

Симуляция сложных пользовательских сценариев

Сложный сценарий складывается из нескольких жестов мыши и клавиатуры с паузами. ActionChains поддерживает pause(seconds), что помогает синхронизировать наведение и появление всплывающих подсказок, если в UI заложена анимация. Пример: сначала раскрывается подменю по hover, затем выбирается пункт, потом подтверждение горячей клавишей.

from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

menu = driver.find_element(By.CSS_SELECTOR, ".menu.profile")
sub  = driver.find_element(By.CSS_SELECTOR, ".menu.profile .submenu")
item = driver.find_element(By.CSS_SELECTOR, ".submenu .logout")

chain = ActionChains(driver)
chain.move_to_element(menu).pause(0.3).move_to_element(sub).pause(0.1).click(item).perform()
driver.find_element(By.TAG_NAME, "body").send_keys(Keys.ENTER)

Имитировать «рисование» на канве или слайдер можно послойным перемещением с малыми оффсетами. Это надёжнее, чем один большой скачок, особенно когда скрипты на странице проверяют траекторию.

slider = driver.find_element(By.CSS_SELECTOR, ".range .thumb")
steps = 5
dx = 20  # пикселей за шаг
chain = ActionChains(driver).click_and_hold(slider)
for _ in range(steps):
    chain.move_by_offset(dx, 0).pause(0.05)
chain.release().perform()

Для HTML5 drag-and-drop встречаются приложения, где нативные события Selenium не триггерят пользовательские обработчики. В таких случаях помогает скрипт с созданием DataTransfer и генерацией dragstart/dragover/drop. Этот приём включает бизнес-логику, которая «слушает» именно HTML5-события.

script = """
const src = arguments[0], tgt = arguments[1];
const data = new DataTransfer();
src.dispatchEvent(new DragEvent('dragstart', {dataTransfer: data, bubbles: true}));
tgt.dispatchEvent(new DragEvent('dragover',  {dataTransfer: data, bubbles: true}));
tgt.dispatchEvent(new DragEvent('drop',      {dataTransfer: data, bubbles: true}));
"""
src = driver.find_element(By.CSS_SELECTOR, ".tile[src]")
tgt = driver.find_element(By.CSS_SELECTOR, ".board[target]")
driver.execute_script(script, src, tgt)

Контекстное меню требует «долгого» клика правой кнопкой. Для этого есть context_click(element). Дальше можно выбрать пункт стрелками или кликом, если меню рендерится в DOM.

file = driver.find_element(By.CSS_SELECTOR, ".file[name='report.pdf']")
ActionChains(driver).context_click(file).perform()
driver.find_element(By.CSS_SELECTOR, ".context-menu .delete").click()

Комбинация мыши и клавиатуры помогает тестировать мультивыбор в таблицах. Удержание Ctrl добавляет элементы в выделение, Shift формирует диапазон.

rows = driver.find_elements(By.CSS_SELECTOR, "table tr.selectable")
ActionChains(driver).click(rows[1])
    .key_down(Keys.SHIFT).click(rows[4]).key_up(Keys.SHIFT)
    .key_down(Keys.CONTROL).click(rows[6]).key_up(Keys.CONTROL)
    .perform()

При сценариях с подсказками и ленивыми панелями стоит закладывать явные ожидания между шагами. Паузы в ActionChains пригодны для анимаций, но появление элементов лучше подтверждать WebDriverWait и expected_conditions, чтобы не зависеть от таймингов фронтенда. Пример: наведение, ожидание появления поповера, клик по кнопке внутри.

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

avatar = driver.find_element(By.CSS_SELECTOR, ".user-avatar")
ActionChains(driver).move_to_element(avatar).perform()
popover = WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".popover.open")))
popover.find_element(By.CSS_SELECTOR, "button.follow").click()

Если курсор должен попасть не в центр, а в конкретную точку, используется move_to_element_with_offset(elem, x, y). Это важно для небольших хэндлов, слайдеров и графиков, где активная зона узкая.

handle = driver.find_element(By.CSS_SELECTOR, ".resize .handle-e")
ActionChains(driver).move_to_element_with_offset(handle, 2, 2).click_and_hold().move_by_offset(80, 0).release().perform()

Стабильность сложных жестов повышают три простых шага: прокрутка элемента в центр вьюпорта скриптом scrollIntoView, подтверждение видимости и кликабельности через WebDriverWait, а также минимизация лишних действий в цепочке. Чем короче и точнее цепочка ActionChains, тем предсказуемее результат и тем меньше флаков при рефакторингах фронтенда.

Shadow DOM и «упрямые» элементы

Современные UI-компоненты нередко инкапсулируют верстку внутри shadow root. Обычные локаторы туда не заглядывают, поэтому сначала нужно получить shadowRoot, а потом искать элементы уже внутри него:

from selenium.webdriver.common.by import By

widget = driver.find_element(By.CSS_SELECTOR, "my-widget")
shadow_root = driver.execute_script("return arguments[0].shadowRoot", widget)
button = shadow_root.find_element(By.CSS_SELECTOR, "button.play")
button.click()

Shadow DOM любит динамические подгрузки, поэтому лучше комбинировать этот подход с WebDriverWait и проверками visibility_of_element_located.

Иногда элемент остаётся нечувствительным ни к клику Selenium, ни к execute_script. Причины бывают разные: перекрывающий слой, нестандартный обработчик событий, баг браузера. В крайнем случае можно задействовать OS-level инструменты вроде pyautogui: навести курсор, кликнуть или отправить клавиши напрямую в операционную систему. Такой подход стоит применять только там, где нет другого выхода — он требует реального экрана и делает запуск более хрупким. Сначала нужно исключить ошибки локаторов, добавить явные ожидания и попробовать JavaScript-клик, и лишь потом прибегать к «тяжёлой артиллерии».

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845