Назад в Центр знаний
Начало работы

Selenium без окна: ChromeOptions и различия

Chrome без окна в Selenium отличается от сеанса с окном в полях, которые читает страница. Нужные ChromeOptions, размер окна по умолчанию и почему режим легче выделить.

BotBrowser Team

Документация

Нужна структурированная документация по теме Начало работы?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

Запуск Selenium без окна это один аргумент в ChromeOptions и другой браузер под ним, и почти вся путаница живёт в зазоре между этими двумя фактами. BotBrowser предоставляет объявленный профиль, который сохраняет согласованность читаемых страницей поверхностей при каждом запуске, и не меняет того, что Selenium попросил у Chrome; переданные опции задают режим, а профиль определяет, что этот режим сообщает. Один локальный запуск трёх сеансов показывает, какие поля меняются вместе с режимом, а какие нет.

Кратко

  • --headless=new это то, что показывает текущая документация Selenium для Chrome, и он запускает полный браузер без окна.
  • Более старая реализация это отдельный бинарник chrome-headless-shell, а не другое значение флага.
  • ChromeOptions это место, где живёт режим: одна и та же строка --headless=new доходит до каждой привязки через её собственный метод аргументов, как показано ниже.
  • Режимы без окна и с окном совпали по вьюпорту, плотности пикселей и числу логических ядер в записанном запуске.
  • Они различались токеном агента пользователя, числом плагинов в shell и объектом chrome в shell.
  • Сеанс без окна легче выделить, чем сеанс с окном, когда эти поля противоречат друг другу.
  • Записывайте бинарник, токен и число плагинов при каждом запуске; один флаг не говорит, какой браузер работал.

Содержание

Две панели. Первая показывает вызов ChromeOptions в Selenium, включающий режим без окна, и три аргумента, которые меняют поведение. Вторая перечисляет поля, которые читает страница в сеансах без окна и с окном, и отмечает совпавшие в записанном запуске и различавшиеся.

Где задаётся режим

Selenium никогда не выбирает режим без окна сам; он передаёт аргументы Chrome. Текущая документация Selenium показывает --headless=new в примере для Chrome, и та же строка встречается в каждой привязке языка, потому что аргумент передаётся без изменений.

Java        options.addArguments("--headless=new");
Python      options.add_argument("--headless=new")
C#          options.AddArgument("--headless=new");
JS          options.addArguments('--headless=new')

Более старая реализация это не другое значение того же флага. Это отдельный исполняемый файл chrome-headless-shell, который Chrome for Testing публикует отдельно, и Selenium попадает в него, указывая путь к бинарнику браузера, а не меняя аргумент. Это различие объясняет целый класс отчётов, где две команды передают одинаковые опции и наблюдают разные браузеры.

Полное сравнение двух реализаций, включая то, что каждая сообщает для плагинов и объекта chrome, есть в статье Chrome headless: новый режим против старого; часть, специфичная для Selenium, касается только выбора бинарника.

same options + chrome binary            -> unified headless
same options + chrome-headless-shell    -> old implementation

Selenium 4 требует, чтобы мажорная версия ChromeDriver совпадала с мажорной версией Chrome, и документация говорит об этом прямо. Несовпадение падает при создании сеанса, и это хороший отказ; плохой случай это совпадающий драйвер с несовпадающим бинарником, потому что тогда сеанс стартует и сообщает то, что сообщает этот бинарник.

Откуда берутся этот бинарник и его профиль, решается отдельно и со своим списком проверок, а интеграция профилей браузера в Selenium разбирает вопросы владения сеансом, которых флаг без окна не касается.

ChromeOptions, которые меняют поведение

Три опции помимо режима меняют то, что видит страница, и каждая предотвращает свой отказ.

--headless=new                 full browser, no window
--window-size=1920,1080        without it, the window is 800x600 or the driver default
--disable-gpu                  older advice; check whether the target needs GPU paths

Размер окна чаще всего объясняет визуальное различие между локальным запуском и CI. Сеанс без окна без явного размера стартует с небольшим значением по умолчанию, и страница, которая верстается иначе ниже точки перелома, отрисует другой документ, а не тот же документ в меньшем кадре. Явно заданный размер делает сравнение осмысленным.

--disable-gpu это унаследованный совет из эпохи, когда режим без окна по умолчанию использовал программный растеризатор. На машине с рабочим GPU он может изменить путь отрисовки, поэтому копирование его в новый набор тестов без проверки вносит различие вместо того, чтобы убрать его.

option                     what it changes                     check before adopting
--headless=new             window visibility, not the engine   yes, it is the mode
--window-size              layout breakpoints                  set it to the product viewport
--disable-gpu              rendering path                      only if the host has no GPU

Ещё две опции встречаются почти в каждом скопированном фрагменте, и ни одна не нужна для режима без окна. --no-sandbox отключает границу безопасности и относится к контейнеру, а не к режиму; --disable-dev-shm-usage обходит маленький /dev/shm в некоторых контейнерах. Обе меняют окружение, в котором работает страница, и обе принадлежат конфигурации хоста, а не файлу теста, который запускается и локально.

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

options.addArguments("--user-data-dir=/tmp/profile-42")   -> state survives the session
no --user-data-dir                                        -> temporary profile, discarded

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

Один и тот же тест локально и в CI

Самая частая проблема Selenium без окна вовсе не в режиме; это различие между машиной, где написан тест, и машиной, где он выполняется. Три свойства хоста меняют документ, который отрисовывает страница, и ни одно из них не задаётся через ChromeOptions.

property        local value              CI value                 effect
fonts           full system set          minimal or none          glyphs render as boxes
GPU             hardware path            software rasteriser      subtle anti-aliasing change
display         real screen              none, or a virtual one   window metrics differ

Шрифты это первое, что стоит проверить, когда запуск без окна даёт изображение, которое почти правильное. Контейнер без шрифтов CJK отрисовывает такие символы как прямоугольники notdef, и отказ идентичен в режиме с окном на том же контейнере, что и есть подсказка: если с окном падает так же, режим не является переменной.

Путь GPU второй. Сеанс без окна на машине без рабочего GPU доходит до программного растеризатора, и итоговый вывод может отличаться от сеанса с окном на той же машине на несколько субпиксельных значений. Допуск сравнения, подобранный на ноутбуке разработчика, поэтому сообщит о различиях, которые идут от хоста, а не от проверяемого кода.

Дисплей третий, и именно с ним пользователи Selenium сталкиваются первым на сервере. Chrome с окном нужен сервер отображения; сеансу без окна он не нужен. Эта асимметрия и есть вся причина использовать режим без окна в CI, и она же означает, что набор тестов, который проходит без окна и падает с окном, обычно имеет проблему дисплея, а не отрисовки.

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

host property     how it shows up in a diff
fonts             boxes, wrong glyph shapes, changed text width
GPU               subpixel differences across the whole image
display           window metrics, outerWidth
time zone         a different date or time string in one region

Часовой пояс заслуживает одной фразы в отчёте о запуске, потому что устранить его дёшево: напечатайте Intl.DateTimeFormat().resolvedOptions().timeZone и navigator.language в обоих сеансах, и несовпадение объяснит себя само.

passes headless, fails headed  -> check the display server first
fails both the same way        -> host property (fonts, GPU, libraries)
fails only headless            -> mode or binary

Запись этих трёх свойств в отчёт о запуске делает предыдущую таблицу пригодной после факта. Снимок без списка шрифтов и без описания GPU это фотография неизвестной машины.

Когда различается сам HTML

Сеанс без окна, который возвращает другую разметку, чем сеанс с окном, это другая проблема, чем сеанс с другими пикселями, и диагностика короче. Страница либо выполнила другой JavaScript, либо получила другие значения, и то и другое видно в DOM.

1. dump document.documentElement.outerHTML in both modes
2. diff the two files, ignoring timestamps and generated ids
3. for each difference, check whether a page script branches on a value you record

Третий шаг закрывает большинство случаев. Страница, которая ветвится по navigator.userAgent, выдаст другую разметку, потому что токен отличается, и это ожидаемо, а не дефект. Страница, которая ветвится по navigator.plugins.length, выдаст другую разметку только когда изменился бинарник, и это другой вывод с другим исправлением.

branch on userAgent       -> differs between headless and headed, expected
branch on plugins.length  -> differs only when the binary is the shell
branch on innerWidth      -> differs only when the window option is missing

Это сопоставление превращает сравнение в решение. Оно же объясняет, почему набор тестов, который проходит локально и падает в CI, может упасть на значении, которое никто не записал: ветка существует на странице, а записанные поля говорят, какую ветку она выбрала.

Отказы, которые стоит назвать

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

symptom                                  actual cause
element not visible, headed works        window size below the breakpoint
session creation fails immediately       driver and browser major mismatch
blank screenshot, text renders as boxes  missing fonts in the container
stale element after navigation           the element handle was captured before the load

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

Третий это случай шрифтов, и он даёт снимок, который выглядит сломанным, а не пустым. Четвёртый это проблема ожидания, а не отрисовки, и ожидания Selenium и стабильные тесты описывает условия стабильного ожидания.

check before debugging the page:
  1. window size at or above the product viewport
  2. driver major equals browser major
  3. container font set matches the developer machine
  4. the element handle was obtained after the navigation settled

Проход по этому списку по порядку стоит несколько минут и убирает четыре самые частые объяснения. Отказ, который выживает после всех четырёх, стоит отлаживать на уровне страницы, и тогда записанные поля из предыдущих разделов говорят, какой режим и какой бинарник его дали.

Что страница читает иначе

Один локальный запуск записал одни и те же поля в трёх сеансах на одном хосте, используя стандартные сборки Chromium и одноразовый профиль на сеанс. Значения это то, что напечатал тот запуск, а не утверждение о каждой сборке.

unified headless  HeadlessChrome/131.0.0.0      plugins=5  chrome=object
headless shell    HeadlessChrome/131.0.6778.33  plugins=0  chrome=undefined
headed            Chrome/131.0.0.0              plugins=5  chrome=object

webdriver=true in all three (Selenium sets it)

Токен это поле, которое страница читает первым, и оно наиболее очевидно разделяет режимы: оба варианта без окна сообщают HeadlessChrome, а сеанс с окном сообщает Chrome. Это намеренный, документированный сигнал, а не случайность, и поэтому набор тестов, который проверяет токен, должен решить, что именно он проверяет.

navigator.userAgent    HeadlessChrome in headless, Chrome in headed
navigator.plugins      0 on the shell, 5 on unified and headed
window.chrome          undefined on the shell, object on the others

Число плагинов и объект chrome это два поля, которые отделяют shell от двух других, а не режим без окна от режима с окном. Это важно для того, кто разбирает различие между локальным запуском и CI: если число плагинов изменилось, изменился бинарник, а не режим.

Размеры окна и экрана по умолчанию

Измерения, которые удивили в записанном запуске, это те, что не сдвинулись. Вьюпорт, размер экрана и плотность пикселей были одинаковыми во всех трёх сеансах, и различался только outerWidth, на два пикселя в сеансе с окном из-за рамки окна.

field              unified   shell   headed
innerWidth         1280      1280    1280
innerHeight        720       720     720
screen.width       1280      1280    1280
outerWidth         1280      1280    1282
devicePixelRatio   1         1       1

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

same mode, two runs differ  -> animation, timing or fonts
different modes, same size  -> mode or binary, not the viewport

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

Почему режим без окна легче выделить

Сеанс без окна не подозрителен сам по себе, но его легче выделить, когда его поля противоречат друг другу. Записанный запуск это чистый случай: токен говорил о режиме без окна, а остальной браузер выглядел обычным. Изменённый сеанс может быть обратным, когда токен говорит одно, а список плагинов или объект chrome другое.

coherent headless    -> token says headless, everything else consistent
patched headless     -> token rewritten, plugin list or chrome object not

Поэтому полезное сравнение идёт между полями, а не с одним ожидаемым значением. Страницу, которая читает только navigator.userAgent, может удовлетворить перезапись; страница, которая читает токен, число плагинов и объект chrome вместе, проверяет, согласован ли браузер внутри. Что меняет ядро браузера по сравнению с внедрёнными подменами это тот же аргумент с другой стороны.

Selenium добавляет свой сигнал: navigator.webdriver равен true, потому что его задаёт драйвер, и это верно и в режиме с окном. Поэтому он не является свидетельством режима без окна, и тест, который считает иначе, получит вывод о драйвере, а не о режиме.

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

page-level fields     token, plugins, chrome object, viewport, time zone
window-level chrome   infobar, window frame, tab strip

Снимок страницы, а не окна, убирает вторую строку из сравнения. Собственный API снимков Selenium захватывает вьюпорт, и это правильное поведение по умолчанию именно поэтому, а снимок всего окна с рабочего стола, а не из драйвера, возвращает проблему.

Есть ещё одно поле, которое стоит записывать тому, кто запускает один и тот же набор против нескольких сборок браузера. Токен HeadlessChrome несёт версию, и версия в этом токене это сборка, которая дала сеанс. Набор, который обновляет Chrome между запусками и записывает только режим без окна, выбросил поле, которое объяснило бы изменение отрисовки, и исправление это ещё одна строка в отчёте, а не правка теста.

recorded token:  HeadlessChrome/131.0.0.0    -> build 131, headless
recorded token:  HeadlessChrome/131.0.6778.33 -> shell build, patch differs

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

Два запуска с одинаковым токеном и разным поведением это более трудный случай, и они почти всегда означают различие хоста, а не режима. Токен говорит о сборке; шрифты, путь GPU и часовой пояс говорят о машине; а четыре проверки выше разделяют эти два случая.

Практический итог

Задайте режим через --headless=new, задайте размер окна равным вьюпорту продукта и записывайте путь к бинарнику рядом с токеном агента пользователя. Эти три факта отделяют shell от унифицированного режима без окна и оба от режима с окном, и делают это со стороны страницы, где тест может их проверить, не читая код запуска.

Когда запуск без окна отличается от запуска с окном, проверяйте поля в таком порядке: токен, число плагинов, объект chrome, вьюпорт, плотность пикселей. Первые три это свойства режима и бинарника; последние два принадлежат хосту и опциям. Различие в первой группе это решение о запуске; различие во второй относится к конфигурации.

Утверждение, которого стоит избегать, состоит в том, что режим без окна не оставляет ничего для чтения. Оно неверно, и записанный запуск показывает, где именно режим виден. Верно более узкое и полезное: согласованный сеанс без окна это полный браузер без окна, и поля, которые его описывают, можно читать и записывать как любые другие.

Источники

#Selenium#Chrome Без Окна#ChromeOptions#Chromedriver#Автоматизация

Эта статья входит в руководство: Начало работы с автоматизацией браузера: карта

Выполните ту же проверку в BotBrowser

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