Python: Selenium

Теория: Настройка браузера

Когда тестов становится много, важно, чтобы браузер вёл себя одинаково на любой машине — в докере, на CI и локально. Если этого не сделать, начинаются случайные падения: у кого-то кнопка уехала в «бургер», у кого-то открылось окно разрешений, а у кого-то не скачался файл. Дальше — самые нужные настройки, которые делают браузер предсказуемым.

Headless-режим

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

## Chrome: headless + безопасные флаги для CI
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

opts = Options()
opts.add_argument("--headless=new")          # запуск без интерфейса, но с полным рендером
opts.add_argument("--no-sandbox")            # нужно в Docker
opts.add_argument("--disable-dev-shm-usage") # уменьшает зависимость от памяти /dev/shm
opts.add_argument("--window-size=1366,768")  # фиксируем размер окна, чтобы вёрстка не прыгала
driver = webdriver.Chrome(options=opts)

driver.get("https://example.com")
print(driver.title)
driver.quit()

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

Размер окна

Адаптивные сайты подстраивают верстку под ширину экрана. Меняются не только стили, но и сама структура DOM: элементы могут скрываться, переноситься, объединяться в «бургер»-меню. Для автотеста это превращается в проблему — один и тот же локатор в мобильной версии просто не существует. Поэтому размер окна нужно зафиксировать заранее.

Когда браузер запускается без указания размеров, он может открыться в случайной ширине — например, 900 px в контейнере CI. В таком виде сайт может сразу перейти на мобильную версию, скрыв часть кнопок. Команда set_window_size() задаёт точную геометрию окна и гарантирует одинаковый DOM на всех машинах.

## Пример: фиксированный размер окна для десктопной версии
from selenium import webdriver

driver = webdriver.Chrome()
driver.set_window_size(1366, 768)  # ширина и высота в пикселях
driver.get("https://example.com")
## при таком размере страница отображается как на стандартном ноутбуке
driver.quit()

Если тест нужно прогнать на разных разрешениях, делают несколько запусков с разными размерами. Например, один раз проверяют поведение на десктопе (1366×768), второй — на мобильной ширине (390×844). Это позволяет убедиться, что верстка не ломается, а функциональность сохраняется.

## Тест одного и того же сценария в двух режимах
from selenium import webdriver

sizes = [(1366, 768), (390, 844)]  # десктоп и мобильный

for width, height in sizes:
    driver = webdriver.Chrome()
    driver.set_window_size(width, height)
    driver.get("https://example.com")
    print(f"Тест при {width}x{height}: заголовок —", driver.title)
    driver.quit()

При уменьшении окна до мобильных размеров можно проверить работу адаптивных элементов: скрываются ли блоки, появляется ли «бургер»-меню, корректно ли работает форма входа на маленьком экране.

Фиксированный размер важен и в headless-режиме. В нём браузер не имеет настоящего окна, поэтому по умолчанию выбирает минимальную ширину — около 800 px. Из-за этого на CI тесты видят не ту страницу, что локально. Решение простое: добавить аргумент --window-size=1366,768 при запуске.

## Headless + фиксированное окно
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--window-size=1366,768")

driver = webdriver.Chrome(options=opts)
driver.get("https://example.com")
assert driver.execute_script("return window.innerWidth") == 1366
driver.quit()

Если тест проверяет мобильную вёрстку, ширину можно уменьшить, а user-agent — подменить, чтобы сайт отдал мобильный шаблон. Это даёт полный контроль над интерфейсом и устраняет случайности, связанные с разными экранами и окружением.

Загрузка куков перед входом

Процесс авторизации через форму — один из самых нестабильных участков в UI-тестах. На проде могут появляться капчи, всплывающие подсказки, анимации, проверки двухфакторной аутентификации. Всё это делает тест длинным и хрупким: любая задержка — и он падает. Чтобы избавиться от этого, можно заранее сохранить cookie авторизованного пользователя и восстанавливать сессию напрямую.

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

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

## Пример восстановления сессии по cookie-файлу
import json
from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://example.com/")  # важно: быть на правильном домене

## Загружаем cookie, сохранённые вручную после входа
with open("cookies.json", "r", encoding="utf-8") as f:
    cookies = json.load(f)

driver.delete_all_cookies()  # очищаем старые данные
for c in cookies:
    driver.add_cookie(c)      # добавляем по одному cookie

driver.refresh()              # страница обновится и применит сессию
driver.get("https://example.com/account")  # сразу переходим в личный кабинет
assert "/account" in driver.current_url
driver.quit()

Файл cookies.json можно получить один раз вручную, после обычного входа в систему. В Python это удобно сделать с помощью простого скрипта: открыть страницу, войти, вызвать driver.get_cookies() и сохранить результат в файл.

## Однократное сохранение куков после успешного логина
import json
from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://example.com/login")

## после ручного ввода логина и пароля
input("После входа нажмите Enter, чтобы сохранить cookie...")
with open("cookies.json", "w", encoding="utf-8") as f:
    json.dump(driver.get_cookies(), f, indent=2)
driver.quit()

Теперь авторизация больше не нужна — при каждом запуске теста куки будут подставляться автоматически.

Такой подход решает несколько проблем:

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

Есть важные нюансы. Если домен или поддомен изменился, cookie не добавятся — браузер их проигнорирует. Также срок действия cookie может истечь. В этом случае тест нужно один раз перезапустить вручную, чтобы обновить файл cookies.json.

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

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

Иногда два одинаковых теста ведут себя по-разному. На одной машине браузер открывает всплывающее окно с запросом уведомлений, на другой — страница внезапно на английском, а на третьей — кнопка смещена из-за другого масштаба интерфейса. Причина проста: настройки браузера по умолчанию отличаются. Чтобы зафиксировать поведение, используют аргументы и профили.

Аргументы (--lang, --headless, --disable-notifications) управляют запуском браузера и задают его поведение с нуля. Профиль — это папка, где браузер хранит состояние между сессиями: сохранённые куки, логины, темы, разрешения, историю.

Пример для Chrome

В этом примере создаётся временный пользовательский профиль — отдельная папка, в которой браузер хранит свои данные только во время одного запуска. После завершения теста профиль удаляется. Такой подход обеспечивает полностью чистое окружение: в нём нет старых cookie, кеша, истории, сохранённых логинов или расширений, которые могут повлиять на тест. Каждый прогон начинается «с нуля».

## Chrome: временный профиль и безопасные аргументы
import tempfile, shutil
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

## создаём временный профиль (папку)
profile = tempfile.mkdtemp(prefix="chrome-prof-")

opts = Options()
opts.add_argument(f"--user-data-dir={profile}")   # используем свой каталог профиля
opts.add_argument("--lang=ru-RU")                 # фиксируем язык
opts.add_argument("--disable-notifications")      # убираем системные запросы
opts.add_argument("--incognito")                  # включаем приватный режим
opts.add_argument("--window-size=1366,768")       # задаём одинаковое окно
opts.add_argument("--headless=new")               # headless для CI и Docker

driver = webdriver.Chrome(options=opts)
driver.get("https://example.com")

## проверим, что профиль работает корректно
print(driver.execute_script("return navigator.language"))  # ru-RU
print(driver.get_window_size())                            # {'width': 1366, 'height': 768}

driver.quit()
## очищаем временные файлы профиля
shutil.rmtree(profile, ignore_errors=True)

Пример для Firefox

Для Firefox логика та же — браузер настраивается так, чтобы каждый запуск был независимым. Здесь вместо аргументов применяются настройки профиля (preferences). Они позволяют управлять поведением движка напрямую: отключать уведомления, задавать язык, определять папку для загрузок. Такая конфигурация избавляет тесты от случайных диалогов и сохраняет одинаковое поведение на всех машинах.

from selenium import webdriver
from selenium.webdriver.firefox.options import Options

opts = Options()
opts.add_argument("-headless")  # скрытый запуск, без окна
opts.set_preference("intl.accept_languages", "ru-RU,ru")   # язык интерфейса
opts.set_preference("dom.webnotifications.enabled", False)  # блокируем уведомления
opts.set_preference("browser.download.useDownloadDir", True)
opts.set_preference("browser.download.folderList", 2)       # использовать указанную папку
opts.set_preference("browser.download.dir", "/tmp")          # путь для сохранений
opts.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/pdf")  # без диалога сохранения

driver = webdriver.Firefox(options=opts)
driver.get("https://example.com")
print(driver.execute_script("return navigator.language"))  # ru-RU
driver.quit()

Такой профиль гарантирует стабильное поведение Firefox: никаких всплывающих окон, предсказуемый язык и папка загрузок, одинаковый результат на всех платформах.

Минимальный набор флагов, который стоит добавить в каждый проект

opts.add_argument("--headless=new")         # headless-режим
opts.add_argument("--window-size=1366,768") # стабильная вёрстка
opts.add_argument("--disable-notifications")# отключение запросов браузера
opts.add_argument("--lang=ru-RU")           # единый язык
opts.add_argument("--incognito")            # чистая сессия

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

Управление загрузками

Когда автотест скачивает файл, браузер по умолчанию открывает системное окно с вопросом: «Открыть или сохранить?». Для человека это удобно, но для теста — катастрофа. Selenium не умеет взаимодействовать с такими диалогами, поэтому сценарий зависает. Чтобы скачать файл автоматически, нужно заранее указать папку загрузки и отключить подтверждения.

Chrome

В Chrome управление загрузками выполняется через словарь prefs. В нём задаются настройки браузера, которые Selenium применяет при запуске.

  • Параметр download.default_directory определяет, куда сохранять файлы.
  • download.prompt_for_download отключает всплывающий диалог.
  • plugins.always_open_pdf_externally заставляет Chrome скачивать PDF, а не открывать их во вкладке.
## Chrome: автоматическая загрузка без диалогов
import os
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

## абсолютный путь до папки, где будут лежать скачанные файлы
download_dir = os.path.abspath("downloads")

## настройки Chrome для загрузок
prefs = {
    "download.default_directory": download_dir,   # куда сохранять файлы
    "download.prompt_for_download": False,        # не показывать окно "Сохранить как"
    "plugins.always_open_pdf_externally": True    # скачивать PDF, не открывая их
}

opts = Options()
opts.add_experimental_option("prefs", prefs)
driver = webdriver.Chrome(options=opts)

## открываем страницу с файлами для скачивания
driver.get("https://file-examples.com/index.php/sample-documents-download/")

## при клике файл сразу сохранит��я в downloads/
print(f"Файлы будут сохранены в: {download_dir}")

driver.quit()

Firefox

Firefox настраивается иначе: через set_preference. Там используется список MIME-типов файлов, которые нужно сохранять без вопросов.

## Firefox: загрузка файлов без диалогов
from selenium import webdriver
from selenium.webdriver.firefox.options import Options

opts = Options()
opts.set_preference("browser.download.folderList", 2)       # использовать заданную папку
opts.set_preference("browser.download.dir", "/tmp")          # путь для сохранения
opts.set_preference("browser.download.useDownloadDir", True)
opts.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/pdf,application/zip")
opts.set_preference("pdfjs.disabled", True)                 # отключаем встроенный PDF viewer

driver = webdriver.Firefox(options=opts)
driver.get("https://file-examples.com/index.php/sample-documents-download/")
driver.quit()

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

Если не указать настройки, Chrome или Firefox покажут модальное окно, которое блокирует тест. Selenium не сможет закрыть его или кликнуть по странице, потому что управление передано диалогу. С явными настройками загрузки тест идёт дальше, файл сохраняется в известную директорию, и можно проверить результат.

Например, после скачивания можно убедиться, что файл действительно появился в нужной папке:

import os, time

download_dir = "downloads"
expected_file = os.path.join(download_dir, "sample.pdf")

## ждём до 10 секунд, пока файл появится
for _ in range(10):
    if os.path.exists(expected_file):
        print("Файл скачан успешно!")
        break
    time.sleep(1)
else:
    raise AssertionError("Файл не был найден после загрузки.")

В итоге один и тот же сценарий стабильно работает и локально, и на CI — без случайных зависаний и ручных действий.

Настройки языка и User-Agent

Если сайт показывает разные версии на русском и английском, важно зафиксировать язык. Также можно задать свой User-Agent, чтобы получить нужную версию страницы.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

opts = Options()
opts.add_argument("--lang=ru-RU")
opts.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
driver = webdriver.Chrome(options=opts)
driver.get("https://example.com")
driver.quit()

Мобильная эмуляция

Большинство современных сайтов имеет адаптивную вёрстку — при узком экране меню превращается в «бургер», блоки перестраиваются в колонку, а часть элементов скрывается. Проверять такое поведение вручную на телефоне неудобно, а запускать Selenium на реальном устройстве — слишком сложно. Решение — включить мобильную эмуляцию прямо в браузере, например в Chrome.

Эмуляция создаёт среду, в которой браузер ведёт себя как смартфон: меняет размеры экрана, плотность пикселей и подменяет user-agent. Сайт думает, что его открыл телефон, и отдаёт мобильный шаблон. Это позволяет проверить мобильную вёрстку, не выходя за рамки обычного автотеста.

Пример для Chrome

## Chrome: эмуляция iPhone
from selenium import webdriver

## Параметры мобильного устройства
mobile_emulation = {
    "deviceMetrics": {"width": 390, "height": 844, "pixelRatio": 3.0},  # размеры и плотность пикселей
    "userAgent": (
        "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) "
        "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile Safari/604.1"
    )
}

opts = webdriver.ChromeOptions()
opts.add_experimental_option("mobileEmulation", mobile_emulation)

driver = webdriver.Chrome(options=opts)
driver.get("https://example.com")

print("Ширина окна:", driver.execute_script("return window.innerWidth"))
print("User-Agent:", driver.execute_script("return navigator.userAgent"))

driver.quit()

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

Проверка поведения интерфейса

В таком режиме можно тестировать мобильные элементы — «бургер»-меню, скрытые кнопки, свайпы, формы ввода на маленьких экранах. Например, автотест может проверять, что меню разворачивается по клику:

from selenium.webdriver.common.by import By

driver = webdriver.Chrome(options=opts)
driver.get("https://example.com")

menu_button = driver.find_element(By.CSS_SELECTOR, ".burger")
menu_button.click()

items = driver.find_elements(By.CSS_SELECTOR, ".menu-item")
assert len(items) > 0, "Меню не открылось"

driver.quit()

Такой тест подтвердит, что адаптивная навигация работает корректно и пользователю доступен весь функционал.

Готовые профили устройств

В Chrome можно использовать встроенные шаблоны: "deviceName": "iPhone SE", "Pixel 7", "Galaxy S8+" и другие. Это удобнее, чем задавать размеры вручную:

opts = webdriver.ChromeOptions()
opts.add_experimental_option("mobileEmulation", {"deviceName": "iPhone SE"})
driver = webdriver.Chrome(options=opts)
driver.get("https://example.com")
driver.quit()

Общая фикстура для Pytest

Чтобы не прописывать всё в каждом тесте, настройки выносят в фикстуру.

## conftest.py
import pytest, os, tempfile, shutil
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

@pytest.fixture
def browser():
    profile = tempfile.mkdtemp(prefix="chrome-prof-")
    opts = Options()
    opts.add_argument(f"--user-data-dir={profile}")
    opts.add_argument("--headless=new")
    opts.add_argument("--window-size=1366,768")
    opts.add_argument("--lang=ru-RU")
    opts.add_argument("--disable-notifications")
    prefs = {
        "download.default_directory": os.path.abspath("downloads"),
        "plugins.always_open_pdf_externally": True
    }
    opts.add_experimental_option("prefs", prefs)

    driver = webdriver.Chrome(options=opts)
    yield driver
    driver.quit()
    shutil.rmtree(profile, ignore_errors=True)

Теперь в тестах можно просто писать:

def test_open_home(browser):
    browser.get("https://example.com")
    assert "Example" in browser.title

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

+7 800 100 22 47

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

+7 495 085 21 62

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

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