2025, Sep 23 11:31

Selenium + Python: стабильный ввод полей на FlixBus через shadow DOM

Разбираем, почему поля Origin/Destination на flixbus.ca сбрасываются в Selenium, и как закрыть баннер cookies в shadow DOM, выбрать автодополнение и кликнуть.

Автозаполнение форм на современных сайтах бывает коварным: вводишь текст в одно поле, переходишь к следующему — и первое незаметно сбрасывается. Именно так ведет себя flixbus.ca, если попытаться изменить маршрут по умолчанию через Selenium/Python. Пункт отправления без проблем меняется с Toronto на Ottawa, но стоит обновить пункт назначения на Sudbury или тронуть дату — исходное поле возвращается к прежнему значению. Причина вовсе не в ваших вызовах send_keys.

Воспроизведение проблемы

Скрипт ниже открывает flixbus.ca, пытается принять файлы cookie, а затем перезаписывает поля From, To и дату. Как только вы взаимодействуете с Destination или Date, поле Origin откатывается к значению по умолчанию.

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.keys import Keys
import time

flix_url = 'https://www.flixbus.ca/'

from_value = 'Ottawa (VIA Rail)'
to_value = 'Sudbury, ON'
when_value = 'Sat, Sep 20'

browser = webdriver.Chrome()
browser.maximize_window()
browser.get(flix_url)

# попытка принять файлы cookie
action_cookie = browser.find_element(By.XPATH, '//*[@id="uc-center     container"]/div[2]/div/div/div/button[2]')
action_cookie.click()

# задать пункт отправления
from_input = browser.find_element(By.ID, 'searchInput-from')
from_input.clear()
time.sleep(1)
from_input.send_keys(from_value)
time.sleep(1)

# задать пункт назначения
to_input = browser.find_element(By.ID, 'searchInput-to')
to_input.clear()
time.sleep(1)
to_input.send_keys(to_value)
time.sleep(1)

# задать дату
date_input = browser.find_element(By.ID, 'dateInput-from')
date_input.click()
time.sleep(1)
date_input.send_keys(when_value)

# отправить форму поиска
submit_btn = browser.find_element(By.XPATH, '//*[@id="search-mask-    component"]/div/div/div/div/div[5]/div/button')
submit_btn.click()

Что на самом деле происходит

Баннер согласия с файлами cookie на этом сайте отрисован внутри shadow DOM. Кнопка Accept All не входит в обычное DOM‑дерево, поэтому прямой поиск и клик через By.XPATH ненадежны. Этот клик получается нестабильным, и в результате последующий Javascript/AJAX, управляющий виджетами From/Origin и To/Destination, не успевает стабилизироваться. Вы вводите текст в поля, которые позже переотрисовывает страница — и ваши изменения стираются.

Рабочий подход

Решение — работать с баннером через его shadow root, а уже потом трогать форму. Используйте shadowRoot.querySelector, чтобы получить кнопку Accept All, кликните по ней и подождите, пока поля действительно станут кликабельными. После ввода выберите нужный вариант из списка автодополнения, чтобы страница зафиксировала ваш выбор.

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import time

browser = webdriver.Chrome()
browser.maximize_window()
browser.get("https://www.flixbus.ca/")

# принять файлы cookie через shadow DOM
time.sleep(10)
accept_btn = browser.execute_script(
    """return document.querySelector('#usercentrics-root').shadowRoot
    .querySelector("button[data-testid='uc-accept-all-button']")"""
)
accept_btn.click()

# заполнить поле From и подтвердить выбор через автодополнение
from_box = WebDriverWait(browser, 20).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "input#searchInput-from"))
)
from_box.click()
from_box.clear()
from_box.send_keys("Ottawa (VIA Rail)")
WebDriverWait(browser, 20).until(
    EC.element_to_be_clickable((By.XPATH, "//div[@data-e2e='autocomplete-options-from']//span[contains(., 'Ottawa (VIA Rail']"))
).click()

# заполнить поле To и подтвердить выбор через автодополнение
to_box = WebDriverWait(browser, 20).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "input#searchInput-to"))
)
to_box.click()
to_box.clear()
to_box.send_keys("Sudbury, ON")
WebDriverWait(browser, 20).until(
    EC.element_to_be_clickable((By.XPATH, "//div[@data-e2e='autocomplete-options-to']//span[contains(., 'Sudbury, ON')]"))
).click()

Почему это важно

Слои согласия с cookies все чаще прячутся в shadow DOM. Если это игнорировать и «продавить» клик через обычный DOM, тест начнет вести себя непредсказуемо. В задачах извлечения данных и E2E‑сценариях такая нестабильность оборачивается фантомными регрессиями, «гейзенбагами» и часами, потраченными на охоту за недетерминированным состоянием. Корректная обработка баннера согласия возвращает стабильность и не позволяет полям формы сбрасываться из‑за поздней отрисовки.

Выводы

Прежде чем трогать ключевые поля, корректно закрывайте оверлеи, построенные на shadow DOM, через shadowRoot.querySelector. Дополните это явными ожиданиями кликабельности и выбором конкретного пункта из подсказок автодополнения. На страницах вроде flixbus.ca такая последовательность не дает полям Origin и Destination откатываться и делает автоматизацию в Selenium надежной.

Статья основана на вопросе со StackOverflow от brooklin7 и ответе от undetected Selenium.