From 2067332e44acbfff1493905dcb783f9704c43eed Mon Sep 17 00:00:00 2001 From: kseniataranov Date: Wed, 5 Aug 2026 15:05:00 +0300 Subject: [PATCH 1/4] docs(emulation): add an article on user environment emulation (ru version only) --- .../user-environment-emulation.mdx | 3 + .../user-environment-emulation.mdx | 440 ++++++++++++++++++ 2 files changed, 443 insertions(+) create mode 100644 docs/basic-guides/user-environment-emulation.mdx create mode 100644 i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx diff --git a/docs/basic-guides/user-environment-emulation.mdx b/docs/basic-guides/user-environment-emulation.mdx new file mode 100644 index 00000000..e725cc09 --- /dev/null +++ b/docs/basic-guides/user-environment-emulation.mdx @@ -0,0 +1,3 @@ +# User Environment Emulation + +Draft diff --git a/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx b/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx new file mode 100644 index 00000000..f1ace798 --- /dev/null +++ b/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx @@ -0,0 +1,440 @@ +import Admonition from "@theme/Admonition"; + +# Эмуляция среды пользователя + + + +- Как воспроизвести мобильное устройство, цветовую схему, геолокацию и разрешения +- Как проверить медленную сеть, офлайн-режим и слабый CPU +- Какие команды требуют WebDriver BiDi или CDP +- Как изолировать тесты и восстанавливать изменённое состояние + + + +## Введение + +Пользовательская среда влияет на то, как приложение выглядит и работает. Один и тот же интерфейс может по-разному вести себя на мобильном экране, при другой локали, в тёмной теме, без доступа к геолокации или при медленном соединении. + +В Testplane эти условия настраиваются несколькими способами: через WebDriver BiDi, обычные WebDriver-команды, CDP и capabilities браузера. Выбор механизма зависит от того, какое свойство среды нужно изменить и в каком браузере выполняется тест. + +| Механизм | Команды | +| ------------------------ | ------------------------------------- | +| WebDriver BiDi | `emulate()`, `setViewport()` | +| Chrome DevTools Protocol | `throttleNetwork()`, `throttleCPU()` | +| WebDriver | `setPermissions()`, `setWindowSize()` | + +Все варианты `emulate()` и команда `setViewport()` требуют WebDriver BiDi. Чтобы его включить, добавьте `webSocketUrl: true` в `desiredCapabilities`: + +```typescript +"chrome": { + desiredCapabilities: { + browserName: "chrome", + browserVersion: "128.0", + webSocketUrl: true, + }, +}, +``` + +Минимальная версия Chrome с поддержкой BiDi — 128, Firefox — 119. + + + +`emulate()` применяется при создании нового документа. Вызывайте команду до `browser.url()`. + +После `browser.restore()` перезагрузите страницу или выполните повторную навигацию, если в том же тесте нужно проверить восстановленное состояние. + + + +## Эмуляция мобильных устройств + +Мобильная эмуляция помогает проверить адаптивную вёрстку, мобильную навигацию и отображение интерфейса на экранах с высоким DPR. Основные возможности ориентированы на Chromium-based браузеры. + +### Профиль устройства + +Чтобы одновременно задать viewport, DPR и user agent, используйте `emulate("device")`: + +```typescript +it("отображает мобильную вёрстку", async ({ browser }) => { + const restoreDevice = await browser.emulate("device", "iPhone 12 Pro Max"); + + try { + await browser.url("/"); + // ... + } finally { + await restoreDevice(); + } +}); +``` + +Команда не включает touch-события и мобильный режим браузера. + + + +`browser.restore()` возвращает user agent, но не viewport, установленный через `emulate("device")`. Для полного отката вызывайте функцию, которую вернул `emulate("device")`. + +Viewport при этом вернётся к профилю `Desktop Chrome`, а не к исходному размеру. + + + +### Viewport и DPR + +Чтобы проверить конкретный брейкпойнт, используйте `setViewport()`: + +```typescript +await browser.setViewport({ + width: 390, + height: 844, + devicePixelRatio: 3, +}); +``` + +Команда применяется к текущему контексту и не возвращает функцию отката. Чтобы восстановить viewport, вызовите `setViewport()` повторно с нужными значениями. + +`setWindowSize()` меняет размер всего окна, а не области отрисовки: + +```typescript +await browser.setWindowSize(500, 600); +``` + +Для мобильных брейкпойнтов используйте `setViewport()`. + +### User agent + +Если клиентский код выбирает мобильный интерфейс по `navigator.userAgent`, задайте значение отдельно: + +```typescript +await browser.emulate( + "userAgent", + "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", +); +``` + +Команда не меняет HTTP-заголовок `User-Agent` и Client Hints. Она подходит только для кода, который читает `navigator.userAgent` в браузере. + +## Язык и временная зона + +Эти параметры нужны для проверки переводов, форматов дат, чисел и времени. + +### Локали + +В Chromium можно изменить локаль `Intl` и заголовок `Accept-Language` через Puppeteer и CDP: + +```typescript +it("открывает страницу с немецкой локалью", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + const client = await page.target().createCDPSession(); + + await client.send("Emulation.setLocaleOverride", { + locale: "de-DE", + }); + + await page.setExtraHTTPHeaders({ + "Accept-Language": "de-DE,de;q=0.9", + }); + + await browser.url("/"); + // ... +}); +``` + +`Emulation.setLocaleOverride` меняет локаль `Intl` и форматирование дат и чисел. `page.setExtraHTTPHeaders()` меняет заголовок `Accept-Language`, который получает сервер. + +Эти настройки не меняют `navigator.language` и `navigator.languages`. Способ подходит, если приложение получает локаль с сервера или использует `Intl` без явно заданной локали. Если клиентский код читает `navigator.language`, потребуется другой способ запуска браузера с нужной системной локалью. + +Способ с Puppeteer и CDP предназначен для Chromium-браузеров. + +### Часовой пояс + +В Chromium используйте `page.emulateTimezone()`: + +```typescript +it("показывает время для Нью-Йорка", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + + try { + await page.emulateTimezone("America/New_York"); + await browser.url("/"); + // ... + } finally { + await page.emulateTimezone(); + } +}); +``` + +Команда меняет часовой пояс для `Date` и `Intl`: `resolvedOptions().timeZone`, `getTimezoneOffset()`, `Date.prototype.toString()` и форматирование без явно заданного `timeZone`. + +Вызывайте `emulateTimezone()` до навигации, чтобы код страницы сразу использовал нужную зону. Технически изменение применяется и к уже открытому документу. + +Значение `"UTC"` не сбрасывает настройку, а устанавливает новую зону. Для снятия override вызовите `emulateTimezone()` без аргумента. + +### Системное время + +Для сценариев, зависящих от даты и таймеров, используйте `emulate("clock")`. Используйте эту команду до навигации, чтобы скрипты страницы сразу использовали подменённое время. + +```typescript +it("показывает акцию на заданную дату", async ({ browser }) => { + const clock = await browser.emulate("clock", { + now: new Date(2025, 11, 31), + }); + + try { + await browser.url("/"); + // ... + } finally { + await clock.restore(); + } +}); +``` + +`emulate("clock")` не заменяет настройку часового пояса: команда управляет временем и таймерами, но не меняет `Intl.DateTimeFormat().resolvedOptions().timeZone`. + +### Таймеры + +Чтобы выполнить действие, запланированное через браузерный таймер, вызовите `tick()` и передайте количество миллисекунд: + +```typescript +it("скрывает уведомление через пять секунд", async ({ browser }) => { + const clock = await browser.emulate("clock"); + + try { + await browser.url("/"); + await browser.findByTestId("show-notification").click(); + + await clock.tick(5000); + + await expect(browser.findByTestId("show-notification")).not.toBeDisplayed(); + } finally { + await clock.restore(); + } +}); +``` + +В этом примере пять секунд проходят для таймеров страницы, но тест не ждёт их в реальном времени. + +### Подмена необходимых таймеров + +По умолчанию clock подменяет поддерживаемые браузерные таймеры и `Date`. Через `toFake` можно ограничить список: + +```typescript +const clock = await browser.emulate("clock", { + toFake: ["Date", "setTimeout", "clearTimeout"], +}); +``` + +Используйте этот вариант, когда тесту нужно управлять только отдельными API и не затрагивать остальные таймеры страницы. + +## Разрешения браузера + +`setPermissions()` изменяет состояние разрешения для текущего origin. Сначала откройте целевую страницу, затем вызовите команду: + +```typescript +it("работает при выданном доступе к геолокации", async ({ browser }) => { + await browser.url("/"); + await browser.setPermissions({ name: "geolocation" }, "granted"); + // ... +}); +``` + +До первой навигации браузер находится на `about:blank`. Для непрозрачного origin этой страницы разрешение выдать нельзя. + +В примерах протокола используются состояния `"granted"`, `"denied"` и `"prompt"`. Поддержка разрешений и значений зависит от драйвера браузера. + + + +Команда поддерживается не всеми браузерами. В типах состояние объявлено как `string`, поэтому опечатка не будет обнаружена при проверке типов. + + + +Для `emulate("geolocation")` предварительно выдавать разрешение не нужно. + +## Цветовая схема + +Чтобы проверить код, который реагирует на `prefers-color-scheme`, используйте `emulate("colorScheme")`: + +```typescript +it("проверяет реакцию JavaScript на тёмную тему", async ({ browser }) => { + await browser.emulate("colorScheme", "dark"); + await browser.url("/"); + // ... +}); +``` + +Команда меняет результат `window.matchMedia()` для запросов `prefers-color-scheme`, но не переключает CSS-правила `@media (prefers-color-scheme)`. + +Используйте её для логики, которая сама читает `matchMedia()`. Для визуальной проверки CSS-темы эта команда не подходит. + +## Сеть и офлайн-режим + +`throttleNetwork()` позволяет замедлить соединение, увеличить задержку или полностью отключить сеть. Команда поддерживается только в Chromium-браузерах с доступным CDP-подключением. + +### Скорость соединения + +Передайте готовый пресет: + +```typescript +it("показывает индикатор загрузки", async ({ browser }) => { + await browser.throttleNetwork("Good2G"); + // ... +}); +``` + +Доступные пресеты: + +- `offline` +- `GPRS` +- `Regular2G` +- `Good2G` +- `Regular3G` +- `Good3G` +- `Regular4G` +- `DSL` +- `WiFi` +- `online` + +Параметры можно задать вручную: + +```typescript +await browser.throttleNetwork({ + offline: false, + downloadThroughput: (10 * 1024) / 8, // максимальная пропускная способность загрузки (byte/sec) + uploadThroughput: (10 * 1024) / 8, // максимальная пропускная способность отправки (byte/sec) + latency: 10, // минимальная задержка от отправки запроса до получения заголовков ответа +}); +``` + +Скорость задаётся в байтах в секунду, задержка — в миллисекундах. + +### Отсутствие сети + +Чтобы отключить сетевые запросы, используйте пресет `offline`: + +```typescript +await browser.throttleNetwork("offline"); +``` + +`emulate("onLine", false)` меняет только `navigator.onLine`: + +```typescript +await browser.emulate("onLine", false); +await browser.url("/"); +``` + +Страница загружается уже с подменённым значением, поэтому не используйте событие `offline` как подтверждение применения эмуляции. + +## Замедление CPU + +Чтобы проверить skeleton-компоненты, спиннеры и debounce-логику на слабом устройстве, используйте `throttleCPU()`: + +```typescript +it("показывает skeleton на слабом устройстве", async ({ browser }) => { + await browser.throttleCPU(4); + // ... +}); +``` + +Значение `1` соответствует обычной скорости, `2` замедляет CPU вдвое, `4` — вчетверо. + +Команда поддерживается только в Chromium-браузерах с доступным CDP-подключением. В Firefox она завершается ошибкой до применения ограничений. + +## Геолокация + +Чтобы проверить региональный контент или ближайшие объекты, передайте координаты в `emulate("geolocation")`: + +```typescript +it("показывает контент для Санкт-Петербурга", async ({ browser }) => { + await browser.emulate("geolocation", { + latitude: 59.95, + longitude: 30.31667, + accuracy: 10, + }); + + await browser.url("/"); + // ... +}); +``` + +Предварительно выдавать разрешение не нужно. + +Чтобы проверить обработку ошибки, передайте объект `Error`: + +```typescript +await browser.emulate("geolocation", new Error("User denied Geolocation")); +``` + +Команда подменяет только `navigator.geolocation.getCurrentPosition()`. Она не влияет на `watchPosition()` и не учитывает параметры `timeout`, `maximumAge` и `enableHighAccuracy`. + +## Отключение JavaScript + +Отключение JavaScript помогает проверить SSR-страницы, базовую доступность контента и fallback-состояния. + +Для Chromium добавьте отдельную конфигурацию браузера и передайте Chrome preference: + +```typescript +"chrome-javascript-disabled": { + headless: true, + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + prefs: { + "profile.managed_default_content_settings.javascript": 2, + }, + }, + }, +}, +``` + +Настройка применяется при создании сессии и действует с первой загрузки страницы. Inline- и внешние скрипты не выполняются, при этом статический HTML, содержимое `noscript`, ссылки и обычные формы остаются доступными. + +Тесты без JavaScript запускайте в отдельной конфигурации браузера. Переключить preference внутри теста нельзя, cleanup не требуется: настройка удаляется вместе с профилем и сессией. Обычные WebDriver-команды продолжают работать. + + + +При включённом JavaScript элемент внутри `noscript` отсутствует в DOM, а не просто скрыт. Проверяйте его существование через `isExisting()`. + + + +Способ предназначен для Chromium-браузеров, поскольку использует `goog:chromeOptions`. + +## Организация настроек в проекте + +Повторяющиеся сценарии удобно оформлять как отдельные браузерные профили или вспомогательные функции: например, `mobile`, `dark-theme` и `slow-network`. + +Настройки, специфичные для одного сценария, оставляйте явными в самом тесте. Состояние `emulate()` может перейти в следующий тест даже при `isolation: true`, поэтому восстанавливайте его в том же тесте: + +```typescript +it("проверяет тёмную тему", async ({ browser }) => { + await browser.emulate("colorScheme", "dark"); + + try { + await browser.url("/"); + // ... + } finally { + await browser.restore("colorScheme"); + } +}); +``` + +Не откладывайте `restore()` до следующего теста: при переиспользовании сессии вызов может завершиться без ошибки, но не снять эмуляцию. + +Если после `restore()` нужно проверить исходное состояние страницы, выполните повторную навигацию. + +Для гарантированно чистой сессии в каждом тесте задайте для браузера: + +```typescript +testsPerSession: 1, +``` + +Сбрасывайте другие ограничения явно: + +| Что изменено | Как вернуть | +| -------------------------------- | -------------------------------------- | +| `emulate("clock")` | `clock.restore()` | +| `emulate("device")` | Сохранённая функция отката | +| `setViewport()` | Повторный вызов с исходными значениями | +| `setWindowSize()` | Повторный вызов с исходными значениями | +| `throttleNetwork()` | `browser.throttleNetwork("online")` | +| `throttleCPU()` | `browser.throttleCPU(1)` | +| `page.emulateTimezone()` | `page.emulateTimezone()` без аргумента | +| Chrome preference для JavaScript | Завершение сессии | From 39b4a07b7830d53fcdaf18234245c00ddc5a17b1 Mon Sep 17 00:00:00 2001 From: kseniataranov Date: Wed, 9 Sep 2026 10:03:26 +0300 Subject: [PATCH 2/4] docs(emulation): rewrite RU guide around BiDi, CDP, and browser config --- .../user-environment-emulation.mdx | 584 +++++++++++------- 1 file changed, 358 insertions(+), 226 deletions(-) diff --git a/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx b/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx index f1ace798..a9f8db61 100644 --- a/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx +++ b/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx @@ -4,123 +4,213 @@ import Admonition from "@theme/Admonition"; -- Как воспроизвести мобильное устройство, цветовую схему, геолокацию и разрешения -- Как проверить медленную сеть, офлайн-режим и слабый CPU -- Какие команды требуют WebDriver BiDi или CDP -- Как изолировать тесты и восстанавливать изменённое состояние +- Как эмулировать устройство и viewport, цветовую схему, время, геолокацию, разрешения +- Как задать язык интерфейса и отключенный JavaScript +- Как проверить поведение приложения при медленной сети и слабом CPU ## Введение -Пользовательская среда влияет на то, как приложение выглядит и работает. Один и тот же интерфейс может по-разному вести себя на мобильном экране, при другой локали, в тёмной теме, без доступа к геолокации или при медленном соединении. +Пользовательская среда влияет на то, как приложение выглядит и работает. Один и тот же интерфейс может по-разному вести себя на мобильном экране, при другой локали, в темной теме, без доступа к геолокации или при медленном соединении. -В Testplane эти условия настраиваются несколькими способами: через WebDriver BiDi, обычные WebDriver-команды, CDP и capabilities браузера. Выбор механизма зависит от того, какое свойство среды нужно изменить и в каком браузере выполняется тест. +В Testplane часть таких условий можно менять во время теста, а часть — задавать в настройках браузера. -| Механизм | Команды | -| ------------------------ | ------------------------------------- | -| WebDriver BiDi | `emulate()`, `setViewport()` | -| Chrome DevTools Protocol | `throttleNetwork()`, `throttleCPU()` | -| WebDriver | `setPermissions()`, `setWindowSize()` | - -Все варианты `emulate()` и команда `setViewport()` требуют WebDriver BiDi. Чтобы его включить, добавьте `webSocketUrl: true` в `desiredCapabilities`: +Для команд [`browser.emulate()`][emulate] и [`setViewport()`][set-viewport] требуется [WebDriver BiDi][webdriver-bidi]. В конфигурации браузера включите `webSocketUrl`: ```typescript -"chrome": { - desiredCapabilities: { - browserName: "chrome", - browserVersion: "128.0", - webSocketUrl: true, +browsers: { + chrome: { + desiredCapabilities: { + browserName: "chrome", + webSocketUrl: true, + }, }, }, ``` -Минимальная версия Chrome с поддержкой BiDi — 128, Firefox — 119. +В одной Chrome-сессии с `webSocketUrl: true` можно вызывать и [`browser.emulate()`][emulate], и команды через [Chrome DevTools Protocol][how-to-use-cdp]: [`getPuppeteer()`][get-puppeteer], [`throttleNetwork()`][throttle-network], [`throttleCPU()`][throttle-cpu]. Отдельную конфигурацию без BiDi для этого заводить не нужно. - +Минимальная версия Chrome с поддержкой BiDi — 128, Firefox — 119. -`emulate()` применяется при создании нового документа. Вызывайте команду до `browser.url()`. + -После `browser.restore()` перезагрузите страницу или выполните повторную навигацию, если в том же тесте нужно проверить восстановленное состояние. +`throttleNetwork()`, `throttleCPU()` и команды, которые используются через `getPuppeteer()`, работают поверх [Chrome DevTools Protocol][how-to-use-cdp] и доступны только в Chromium. -## Эмуляция мобильных устройств +### Порядок вызовов + +Команда `emulate()` работает через preload-скрипты BiDi: браузер применяет их при создании документа. Поэтому вызывать ее нужно до навигации: на уже открытой странице она ничего не изменит. Команда `restore()` снимает эмуляцию только для следующих документов: текущая страница останется как была, пока ее не открыть заново. + +На команды через CDP это не распространяется: например, [`page.emulateTimezone()`][page-emulate-timezone] переключает зону и в уже открытом документе. + +## Экран и устройство + +### Viewport + +[`setViewport()`][set-viewport] задает размер области отрисовки. Команда подходит для проверки адаптивной верстки и поведения интерфейса на разных брейкпойнтах. Если нужно менять не область отрисовки, а размер окна браузера, используйте [`setWindowSize()`][set-window-size]. + +```typescript +it("показывает мобильную навигацию на узком экране", async ({ browser }) => { + await browser.setViewport({ + width: 390, + height: 844, + }); + + await browser.url("/"); + + await expect(browser.$("[data-testid='mobile-menu']")).toBeDisplayed(); +}); +``` -Мобильная эмуляция помогает проверить адаптивную вёрстку, мобильную навигацию и отображение интерфейса на экранах с высоким DPR. Основные возможности ориентированы на Chromium-based браузеры. +Размер, который задает `setViewport()`, действует до конца сессии, отдельной команды отката нет. ### Профиль устройства -Чтобы одновременно задать viewport, DPR и user agent, используйте `emulate("device")`: +[`emulate("device")`][emulate-device] применяет готовый профиль устройства: viewport, DPR и `navigator.userAgent`. + +Например, несколько тестов для iPhone можно объединить одним профилем: ```typescript -it("отображает мобильную вёрстку", async ({ browser }) => { - const restoreDevice = await browser.emulate("device", "iPhone 12 Pro Max"); +describe("iPhone 15", () => { + let restoreDevice: (() => Promise) | undefined; + let viewport: { width: number; height: number; devicePixelRatio: number }; - try { + before(async ({ browser }) => { await browser.url("/"); - // ... - } finally { - await restoreDevice(); - } + + viewport = await browser.execute(() => ({ + width: window.innerWidth, + height: window.innerHeight, + devicePixelRatio: window.devicePixelRatio, + })); + }); + + beforeEach(async ({ browser }) => { + restoreDevice = await browser.emulate("device", "iPhone 15"); + }); + + afterEach(async ({ browser }) => { + await restoreDevice?.(); + restoreDevice = undefined; + + await browser.setViewport(viewport); + }); + + it("показывает инструкцию для iOS на профиле iPhone 15", async ({ browser }) => { + await browser.url("/"); + + await expect(browser.$("[data-testid='ios-install-guide']")).toBeDisplayed(); + }); }); ``` -Команда не включает touch-события и мобильный режим браузера. +Функция, которую возвращает `emulate("device")`, снимает подмененный user agent, но исходный viewport не возвращает: вместо него ставится профиль Desktop Chrome — 1280 × 720, DPR 1. Поэтому в примере размер запоминается в `before` и после каждого теста возвращается через [`setViewport()`][set-viewport]. Замерять нужно на странице приложения: на `about:blank` значения будут другими. Сам `emulate("device")` по-прежнему вызывается до навигации. - +### User agent -`browser.restore()` возвращает user agent, но не viewport, установленный через `emulate("device")`. Для полного отката вызывайте функцию, которую вернул `emulate("device")`. +Для user agent есть два разных сценария: клиентский код может читать `navigator.userAgent`, а сервер — HTTP-заголовок `User-Agent`. -Viewport при этом вернётся к профилю `Desktop Chrome`, а не к исходному размеру. +#### navigator.userAgent - +[`emulate("userAgent")`][emulate-user-agent] меняет значение, доступное клиентскому JavaScript через `navigator.userAgent`. -### Viewport и DPR +```typescript +it("показывает инструкцию для iOS по navigator.userAgent", async ({ browser }) => { + await browser.emulate( + "userAgent", + "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", + ); -Чтобы проверить конкретный брейкпойнт, используйте `setViewport()`: + await browser.url("/"); -```typescript -await browser.setViewport({ - width: 390, - height: 844, - devicePixelRatio: 3, + await expect(browser.$("[data-testid='ios-install-guide']")).toBeDisplayed(); }); ``` -Команда применяется к текущему контексту и не возвращает функцию отката. Чтобы восстановить viewport, вызовите `setViewport()` повторно с нужными значениями. +#### HTTP User-Agent -`setWindowSize()` меняет размер всего окна, а не области отрисовки: +Если приложение определяет тип клиента на сервере по заголовку `User-Agent`, используйте [`browser.getPuppeteer()`][get-puppeteer] и Puppeteer [`page.setUserAgent()`][page-set-user-agent]. Подробнее о работе с Puppeteer и CDP — в разделе [«Как использовать Chrome DevTools Protocol в Testplane»][how-to-use-cdp]. ```typescript -await browser.setWindowSize(500, 600); +it("передает мобильный User-Agent на сервер", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + + await page.setUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", + ); + + await browser.url("/"); + // ... +}); ``` -Для мобильных брейкпойнтов используйте `setViewport()`. +`page.setUserAgent()` меняет HTTP `User-Agent` и одновременно меняет `navigator.userAgent`. -### User agent +## Локаль + +Язык, который приложение читает в `navigator.language` и в заголовке `Accept-Language`, и локаль, по которой `Intl` форматирует числа и даты, задаются по отдельности. Настройка `intl.accept_languages` меняет языковые предпочтения и не трогает `Intl`. [`Emulation.setLocaleOverride`][cdp-set-locale-override] меняет локаль `Intl`, но не языковые предпочтения браузера. + +В Chrome на macOS аргумент запуска `--lang` принимается и молча игнорируется, браузер продолжает сообщать системный язык. + +### Язык интерфейса -Если клиентский код выбирает мобильный интерфейс по `navigator.userAgent`, задайте значение отдельно: +Если приложение выбирает язык по языковым предпочтениям браузера или заголовку `Accept-Language`, задайте `intl.accept_languages` в конфигурации браузера. + +Для Chrome: ```typescript -await browser.emulate( - "userAgent", - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", -); +browsers: { + "chrome-de": { + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + prefs: { + "intl.accept_languages": "de-DE,de", + }, + }, + }, + }, +}, ``` -Команда не меняет HTTP-заголовок `User-Agent` и Client Hints. Она подходит только для кода, который читает `navigator.userAgent` в браузере. +Для Firefox: + +```typescript +browsers: { + "firefox-de": { + desiredCapabilities: { + browserName: "firefox", + "moz:firefoxOptions": { + prefs: { + "intl.accept_languages": "de-DE,de", + }, + }, + }, + }, +}, +``` + +После этого тест может проверять интерфейс с нужной локалью: + +```typescript +it("показывает интерфейс на немецком", async ({ browser }) => { + await browser.url("/"); -## Язык и временная зона + await expect(browser.$("[data-testid='page-title']")).toHaveText("Bestellungen"); +}); +``` -Эти параметры нужны для проверки переводов, форматов дат, чисел и времени. +### Форматирование через `Intl` -### Локали +Если приложение форматирует числа или даты через `Intl`, локаль `Intl` можно изменить через Puppeteer. -В Chromium можно изменить локаль `Intl` и заголовок `Accept-Language` через Puppeteer и CDP: +Например, так можно проверить форматирование числа для немецкой локали: ```typescript -it("открывает страницу с немецкой локалью", async ({ browser }) => { +it("форматирует число для немецкой локали", async ({ browser }) => { const puppeteer = await browser.getPuppeteer(); const [page] = await puppeteer.pages(); const client = await page.target().createCDPSession(); @@ -129,312 +219,354 @@ it("открывает страницу с немецкой локалью", asy locale: "de-DE", }); - await page.setExtraHTTPHeaders({ - "Accept-Language": "de-DE,de;q=0.9", - }); - await browser.url("/"); - // ... + + await expect(browser.$("[data-testid='average-value']")).toHaveText("1.234,56"); }); ``` -`Emulation.setLocaleOverride` меняет локаль `Intl` и форматирование дат и чисел. `page.setExtraHTTPHeaders()` меняет заголовок `Accept-Language`, который получает сервер. - -Эти настройки не меняют `navigator.language` и `navigator.languages`. Способ подходит, если приложение получает локаль с сервера или использует `Intl` без явно заданной локали. Если клиентский код читает `navigator.language`, потребуется другой способ запуска браузера с нужной системной локалью. - -Способ с Puppeteer и CDP предназначен для Chromium-браузеров. +В этом сценарии приложение форматирует значение `1234.56` через `Intl.NumberFormat`, поэтому при локали `de-DE` оно отображается как `1.234,56`. -### Часовой пояс +## Часовой пояс -В Chromium используйте `page.emulateTimezone()`: +Если отображение дат и времени зависит от часового пояса пользователя, задайте нужный часовой пояс через Puppeteer [`page.emulateTimezone()`][page-emulate-timezone]. ```typescript -it("показывает время для Нью-Йорка", async ({ browser }) => { +it("показывает время события в часовом поясе пользователя", async ({ browser }) => { const puppeteer = await browser.getPuppeteer(); const [page] = await puppeteer.pages(); - try { - await page.emulateTimezone("America/New_York"); - await browser.url("/"); - // ... - } finally { - await page.emulateTimezone(); - } + await page.emulateTimezone("America/New_York"); + await browser.url("/"); + + await expect(browser.$("[data-testid='event-time']")).toHaveText("07:00"); }); ``` -Команда меняет часовой пояс для `Date` и `Intl`: `resolvedOptions().timeZone`, `getTimezoneOffset()`, `Date.prototype.toString()` и форматирование без явно заданного `timeZone`. +В этом примере страница показывает время события `2024-09-04T11:00:00Z`, в зоне `America/New_York` это 07:00. -Вызывайте `emulateTimezone()` до навигации, чтобы код страницы сразу использовал нужную зону. Технически изменение применяется и к уже открытому документу. +## Время и таймеры -Значение `"UTC"` не сбрасывает настройку, а устанавливает новую зону. Для снятия override вызовите `emulateTimezone()` без аргумента. +Когда поведение интерфейса зависит от текущего времени или таймеров, используйте [`browser.emulate("clock")`][emulate-clock]. -### Системное время +### Фиксированное время -Для сценариев, зависящих от даты и таймеров, используйте `emulate("clock")`. Используйте эту команду до навигации, чтобы скрипты страницы сразу использовали подменённое время. +Например, так можно проверить состояние страницы в определенный момент: ```typescript -it("показывает акцию на заданную дату", async ({ browser }) => { +it("показывает активную акцию в заданный период", async ({ browser }) => { const clock = await browser.emulate("clock", { - now: new Date(2025, 11, 31), + now: new Date("2024-09-04T12:30:00Z"), }); try { await browser.url("/"); - // ... + + await expect(browser.$("[data-testid='promo-status']")).toHaveText("Акция началась"); } finally { await clock.restore(); } }); ``` -`emulate("clock")` не заменяет настройку часового пояса: команда управляет временем и таймерами, но не меняет `Intl.DateTimeFormat().resolvedOptions().timeZone`. - ### Таймеры -Чтобы выполнить действие, запланированное через браузерный таймер, вызовите `tick()` и передайте количество миллисекунд: +[`tick(ms)`][clock-tick] продвигает виртуальное время на указанное количество миллисекунд и запускает таймеры, которые должны сработать. ```typescript -it("скрывает уведомление через пять секунд", async ({ browser }) => { - const clock = await browser.emulate("clock"); +it("скрывает уведомление через 5 секунд", async ({ browser }) => { + const clock = await browser.emulate("clock", { + now: new Date("2024-09-04T12:30:00Z"), + }); try { await browser.url("/"); - await browser.findByTestId("show-notification").click(); await clock.tick(5000); - await expect(browser.findByTestId("show-notification")).not.toBeDisplayed(); + await expect(browser.$("[data-testid='notification']")).not.toBeDisplayed(); } finally { await clock.restore(); } }); ``` -В этом примере пять секунд проходят для таймеров страницы, но тест не ждёт их в реальном времени. +## Цветовая схема -### Подмена необходимых таймеров +Если приложение определяет цветовую схему через `window.matchMedia()`, используйте [`browser.emulate("colorScheme")`][emulate-color-scheme]. -По умолчанию clock подменяет поддерживаемые браузерные таймеры и `Date`. Через `toFake` можно ограничить список: +Например, так можно проверить выбор изображения для темной цветовой схемы: ```typescript -const clock = await browser.emulate("clock", { - toFake: ["Date", "setTimeout", "clearTimeout"], +it("показывает изображение для темной цветовой схемы", async ({ browser }) => { + await browser.emulate("colorScheme", "dark"); + await browser.url("/"); + + await expect(browser.$("[data-testid='theme-image']")).toHaveAttribute( + "src", + "/images/night.svg", + ); }); ``` -Используйте этот вариант, когда тесту нужно управлять только отдельными API и не затрагивать остальные таймеры страницы. +`browser.emulate("colorScheme")` меняет результат `matchMedia()` для `prefers-color-scheme`. Для проверки стилей, заданных через CSS `@media (prefers-color-scheme)`, используйте [`Emulation.setEmulatedMedia`][cdp-set-emulated-media]. -## Разрешения браузера +```typescript +it("применяет стили для темной цветовой схемы", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + const client = await page.target().createCDPSession(); -`setPermissions()` изменяет состояние разрешения для текущего origin. Сначала откройте целевую страницу, затем вызовите команду: + await client.send("Emulation.setEmulatedMedia", { + features: [ + { + name: "prefers-color-scheme", + value: "dark", + }, + ], + }); -```typescript -it("работает при выданном доступе к геолокации", async ({ browser }) => { await browser.url("/"); - await browser.setPermissions({ name: "geolocation" }, "granted"); - // ... + + const background = await browser + .$("[data-testid='theme-box']") + .getCSSProperty("background-color"); + + expect(background.value).toBe("rgba(0,0,0,1)"); }); ``` -До первой навигации браузер находится на `about:blank`. Для непрозрачного origin этой страницы разрешение выдать нельзя. +## Сеть -В примерах протокола используются состояния `"granted"`, `"denied"` и `"prompt"`. Поддержка разрешений и значений зависит от драйвера браузера. +### Отсутствие сети - +Для проверки работы приложения без сети используйте [`browser.throttleNetwork("offline")`][throttle-network]. -Команда поддерживается не всеми браузерами. В типах состояние объявлено как `string`, поэтому опечатка не будет обнаружена при проверке типов. +Например, так можно проверить сообщение об ошибке при сетевом запросе: - +```typescript +it("показывает сообщение при отсутствии сети", async ({ browser }) => { + await browser.url("/"); -Для `emulate("geolocation")` предварительно выдавать разрешение не нужно. + await browser.throttleNetwork("offline"); -## Цветовая схема + await browser.$("[data-testid='load-orders']").click(); -Чтобы проверить код, который реагирует на `prefers-color-scheme`, используйте `emulate("colorScheme")`: + await expect(browser.$("[data-testid='network-error']")).toHaveText("Нет подключения к сети"); -```typescript -it("проверяет реакцию JavaScript на тёмную тему", async ({ browser }) => { - await browser.emulate("colorScheme", "dark"); - await browser.url("/"); - // ... + await browser.throttleNetwork("online"); }); ``` -Команда меняет результат `window.matchMedia()` для запросов `prefers-color-scheme`, но не переключает CSS-правила `@media (prefers-color-scheme)`. +Сначала загрузите страницу, а затем отключите сеть перед действием, которое отправляет запрос. В отличие от `emulate()`, эту команду нужно вызывать после навигации. Для возврата к обычному сетевому режиму используйте профиль `"online"`. -Используйте её для логики, которая сама читает `matchMedia()`. Для визуальной проверки CSS-темы эта команда не подходит. +### Медленное соединение -## Сеть и офлайн-режим +Для проверки интерфейса при медленном соединении передайте параметры сети в [`browser.throttleNetwork()`][throttle-network]: -`throttleNetwork()` позволяет замедлить соединение, увеличить задержку или полностью отключить сеть. Команда поддерживается только в Chromium-браузерах с доступным CDP-подключением. +```typescript +it("показывает состояние загрузки при медленной сети", async ({ browser }) => { + await browser.url("/orders"); + + await browser.throttleNetwork({ + offline: false, + latency: 500, + downloadThroughput: (50 * 1024) / 8, + uploadThroughput: (20 * 1024) / 8, + }); -### Скорость соединения + await browser.$("[data-testid='load-orders']").click(); -Передайте готовый пресет: + await expect(browser.$("[data-testid='loading']")).toBeDisplayed(); -```typescript -it("показывает индикатор загрузки", async ({ browser }) => { - await browser.throttleNetwork("Good2G"); - // ... + await browser.throttleNetwork("online"); }); ``` -Доступные пресеты: +В объекте четыре поля: `offline`, `latency` в миллисекундах и `downloadThroughput` / `uploadThroughput` — скорость в байтах в секунду. + +Для типовых условий объект не нужен: можно передать имя профиля, например `"Good3G"` или `"offline"`. -- `offline` -- `GPRS` -- `Regular2G` -- `Good2G` -- `Regular3G` -- `Good3G` -- `Regular4G` -- `DSL` -- `WiFi` -- `online` +### navigator.onLine -Параметры можно задать вручную: +Если приложение определяет состояние подключения по `navigator.onLine`, используйте [`browser.emulate("onLine")`][emulate-online]: ```typescript -await browser.throttleNetwork({ - offline: false, - downloadThroughput: (10 * 1024) / 8, // максимальная пропускная способность загрузки (byte/sec) - uploadThroughput: (10 * 1024) / 8, // максимальная пропускная способность отправки (byte/sec) - latency: 10, // минимальная задержка от отправки запроса до получения заголовков ответа +it("показывает офлайн-режим", async ({ browser }) => { + await browser.emulate("onLine", false); + await browser.url("/"); + + await expect(browser.$("[data-testid='connection-status']")).toHaveText("Офлайн"); }); ``` -Скорость задаётся в байтах в секунду, задержка — в миллисекундах. +`browser.emulate("onLine", false)` меняет значение `navigator.onLine`, но не отключает сеть: HTTP-запросы продолжают выполняться. -### Отсутствие сети +## Производительность CPU -Чтобы отключить сетевые запросы, используйте пресет `offline`: +Для проверки интерфейса при ограниченной производительности процессора используйте [`browser.throttleCPU()`][throttle-cpu]. -```typescript -await browser.throttleNetwork("offline"); -``` - -`emulate("onLine", false)` меняет только `navigator.onLine`: +Например, так можно запустить сценарий с четырехкратным замедлением CPU: ```typescript -await browser.emulate("onLine", false); -await browser.url("/"); -``` - -Страница загружается уже с подменённым значением, поэтому не используйте событие `offline` как подтверждение применения эмуляции. - -## Замедление CPU +it("работает при замедленном CPU", async ({ browser }) => { + await browser.throttleCPU(4); -Чтобы проверить skeleton-компоненты, спиннеры и debounce-логику на слабом устройстве, используйте `throttleCPU()`: + await browser.url("/"); -```typescript -it("показывает skeleton на слабом устройстве", async ({ browser }) => { - await browser.throttleCPU(4); // ... + + await browser.throttleCPU(1); }); ``` -Значение `1` соответствует обычной скорости, `2` замедляет CPU вдвое, `4` — вчетверо. - -Команда поддерживается только в Chromium-браузерах с доступным CDP-подключением. В Firefox она завершается ошибкой до применения ограничений. +Чем больше коэффициент, тем сильнее замедляется выполнение. Значение `1` отключает throttling. ## Геолокация -Чтобы проверить региональный контент или ближайшие объекты, передайте координаты в `emulate("geolocation")`: +Если приложение использует координаты пользователя, задайте их через [`browser.emulate("geolocation")`][emulate-geolocation]. + +Например, так можно проверить поиск ближайшего пункта выдачи для пользователя в Берлине: ```typescript -it("показывает контент для Санкт-Петербурга", async ({ browser }) => { +it("показывает ближайший пункт выдачи", async ({ browser }) => { await browser.emulate("geolocation", { - latitude: 59.95, - longitude: 30.31667, - accuracy: 10, + latitude: 52.52, + longitude: 13.405, }); await browser.url("/"); - // ... + + await expect(browser.$("[data-testid='nearest-point']")).toHaveText( + "Пункт выдачи на Alexanderplatz", + ); }); ``` -Предварительно выдавать разрешение не нужно. +`browser.emulate("geolocation")` подменяет координаты, которые приложение получает через `navigator.geolocation.getCurrentPosition()`, для этого не требуется отдельно настраивать разрешение на геолокацию. -Чтобы проверить обработку ошибки, передайте объект `Error`: +## Разрешения браузера + +Если поведение приложения зависит от разрешений браузера, используйте [`browser.setPermissions()`][set-permissions]. + +Например, так можно проверить статус уведомлений: ```typescript -await browser.emulate("geolocation", new Error("User denied Geolocation")); +it("показывает статус уведомлений", async ({ browser }) => { + await browser.url("/"); + + await browser.setPermissions( + { + name: "notifications", + }, + "granted", + ); + + await browser.$("[data-testid='check-notifications']").click(); + + await expect(browser.$("[data-testid='notification-status']")).toHaveText( + "Уведомления включены", + ); +}); ``` -Команда подменяет только `navigator.geolocation.getCurrentPosition()`. Она не влияет на `watchPosition()` и не учитывает параметры `timeout`, `maximumAge` и `enableHighAccuracy`. +Вызывайте `browser.setPermissions()` после перехода на страницу приложения: разрешение привязывается к адресу открытой страницы, а до навигации она будет пустой, и команда упадет с ошибкой. -## Отключение JavaScript +## JavaScript -Отключение JavaScript помогает проверить SSR-страницы, базовую доступность контента и fallback-состояния. +Если нужно проверить работу страницы без JavaScript, отключите его в конфигурации браузера. -Для Chromium добавьте отдельную конфигурацию браузера и передайте Chrome preference: +Для Chrome: ```typescript -"chrome-javascript-disabled": { - headless: true, - desiredCapabilities: { - browserName: "chrome", - "goog:chromeOptions": { - prefs: { - "profile.managed_default_content_settings.javascript": 2, +browsers: { + "chrome-no-js": { + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + prefs: { + "profile.managed_default_content_settings.javascript": 2, + }, }, }, }, }, ``` -Настройка применяется при создании сессии и действует с первой загрузки страницы. Inline- и внешние скрипты не выполняются, при этом статический HTML, содержимое `noscript`, ссылки и обычные формы остаются доступными. +Для Firefox: -Тесты без JavaScript запускайте в отдельной конфигурации браузера. Переключить preference внутри теста нельзя, cleanup не требуется: настройка удаляется вместе с профилем и сессией. Обычные WebDriver-команды продолжают работать. +```typescript +browsers: { + "firefox-no-js": { + desiredCapabilities: { + browserName: "firefox", + "moz:firefoxOptions": { + prefs: { + "javascript.enabled": false, + }, + }, + }, + }, +}, +``` - +После этого тест запускается сразу в браузере с отключенным JavaScript: -При включённом JavaScript элемент внутри `noscript` отсутствует в DOM, а не просто скрыт. Проверяйте его существование через `isExisting()`. +```typescript +it("показывает содержимое без JavaScript", async ({ browser }) => { + await browser.url("/"); - + await expect(browser.$("[data-testid='no-js-message']")).toBeDisplayed(); +}); +``` -Способ предназначен для Chromium-браузеров, поскольку использует `goog:chromeOptions`. +## Состояние и изоляция -## Организация настроек в проекте +Некоторые настройки среды сохраняются в рамках WebDriver-сессии и могут повлиять на следующие тесты. -Повторяющиеся сценарии удобно оформлять как отдельные браузерные профили или вспомогательные функции: например, `mobile`, `dark-theme` и `slow-network`. +Снимайте эмуляцию в том же тесте, где ее включили, или в `afterEach`. Вызов [`restore()`][restore] из следующего теста уже не сработает: у нового теста другой объект `browser`. -Настройки, специфичные для одного сценария, оставляйте явными в самом тесте. Состояние `emulate()` может перейти в следующий тест даже при `isolation: true`, поэтому восстанавливайте его в том же тесте: +Если одна и та же эмуляция нужна в нескольких тестах, задавайте и снимайте ее в хуках: ```typescript -it("проверяет тёмную тему", async ({ browser }) => { - await browser.emulate("colorScheme", "dark"); +describe("темная цветовая схема", () => { + beforeEach(async ({ browser }) => { + await browser.emulate("colorScheme", "dark"); + }); - try { - await browser.url("/"); - // ... - } finally { + afterEach(async ({ browser }) => { await browser.restore("colorScheme"); - } -}); -``` - -Не откладывайте `restore()` до следующего теста: при переиспользовании сессии вызов может завершиться без ошибки, но не снять эмуляцию. - -Если после `restore()` нужно проверить исходное состояние страницы, выполните повторную навигацию. + }); -Для гарантированно чистой сессии в каждом тесте задайте для браузера: + it("показывает изображение для темной схемы", async ({ browser }) => { + await browser.url("/"); -```typescript -testsPerSession: 1, + // ... + }); +}); ``` -Сбрасывайте другие ограничения явно: - -| Что изменено | Как вернуть | -| -------------------------------- | -------------------------------------- | -| `emulate("clock")` | `clock.restore()` | -| `emulate("device")` | Сохранённая функция отката | -| `setViewport()` | Повторный вызов с исходными значениями | -| `setWindowSize()` | Повторный вызов с исходными значениями | -| `throttleNetwork()` | `browser.throttleNetwork("online")` | -| `throttleCPU()` | `browser.throttleCPU(1)` | -| `page.emulateTimezone()` | `page.emulateTimezone()` без аргумента | -| Chrome preference для JavaScript | Завершение сессии | +Для настроек, которые должны действовать всю сессию, используйте отдельную конфигурацию браузера. Например, так удобнее задавать язык браузера, запускать тесты с отключенным JavaScript или фиксировать размер окна опцией [`windowSize`][window-size]. + +[emulate]: https://webdriver.io/docs/api/browser/emulate +[webdriver-bidi]: https://w3c.github.io/webdriver-bidi/ +[set-viewport]: https://webdriver.io/docs/api/browser/setViewport +[set-window-size]: ../commands/browser/setWindowSize.mdx +[window-size]: ../reference/config/browsers.mdx#window_size +[how-to-use-cdp]: ../guides/how-to-use-cdp.mdx +[emulate-device]: https://webdriver.io/docs/emulation#device +[emulate-user-agent]: https://webdriver.io/docs/emulation#user-agent +[get-puppeteer]: ../commands/browser/getPuppeteer.mdx +[page-set-user-agent]: https://pptr.dev/api/puppeteer.page.setuseragent +[cdp-set-locale-override]: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setLocaleOverride +[page-emulate-timezone]: https://pptr.dev/api/puppeteer.page.emulatetimezone +[emulate-clock]: https://webdriver.io/docs/emulation#clock +[clock-tick]: https://webdriver.io/docs/api/clock/tick +[emulate-color-scheme]: https://webdriver.io/docs/emulation#color-scheme +[cdp-set-emulated-media]: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setEmulatedMedia +[throttle-network]: https://webdriver.io/docs/api/browser/throttleNetwork +[emulate-online]: https://webdriver.io/docs/emulation#online-property +[throttle-cpu]: https://webdriver.io/docs/api/browser/throttleCPU +[emulate-geolocation]: https://webdriver.io/docs/emulation#geolocation +[set-permissions]: https://webdriver.io/docs/api/webdriver#setpermissions +[restore]: https://webdriver.io/docs/api/browser/restore From 23deecfa1aed2b6a8ef11efd6ed1115478cde66a Mon Sep 17 00:00:00 2001 From: kseniataranov Date: Wed, 16 Sep 2026 12:03:17 +0300 Subject: [PATCH 3/4] docs(emulation): improve RU guide and switch examples to testing-library --- .../user-environment-emulation.mdx | 196 +++++++++--------- 1 file changed, 100 insertions(+), 96 deletions(-) diff --git a/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx b/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx index a9f8db61..e356eaaf 100644 --- a/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx +++ b/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx @@ -4,17 +4,17 @@ import Admonition from "@theme/Admonition"; -- Как эмулировать устройство и viewport, цветовую схему, время, геолокацию, разрешения -- Как задать язык интерфейса и отключенный JavaScript -- Как проверить поведение приложения при медленной сети и слабом CPU +- Как эмулировать устройство и viewport, цветовую схему, время, геолокацию и разрешения +- Как задать язык интерфейса и отключить JavaScript +- Как проверить работу приложения при медленной сети и слабом CPU ## Введение -Пользовательская среда влияет на то, как приложение выглядит и работает. Один и тот же интерфейс может по-разному вести себя на мобильном экране, при другой локали, в темной теме, без доступа к геолокации или при медленном соединении. +Пользовательская среда влияет на внешний вид и работу приложения. Один и тот же интерфейс может по-разному вести себя на мобильном экране, при другой локали, в темной теме, без доступа к геолокации или при медленном соединении. -В Testplane часть таких условий можно менять во время теста, а часть — задавать в настройках браузера. +В Testplane одни параметры среды можно менять прямо во время теста, а другие нужно заранее задать в настройках браузера. Для команд [`browser.emulate()`][emulate] и [`setViewport()`][set-viewport] требуется [WebDriver BiDi][webdriver-bidi]. В конфигурации браузера включите `webSocketUrl`: @@ -29,27 +29,25 @@ browsers: { }, ``` -В одной Chrome-сессии с `webSocketUrl: true` можно вызывать и [`browser.emulate()`][emulate], и команды через [Chrome DevTools Protocol][how-to-use-cdp]: [`getPuppeteer()`][get-puppeteer], [`throttleNetwork()`][throttle-network], [`throttleCPU()`][throttle-cpu]. Отдельную конфигурацию без BiDi для этого заводить не нужно. +В одной сессии Chrome с `webSocketUrl: true` можно использовать и [`browser.emulate()`][emulate], и команды, которые работают через [Chrome DevTools Protocol][how-to-use-cdp]: [`getPuppeteer()`][get-puppeteer], [`throttleNetwork()`][throttle-network] и [`throttleCPU()`][throttle-cpu]. Создавать для них отдельную конфигурацию без BiDi не нужно. Минимальная версия Chrome с поддержкой BiDi — 128, Firefox — 119. +В большинстве случаев браузер применяет настройки `emulate()` при открытии страницы. Поэтому сначала вызовите команду, а затем переходите на нужную страницу. Исключения указаны в соответствующих разделах. + +Чтобы искать элементы через `getByTestId` и `findByTestId`, как в примерах из этой статьи, установите и подключите [@testplane/testing-library][testing-library-guide]. В тесте с отключенным JavaScript эти команды недоступны, поэтому в нем используется `$()`. + `throttleNetwork()`, `throttleCPU()` и команды, которые используются через `getPuppeteer()`, работают поверх [Chrome DevTools Protocol][how-to-use-cdp] и доступны только в Chromium. -### Порядок вызовов - -Команда `emulate()` работает через preload-скрипты BiDi: браузер применяет их при создании документа. Поэтому вызывать ее нужно до навигации: на уже открытой странице она ничего не изменит. Команда `restore()` снимает эмуляцию только для следующих документов: текущая страница останется как была, пока ее не открыть заново. - -На команды через CDP это не распространяется: например, [`page.emulateTimezone()`][page-emulate-timezone] переключает зону и в уже открытом документе. - ## Экран и устройство ### Viewport -[`setViewport()`][set-viewport] задает размер области отрисовки. Команда подходит для проверки адаптивной верстки и поведения интерфейса на разных брейкпойнтах. Если нужно менять не область отрисовки, а размер окна браузера, используйте [`setWindowSize()`][set-window-size]. +[`setViewport()`][set-viewport] задает размер области отрисовки. С помощью команды можно проверять адаптивную верстку и поведение интерфейса на разных брейкпойнтах. Чтобы изменить размер всего окна браузера, а не viewport, используйте [`setWindowSize()`][set-window-size]. ```typescript it("показывает мобильную навигацию на узком экране", async ({ browser }) => { @@ -60,57 +58,36 @@ it("показывает мобильную навигацию на узком await browser.url("/"); - await expect(browser.$("[data-testid='mobile-menu']")).toBeDisplayed(); + const mobileMenu = await browser.getByTestId("mobile-menu"); + await expect(mobileMenu).toBeDisplayed(); }); ``` -Размер, который задает `setViewport()`, действует до конца сессии, отдельной команды отката нет. +Размер, заданный через `setViewport()`, сохраняется до конца сессии. Отдельной команды для отката нет. ### Профиль устройства -[`emulate("device")`][emulate-device] применяет готовый профиль устройства: viewport, DPR и `navigator.userAgent`. +[`emulate("device")`][emulate-device] применяет готовый профиль устройства: viewport, DPR и `navigator.userAgent`. Viewport меняется сразу, а user agent — только в документах, созданных после вызова команды. Поэтому сначала включите эмуляцию, а затем переходите на нужную страницу. -Например, несколько тестов для iPhone можно объединить одним профилем: +Профиль устройства подменяет user agent и размеры, но не превращает десктопный браузер в мобильный. Например, дескриптор `iPhone 15` содержит параметры `isMobile` и `hasTouch`, но команда их не применяет. Тач-события и `navigator.maxTouchPoints` не эмулируются, движок остается Chromium вместо WebKit/iOS, а системные шрифты, экранная клавиатура, адресная строка и производительность не меняются. Поэтому такая эмуляция не заменяет проверку на реальном устройстве. ```typescript -describe("iPhone 15", () => { - let restoreDevice: (() => Promise) | undefined; - let viewport: { width: number; height: number; devicePixelRatio: number }; - - before(async ({ browser }) => { - await browser.url("/"); - - viewport = await browser.execute(() => ({ - width: window.innerWidth, - height: window.innerHeight, - devicePixelRatio: window.devicePixelRatio, - })); - }); - - beforeEach(async ({ browser }) => { - restoreDevice = await browser.emulate("device", "iPhone 15"); - }); - - afterEach(async ({ browser }) => { - await restoreDevice?.(); - restoreDevice = undefined; - - await browser.setViewport(viewport); - }); - - it("показывает инструкцию для iOS на профиле iPhone 15", async ({ browser }) => { - await browser.url("/"); +it("показывает инструкцию для iOS на профиле iPhone 15", async ({ browser }) => { + await browser.emulate("device", "iPhone 15"); + await browser.url("/"); - await expect(browser.$("[data-testid='ios-install-guide']")).toBeDisplayed(); - }); + const installGuide = await browser.getByTestId("ios-install-guide"); + await expect(installGuide).toBeDisplayed(); }); ``` -Функция, которую возвращает `emulate("device")`, снимает подмененный user agent, но исходный viewport не возвращает: вместо него ставится профиль Desktop Chrome — 1280 × 720, DPR 1. Поэтому в примере размер запоминается в `before` и после каждого теста возвращается через [`setViewport()`][set-viewport]. Замерять нужно на странице приложения: на `about:blank` значения будут другими. Сам `emulate("device")` по-прежнему вызывается до навигации. +В этом примере эмуляция не снимается и остается до конца сессии. Если после него в той же сессии идут другие тесты, сохраните функцию, которую вернул `emulate("device")`, и вызовите ее в том же тесте или в `afterEach`. + +Функция отката убирает подмену user agent и устанавливает viewport профиля Desktop Chrome — 1280 × 720 с DPR 1. Исходный размер viewport она не восстанавливает. Если следующим тестам нужен другой размер, после отката вызовите [`setViewport()`][set-viewport] с константой. ### User agent -Для user agent есть два разных сценария: клиентский код может читать `navigator.userAgent`, а сервер — HTTP-заголовок `User-Agent`. +User agent можно проверять в двух разных местах: клиентский код читает `navigator.userAgent`, а сервер получает HTTP-заголовок `User-Agent`. #### navigator.userAgent @@ -125,13 +102,16 @@ it("показывает инструкцию для iOS по navigator.userAgen await browser.url("/"); - await expect(browser.$("[data-testid='ios-install-guide']")).toBeDisplayed(); + const installGuide = await browser.getByTestId("ios-install-guide"); + await expect(installGuide).toBeDisplayed(); }); ``` +Эмуляция сохраняется до конца сессии, поэтому снимайте ее в том же тесте или в `afterEach`. После `restore()` новые страницы открываются уже без эмуляции. Страница, которая уже открыта, не меняется, пока ее не перезагрузить. Подробнее — в разделе [«Состояние и изоляция»](#state-and-isolation). + #### HTTP User-Agent -Если приложение определяет тип клиента на сервере по заголовку `User-Agent`, используйте [`browser.getPuppeteer()`][get-puppeteer] и Puppeteer [`page.setUserAgent()`][page-set-user-agent]. Подробнее о работе с Puppeteer и CDP — в разделе [«Как использовать Chrome DevTools Protocol в Testplane»][how-to-use-cdp]. +Если приложение определяет тип клиента на сервере по заголовку `User-Agent`, используйте [`browser.getPuppeteer()`][get-puppeteer] и Puppeteer [`page.setUserAgent()`][page-set-user-agent]. ```typescript it("передает мобильный User-Agent на сервер", async ({ browser }) => { @@ -151,13 +131,13 @@ it("передает мобильный User-Agent на сервер", async ({ ## Локаль -Язык, который приложение читает в `navigator.language` и в заголовке `Accept-Language`, и локаль, по которой `Intl` форматирует числа и даты, задаются по отдельности. Настройка `intl.accept_languages` меняет языковые предпочтения и не трогает `Intl`. [`Emulation.setLocaleOverride`][cdp-set-locale-override] меняет локаль `Intl`, но не языковые предпочтения браузера. +Языковые предпочтения браузера и локаль `Intl` задаются отдельно. Приложение получает языковые предпочтения из `navigator.language` и заголовка `Accept-Language`, а локаль `Intl` определяет формат чисел и дат. Настройка `intl.accept_languages` меняет только языковые предпочтения. Команда [`Emulation.setLocaleOverride`][cdp-set-locale-override], наоборот, меняет локаль `Intl`, но не языковые предпочтения браузера. -В Chrome на macOS аргумент запуска `--lang` принимается и молча игнорируется, браузер продолжает сообщать системный язык. +В Chrome на macOS аргумент запуска `--lang` не дает нужного эффекта: браузер принимает его без ошибки, но продолжает сообщать системный язык. ### Язык интерфейса -Если приложение выбирает язык по языковым предпочтениям браузера или заголовку `Accept-Language`, задайте `intl.accept_languages` в конфигурации браузера. +Если приложение выбирает язык по настройкам браузера или заголовку `Accept-Language`, задайте `intl.accept_languages` в конфигурации браузера. Для Chrome: @@ -193,19 +173,20 @@ browsers: { }, ``` -После этого тест может проверять интерфейс с нужной локалью: +После этого в тесте можно проверить интерфейс на нужном языке: ```typescript it("показывает интерфейс на немецком", async ({ browser }) => { await browser.url("/"); - await expect(browser.$("[data-testid='page-title']")).toHaveText("Bestellungen"); + const pageTitle = await browser.getByTestId("page-title"); + await expect(pageTitle).toHaveText("Bestellungen"); }); ``` ### Форматирование через `Intl` -Если приложение форматирует числа или даты через `Intl`, локаль `Intl` можно изменить через Puppeteer. +Если приложение форматирует числа или даты через `Intl`, измените локаль `Intl` через Puppeteer. Например, так можно проверить форматирование числа для немецкой локали: @@ -221,11 +202,12 @@ it("форматирует число для немецкой локали", asy await browser.url("/"); - await expect(browser.$("[data-testid='average-value']")).toHaveText("1.234,56"); + const averageValue = await browser.getByTestId("average-value"); + await expect(averageValue).toHaveText("1.234,56"); }); ``` -В этом сценарии приложение форматирует значение `1234.56` через `Intl.NumberFormat`, поэтому при локали `de-DE` оно отображается как `1.234,56`. +В этом примере приложение форматирует значение `1234.56` через `Intl.NumberFormat`. Для локали `de-DE` результат выглядит как `1.234,56`. ## Часовой пояс @@ -239,16 +221,23 @@ it("показывает время события в часовом поясе await page.emulateTimezone("America/New_York"); await browser.url("/"); - await expect(browser.$("[data-testid='event-time']")).toHaveText("07:00"); + const eventTime = await browser.getByTestId("event-time"); + await expect(eventTime).toHaveText("07:00"); }); ``` -В этом примере страница показывает время события `2024-09-04T11:00:00Z`, в зоне `America/New_York` это 07:00. +В этом примере время события — `2024-09-04T11:00:00Z`. В часовом поясе `America/New_York` страница показывает его как 07:00. + +Команды через CDP применяются и к уже открытой странице, поэтому часовой пояс можно изменить в середине теста. ## Время и таймеры Когда поведение интерфейса зависит от текущего времени или таймеров, используйте [`browser.emulate("clock")`][emulate-clock]. +По умолчанию `emulate("clock")` подменяет не только дату, но и `setTimeout`, `setInterval`, `requestAnimationFrame`, `performance` и другие API, связанные со временем. Таймеры становятся виртуальными и сами по себе не срабатывают: время продвигается только после вызова [`tick()`][clock-tick]. Если в тесте нужно изменить лишь дату, ограничьте подмену с помощью `toFake`. + +В отличие от остальных настроек `emulate()`, время можно подменить и на уже открытой странице. Вызов `clock.restore()` также восстанавливает время на текущей странице. + ### Фиксированное время Например, так можно проверить состояние страницы в определенный момент: @@ -257,12 +246,14 @@ it("показывает время события в часовом поясе it("показывает активную акцию в заданный период", async ({ browser }) => { const clock = await browser.emulate("clock", { now: new Date("2024-09-04T12:30:00Z"), + toFake: ["Date"], }); try { await browser.url("/"); - await expect(browser.$("[data-testid='promo-status']")).toHaveText("Акция началась"); + const promoStatus = await browser.getByTestId("promo-status"); + await expect(promoStatus).toHaveText("Акция началась"); } finally { await clock.restore(); } @@ -271,7 +262,7 @@ it("показывает активную акцию в заданный пер ### Таймеры -[`tick(ms)`][clock-tick] продвигает виртуальное время на указанное количество миллисекунд и запускает таймеры, которые должны сработать. +[`tick(ms)`][clock-tick] продвигает виртуальное время на указанное количество миллисекунд. При этом срабатывают таймеры, запланированные на этот промежуток. ```typescript it("скрывает уведомление через 5 секунд", async ({ browser }) => { @@ -284,7 +275,8 @@ it("скрывает уведомление через 5 секунд", async ({ await clock.tick(5000); - await expect(browser.$("[data-testid='notification']")).not.toBeDisplayed(); + const notification = await browser.getByTestId("notification"); + await expect(notification).not.toBeDisplayed(); } finally { await clock.restore(); } @@ -302,14 +294,14 @@ it("показывает изображение для темной цветов await browser.emulate("colorScheme", "dark"); await browser.url("/"); - await expect(browser.$("[data-testid='theme-image']")).toHaveAttribute( - "src", - "/images/night.svg", - ); + const themeLogo = await browser.getByTestId("theme-logo"); + await expect(themeLogo).toHaveAttribute("src", "/images/night.svg"); }); ``` -`browser.emulate("colorScheme")` меняет результат `matchMedia()` для `prefers-color-scheme`. Для проверки стилей, заданных через CSS `@media (prefers-color-scheme)`, используйте [`Emulation.setEmulatedMedia`][cdp-set-emulated-media]. +Эмуляция сохраняется до конца сессии, поэтому снимайте ее в том же тесте или в `afterEach`. После `restore()` новые страницы открываются уже без эмуляции. Страница, которая уже открыта, не меняется, пока ее не перезагрузить. Подробнее — в разделе [«Состояние и изоляция»](#state-and-isolation). + +`browser.emulate("colorScheme")` меняет результат `matchMedia()` для `prefers-color-scheme`, но не влияет на CSS. Чтобы проверить стили из `@media (prefers-color-scheme)`, используйте [`Emulation.setEmulatedMedia`][cdp-set-emulated-media]. ```typescript it("применяет стили для темной цветовой схемы", async ({ browser }) => { @@ -328,9 +320,8 @@ it("применяет стили для темной цветовой схем await browser.url("/"); - const background = await browser - .$("[data-testid='theme-box']") - .getCSSProperty("background-color"); + const themeBox = await browser.getByTestId("theme-box"); + const background = await themeBox.getCSSProperty("background-color"); expect(background.value).toBe("rgba(0,0,0,1)"); }); @@ -350,15 +341,17 @@ it("показывает сообщение при отсутствии сети await browser.throttleNetwork("offline"); - await browser.$("[data-testid='load-orders']").click(); + const loadOrders = await browser.getByTestId("load-orders"); + await loadOrders.click(); - await expect(browser.$("[data-testid='network-error']")).toHaveText("Нет подключения к сети"); + const networkError = await browser.findByTestId("network-error"); + await expect(networkError).toHaveText("Нет подключения к сети"); await browser.throttleNetwork("online"); }); ``` -Сначала загрузите страницу, а затем отключите сеть перед действием, которое отправляет запрос. В отличие от `emulate()`, эту команду нужно вызывать после навигации. Для возврата к обычному сетевому режиму используйте профиль `"online"`. +Сначала загрузите страницу, а затем отключите сеть перед действием, которое отправляет запрос. В отличие от `emulate()`, `throttleNetwork()` нужно вызывать после навигации. Чтобы вернуть обычный сетевой режим, используйте профиль `"online"`. ### Медленное соединение @@ -375,17 +368,19 @@ it("показывает состояние загрузки при медлен uploadThroughput: (20 * 1024) / 8, }); - await browser.$("[data-testid='load-orders']").click(); + const loadOrders = await browser.getByTestId("load-orders"); + await loadOrders.click(); - await expect(browser.$("[data-testid='loading']")).toBeDisplayed(); + const loading = await browser.findByTestId("loading"); + await expect(loading).toBeDisplayed(); await browser.throttleNetwork("online"); }); ``` -В объекте четыре поля: `offline`, `latency` в миллисекундах и `downloadThroughput` / `uploadThroughput` — скорость в байтах в секунду. +Объект содержит четыре поля: `offline`, задержку `latency` в миллисекундах, а также скорости загрузки и отправки данных `downloadThroughput` и `uploadThroughput` в байтах в секунду. -Для типовых условий объект не нужен: можно передать имя профиля, например `"Good3G"` или `"offline"`. +Для типовых условий параметры можно не задавать вручную. Вместо объекта передайте имя готового профиля, например `"Good3G"` или `"offline"`. ### navigator.onLine @@ -396,17 +391,20 @@ it("показывает офлайн-режим", async ({ browser }) => { await browser.emulate("onLine", false); await browser.url("/"); - await expect(browser.$("[data-testid='connection-status']")).toHaveText("Офлайн"); + const connectionStatus = await browser.getByTestId("connection-status"); + await expect(connectionStatus).toHaveText("Офлайн"); }); ``` -`browser.emulate("onLine", false)` меняет значение `navigator.onLine`, но не отключает сеть: HTTP-запросы продолжают выполняться. +Эмуляция сохраняется до конца сессии, поэтому снимайте ее в том же тесте или в `afterEach`. После `restore()` новые страницы открываются уже без эмуляции. Страница, которая уже открыта, не меняется, пока ее не перезагрузить. Подробнее — в разделе [«Состояние и изоляция»](#state-and-isolation). + +`browser.emulate("onLine", false)` только меняет значение `navigator.onLine`, но не отключает сеть: HTTP-запросы продолжают выполняться. ## Производительность CPU Для проверки интерфейса при ограниченной производительности процессора используйте [`browser.throttleCPU()`][throttle-cpu]. -Например, так можно запустить сценарий с четырехкратным замедлением CPU: +Например, так можно проверить сценарий при четырехкратном замедлении CPU: ```typescript it("работает при замедленном CPU", async ({ browser }) => { @@ -420,7 +418,7 @@ it("работает при замедленном CPU", async ({ browser }) => }); ``` -Чем больше коэффициент, тем сильнее замедляется выполнение. Значение `1` отключает throttling. +Чем больше коэффициент, тем медленнее выполняется код. Значение `1` отключает замедление. ## Геолокация @@ -437,13 +435,14 @@ it("показывает ближайший пункт выдачи", async ({ b await browser.url("/"); - await expect(browser.$("[data-testid='nearest-point']")).toHaveText( - "Пункт выдачи на Alexanderplatz", - ); + const nearestPoint = await browser.findByTestId("nearest-point"); + await expect(nearestPoint).toHaveText("Пункт выдачи на Alexanderplatz"); }); ``` -`browser.emulate("geolocation")` подменяет координаты, которые приложение получает через `navigator.geolocation.getCurrentPosition()`, для этого не требуется отдельно настраивать разрешение на геолокацию. +Эмуляция сохраняется до конца сессии, поэтому снимайте ее в том же тесте или в `afterEach`. После `restore()` новые страницы открываются уже без эмуляции. Страница, которая уже открыта, не меняется, пока ее не перезагрузить. Подробнее — в разделе [«Состояние и изоляция»](#state-and-isolation). + +`browser.emulate("geolocation")` подменяет координаты, которые приложение получает через `navigator.geolocation.getCurrentPosition()`. Отдельно настраивать разрешение на геолокацию не нужно. ## Разрешения браузера @@ -462,15 +461,15 @@ it("показывает статус уведомлений", async ({ browser "granted", ); - await browser.$("[data-testid='check-notifications']").click(); + const checkNotifications = await browser.getByTestId("check-notifications"); + await checkNotifications.click(); - await expect(browser.$("[data-testid='notification-status']")).toHaveText( - "Уведомления включены", - ); + const notificationStatus = await browser.findByTestId("notification-status"); + await expect(notificationStatus).toHaveText("Уведомления включены"); }); ``` -Вызывайте `browser.setPermissions()` после перехода на страницу приложения: разрешение привязывается к адресу открытой страницы, а до навигации она будет пустой, и команда упадет с ошибкой. +Вызывайте `browser.setPermissions()` после перехода на страницу приложения. Разрешение привязывается к адресу текущей страницы. До навигации открыта пустая страница, поэтому команда завершится с ошибкой. ## JavaScript @@ -493,6 +492,8 @@ browsers: { }, ``` +Значение `1` в `profile.managed_default_content_settings.javascript` разрешает JavaScript, а `2` блокирует его. + Для Firefox: ```typescript @@ -510,7 +511,9 @@ browsers: { }, ``` -После этого тест запускается сразу в браузере с отключенным JavaScript: +В Firefox `javascript.enabled` принимает логическое значение, а не число. + +После этого тест сразу запускается в браузере с отключенным JavaScript: ```typescript it("показывает содержимое без JavaScript", async ({ browser }) => { @@ -520,11 +523,11 @@ it("показывает содержимое без JavaScript", async ({ brows }); ``` -## Состояние и изоляция +## Состояние и изоляция {/* #state-and-isolation */} -Некоторые настройки среды сохраняются в рамках WebDriver-сессии и могут повлиять на следующие тесты. +Некоторые настройки среды сохраняются до конца WebDriver-сессии и могут повлиять на следующие тесты. -Снимайте эмуляцию в том же тесте, где ее включили, или в `afterEach`. Вызов [`restore()`][restore] из следующего теста уже не сработает: у нового теста другой объект `browser`. +Снимайте эмуляцию в том же тесте, где ее включили, или в `afterEach`. Не откладывайте [`restore()`][restore] до следующего теста: он получит другой объект `browser`, и команда уже не сработает. Для профиля устройства используйте функцию, которую вернул `emulate("device")`, потому что [`restore("device")`][restore] не поддерживается. Если одна и та же эмуляция нужна в нескольких тестах, задавайте и снимайте ее в хуках: @@ -546,7 +549,7 @@ describe("темная цветовая схема", () => { }); ``` -Для настроек, которые должны действовать всю сессию, используйте отдельную конфигурацию браузера. Например, так удобнее задавать язык браузера, запускать тесты с отключенным JavaScript или фиксировать размер окна опцией [`windowSize`][window-size]. +Если настройка должна действовать всю сессию, создайте для нее отдельную конфигурацию браузера. Так удобнее задавать язык браузера, запускать тесты с отключенным JavaScript и фиксировать размер окна с помощью [`windowSize`][window-size]. [emulate]: https://webdriver.io/docs/api/browser/emulate [webdriver-bidi]: https://w3c.github.io/webdriver-bidi/ @@ -570,3 +573,4 @@ describe("темная цветовая схема", () => { [emulate-geolocation]: https://webdriver.io/docs/emulation#geolocation [set-permissions]: https://webdriver.io/docs/api/webdriver#setpermissions [restore]: https://webdriver.io/docs/api/browser/restore +[testing-library-guide]: ../guides/how-to-add-testing-library.mdx From d1c2cffc2b4a9b9ea86b448ce718f4a1e40d352d Mon Sep 17 00:00:00 2001 From: kseniataranov Date: Fri, 18 Sep 2026 16:16:48 +0300 Subject: [PATCH 4/4] docs(emulation): add English user environment guide --- .../user-environment-emulation.mdx | 577 +++++++++++++++++- 1 file changed, 575 insertions(+), 2 deletions(-) diff --git a/docs/basic-guides/user-environment-emulation.mdx b/docs/basic-guides/user-environment-emulation.mdx index e725cc09..d401a8df 100644 --- a/docs/basic-guides/user-environment-emulation.mdx +++ b/docs/basic-guides/user-environment-emulation.mdx @@ -1,3 +1,576 @@ -# User Environment Emulation +import Admonition from "@theme/Admonition"; -Draft +# Emulating the user environment + + + +- How to emulate a device and viewport, color scheme, time, geolocation, and permissions +- How to set the interface language and disable JavaScript +- How to test your application on a slow network and a throttled CPU + + + +## Introduction + +The user environment affects how an application looks and behaves. The same interface may behave differently on a mobile screen, with a different locale, in dark mode, without access to geolocation, or over a slow connection. + +In Testplane, some environment settings can be changed directly during a test, while others must be set in the browser configuration beforehand. + +The [`browser.emulate()`][emulate] and [`setViewport()`][set-viewport] commands require [WebDriver BiDi][webdriver-bidi]. Enable `webSocketUrl` in the browser configuration: + +```typescript +browsers: { + chrome: { + desiredCapabilities: { + browserName: "chrome", + webSocketUrl: true, + }, + }, +}, +``` + +In the same Chrome session with `webSocketUrl: true`, you can use both [`browser.emulate()`][emulate] and commands that work through the [Chrome DevTools Protocol][how-to-use-cdp]: [`getPuppeteer()`][get-puppeteer], [`throttleNetwork()`][throttle-network], and [`throttleCPU()`][throttle-cpu]. You do not need a separate configuration without BiDi for these commands. + +The minimum versions with BiDi support are Chrome 128 and Firefox 119. + +In most cases, the browser applies `emulate()` settings when a page is opened. Therefore, call the command first and then navigate to the page you need. Any exceptions are noted in the relevant sections. + +To locate elements using `getByTestId` and `findByTestId`, as shown in this article, install and set up [@testplane/testing-library][testing-library-guide]. These commands are unavailable when JavaScript is disabled, so that test uses `$()` instead. + + + +`throttleNetwork()`, `throttleCPU()`, and commands invoked via `getPuppeteer()` rely on the [Chrome DevTools Protocol][how-to-use-cdp] and are available only in Chromium. + + + +## Screen and device + +### Viewport + +[`setViewport()`][set-viewport] sets the size of the page viewport. Use this command to test responsive layouts and interface behavior at different breakpoints. To resize the entire browser window rather than the viewport, use [`setWindowSize()`][set-window-size]. + +```typescript +it("shows mobile navigation on a narrow screen", async ({ browser }) => { + await browser.setViewport({ + width: 390, + height: 844, + }); + + await browser.url("/"); + + const mobileMenu = await browser.getByTestId("mobile-menu"); + await expect(mobileMenu).toBeDisplayed(); +}); +``` + +The size set through `setViewport()` persists until the end of the session. There is no separate command to restore it. + +### Device profile + +[`emulate("device")`][emulate-device] applies a predefined device profile: viewport, DPR, and `navigator.userAgent`. The viewport changes immediately, while the user agent changes only in documents created after the command is called. Therefore, enable emulation first and then navigate to the page you need. + +A device profile overrides the user agent and dimensions, but it does not turn a desktop browser into a mobile browser. For example, the `iPhone 15` descriptor includes the `isMobile` and `hasTouch` parameters, but the command does not apply them. Touch events and `navigator.maxTouchPoints` are not emulated, the browser engine remains Chromium rather than WebKit/iOS, and the system fonts, on-screen keyboard, address bar, and performance do not change. Therefore, this emulation does not replace testing on a real device. + +```typescript +it("shows the iOS install instructions when using the iPhone 15 profile", async ({ browser }) => { + await browser.emulate("device", "iPhone 15"); + await browser.url("/"); + + const installGuide = await browser.getByTestId("ios-install-guide"); + await expect(installGuide).toBeDisplayed(); +}); +``` + +In this example, emulation is not restored and remains active until the end of the session. If other tests run in the same session afterward, save the function returned by `emulate("device")` and call it in the same test or in `afterEach`. + +The restore function removes the user agent override and sets the viewport to the Desktop Chrome profile: 1280 × 720 with DPR 1. It does not restore the original viewport size. If subsequent tests require a different size, call [`setViewport()`][set-viewport] with a constant after restoring the profile. + +### User agent + +The user agent appears in two places: client-side code reads `navigator.userAgent`, and the server receives the `User-Agent` HTTP header. + +#### navigator.userAgent + +[`emulate("userAgent")`][emulate-user-agent] changes the value available to client-side JavaScript through `navigator.userAgent`. + +```typescript +it("shows the iOS instructions based on navigator.userAgent", async ({ browser }) => { + await browser.emulate( + "userAgent", + "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", + ); + + await browser.url("/"); + + const installGuide = await browser.getByTestId("ios-install-guide"); + await expect(installGuide).toBeDisplayed(); +}); +``` + +The emulation persists until the end of the session, so restore it in the same test or in `afterEach`. After `restore()`, new pages open without emulation. A page that is already open does not change until it is reloaded. For details, see [State and isolation](#state-and-isolation). + +#### HTTP User-Agent + +If the application determines the client type on the server from the `User-Agent` header, use [`browser.getPuppeteer()`][get-puppeteer] and Puppeteer's [`page.setUserAgent()`][page-set-user-agent]. + +```typescript +it("sends a mobile User-Agent to the server", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + + await page.setUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", + ); + + await browser.url("/"); + // ... +}); +``` + +`page.setUserAgent()` changes the HTTP `User-Agent` and `navigator.userAgent` at the same time. + +## Locale + +Browser language preferences and the `Intl` locale are configured separately. The application obtains language preferences from `navigator.language` and the `Accept-Language` header, while the `Intl` locale determines the formatting of numbers and dates. The `intl.accept_languages` setting changes only the language preferences. Conversely, the [`Emulation.setLocaleOverride`][cdp-set-locale-override] command changes the `Intl` locale but does not change the browser's language preferences. + +In Chrome on macOS, the `--lang` launch argument does not produce the desired effect: the browser accepts it without an error but continues to report the system language. + +### Interface language + +If the application selects a language based on browser settings or the `Accept-Language` header, set `intl.accept_languages` in the browser configuration. + +For Chrome: + +```typescript +browsers: { + "chrome-de": { + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + prefs: { + "intl.accept_languages": "de-DE,de", + }, + }, + }, + }, +}, +``` + +For Firefox: + +```typescript +browsers: { + "firefox-de": { + desiredCapabilities: { + browserName: "firefox", + "moz:firefoxOptions": { + prefs: { + "intl.accept_languages": "de-DE,de", + }, + }, + }, + }, +}, +``` + +You can then verify the interface in the desired language: + +```typescript +it("shows the interface in German", async ({ browser }) => { + await browser.url("/"); + + const pageTitle = await browser.getByTestId("page-title"); + await expect(pageTitle).toHaveText("Bestellungen"); +}); +``` + +### Formatting with `Intl` + +If the application formats numbers or dates through `Intl`, change the `Intl` locale through Puppeteer. + +For example, you can test number formatting for the German locale as follows: + +```typescript +it("formats a number for the German locale", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + const client = await page.target().createCDPSession(); + + await client.send("Emulation.setLocaleOverride", { + locale: "de-DE", + }); + + await browser.url("/"); + + const averageValue = await browser.getByTestId("average-value"); + await expect(averageValue).toHaveText("1.234,56"); +}); +``` + +In this example, the application formats the value `1234.56` through `Intl.NumberFormat`. For the `de-DE` locale, the result is `1.234,56`. + +## Time zone + +If the way dates and times are displayed depends on the user's time zone, set the desired time zone using Puppeteer's [`page.emulateTimezone()`][page-emulate-timezone]. + +```typescript +it("shows the event time in the user's time zone", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + + await page.emulateTimezone("America/New_York"); + await browser.url("/"); + + const eventTime = await browser.getByTestId("event-time"); + await expect(eventTime).toHaveText("07:00"); +}); +``` + +In this example, the event time is `2024-09-04T11:00:00Z`. In the `America/New_York` time zone, the page displays it as 07:00. + +Commands that use CDP also apply to an already open page, so you can change the time zone in the middle of a test. + +## Time and timers + +When interface behavior depends on the current time or timers, use [`browser.emulate("clock")`][emulate-clock]. + +By default, `emulate("clock")` overrides not only the date but also `setTimeout`, `setInterval`, `requestAnimationFrame`, `performance`, and other time-related APIs. Timers become virtual and do not fire on their own: time advances only after [`tick()`][clock-tick] is called. If you only need to change the date in a test, limit the override using `toFake`. + +Unlike other `emulate()` settings, time can also be overridden on an already open page. Calling `clock.restore()` also restores time on the current page. + +### Fixed time + +For example, you can test the state of a page at a specific time as follows: + +```typescript +it("shows an active promotion during the specified period", async ({ browser }) => { + const clock = await browser.emulate("clock", { + now: new Date("2024-09-04T12:30:00Z"), + toFake: ["Date"], + }); + + try { + await browser.url("/"); + + const promoStatus = await browser.getByTestId("promo-status"); + await expect(promoStatus).toHaveText("Promotion started"); + } finally { + await clock.restore(); + } +}); +``` + +### Timers + +[`tick(ms)`][clock-tick] advances virtual time by the specified number of milliseconds. Any timers scheduled within that interval fire as time advances. + +```typescript +it("hides the notification after 5 seconds", async ({ browser }) => { + const clock = await browser.emulate("clock", { + now: new Date("2024-09-04T12:30:00Z"), + }); + + try { + await browser.url("/"); + + await clock.tick(5000); + + const notification = await browser.getByTestId("notification"); + await expect(notification).not.toBeDisplayed(); + } finally { + await clock.restore(); + } +}); +``` + +## Color scheme + +If the application determines the color scheme through `window.matchMedia()`, use [`browser.emulate("colorScheme")`][emulate-color-scheme]. + +For example, you can test which image is selected for the dark color scheme as follows: + +```typescript +it("shows the image for the dark color scheme", async ({ browser }) => { + await browser.emulate("colorScheme", "dark"); + await browser.url("/"); + + const themeLogo = await browser.getByTestId("theme-logo"); + await expect(themeLogo).toHaveAttribute("src", "/images/night.svg"); +}); +``` + +The emulation persists until the end of the session, so restore it in the same test or in `afterEach`. After `restore()`, new pages open without emulation. A page that is already open does not change until it is reloaded. For details, see [State and isolation](#state-and-isolation). + +`browser.emulate("colorScheme")` changes the result of `matchMedia()` for `prefers-color-scheme` but does not affect CSS. To test styles from `@media (prefers-color-scheme)`, use [`Emulation.setEmulatedMedia`][cdp-set-emulated-media]. + +```typescript +it("applies styles for the dark color scheme", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + const client = await page.target().createCDPSession(); + + await client.send("Emulation.setEmulatedMedia", { + features: [ + { + name: "prefers-color-scheme", + value: "dark", + }, + ], + }); + + await browser.url("/"); + + const themeBox = await browser.getByTestId("theme-box"); + const background = await themeBox.getCSSProperty("background-color"); + + expect(background.value).toBe("rgba(0,0,0,1)"); +}); +``` + +## Network + +### Offline mode + +To test how the application works without a network connection, use [`browser.throttleNetwork("offline")`][throttle-network]. + +For example, you can test the error message displayed when a network request fails: + +```typescript +it("shows a message when there is no network connection", async ({ browser }) => { + await browser.url("/"); + + await browser.throttleNetwork("offline"); + + const loadOrders = await browser.getByTestId("load-orders"); + await loadOrders.click(); + + const networkError = await browser.findByTestId("network-error"); + await expect(networkError).toHaveText("No network connection"); + + await browser.throttleNetwork("online"); +}); +``` + +Load the page first, then disable the network before the action that sends a request. Unlike `emulate()`, `throttleNetwork()` must be called after navigation. To restore the normal network mode, use the `"online"` profile. + +### Slow connection + +To test the interface over a slow connection, pass network parameters to [`browser.throttleNetwork()`][throttle-network]: + +```typescript +it("shows the loading state on a slow network", async ({ browser }) => { + await browser.url("/orders"); + + await browser.throttleNetwork({ + offline: false, + latency: 500, + downloadThroughput: (50 * 1024) / 8, + uploadThroughput: (20 * 1024) / 8, + }); + + const loadOrders = await browser.getByTestId("load-orders"); + await loadOrders.click(); + + const loading = await browser.findByTestId("loading"); + await expect(loading).toBeDisplayed(); + + await browser.throttleNetwork("online"); +}); +``` + +The object contains four fields: `offline`, the `latency` in milliseconds, and the `downloadThroughput` and `uploadThroughput` rates in bytes per second. + +For common network conditions, you do not have to specify the parameters manually. Pass the name of a predefined profile instead of an object, such as `"Good3G"` or `"offline"`. + +### navigator.onLine + +If the application determines the connection state using `navigator.onLine`, use [`browser.emulate("onLine")`][emulate-online]: + +```typescript +it("shows offline mode", async ({ browser }) => { + await browser.emulate("onLine", false); + await browser.url("/"); + + const connectionStatus = await browser.getByTestId("connection-status"); + await expect(connectionStatus).toHaveText("Offline"); +}); +``` + +The emulation persists until the end of the session, so restore it in the same test or in `afterEach`. After `restore()`, new pages open without emulation. A page that is already open does not change until it is reloaded. For details, see [State and isolation](#state-and-isolation). + +`browser.emulate("onLine", false)` only changes the value of `navigator.onLine`; it does not disable the network, and HTTP requests continue to work. + +## CPU performance + +To test the interface with limited CPU performance, use [`browser.throttleCPU()`][throttle-cpu]. + +For example, you can test a scenario with 4× CPU slowdown as follows: + +```typescript +it("works with a throttled CPU", async ({ browser }) => { + await browser.throttleCPU(4); + + await browser.url("/"); + + // ... + + await browser.throttleCPU(1); +}); +``` + +The higher the slowdown factor, the slower the code runs. A value of `1` disables throttling. + +## Geolocation + +If the application uses the user's coordinates, set them through [`browser.emulate("geolocation")`][emulate-geolocation]. + +For example, you can test the search for the nearest pickup point for a user in Berlin as follows: + +```typescript +it("shows the nearest pickup point", async ({ browser }) => { + await browser.emulate("geolocation", { + latitude: 52.52, + longitude: 13.405, + }); + + await browser.url("/"); + + const nearestPoint = await browser.findByTestId("nearest-point"); + await expect(nearestPoint).toHaveText("Pickup point at Alexanderplatz"); +}); +``` + +The emulation persists until the end of the session, so restore it in the same test or in `afterEach`. After `restore()`, new pages open without emulation. A page that is already open does not change until it is reloaded. For details, see [State and isolation](#state-and-isolation). + +`browser.emulate("geolocation")` overrides the coordinates that the application obtains through `navigator.geolocation.getCurrentPosition()`. You do not need to configure the geolocation permission separately. + +## Browser permissions + +If application behavior depends on browser permissions, use [`browser.setPermissions()`][set-permissions]. + +For example, you can test the notification status as follows: + +```typescript +it("shows the notification status", async ({ browser }) => { + await browser.url("/"); + + await browser.setPermissions( + { + name: "notifications", + }, + "granted", + ); + + const checkNotifications = await browser.getByTestId("check-notifications"); + await checkNotifications.click(); + + const notificationStatus = await browser.findByTestId("notification-status"); + await expect(notificationStatus).toHaveText("Notifications enabled"); +}); +``` + +Call `browser.setPermissions()` after navigating to the application page. The permission is associated with the current page's address. Before navigation, a blank page is open, so the command fails. + +## JavaScript + +To test how a page works without JavaScript, disable it in the browser configuration. + +For Chrome: + +```typescript +browsers: { + "chrome-no-js": { + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + prefs: { + "profile.managed_default_content_settings.javascript": 2, + }, + }, + }, + }, +}, +``` + +A value of `1` for `profile.managed_default_content_settings.javascript` allows JavaScript, while `2` blocks it. + +For Firefox: + +```typescript +browsers: { + "firefox-no-js": { + desiredCapabilities: { + browserName: "firefox", + "moz:firefoxOptions": { + prefs: { + "javascript.enabled": false, + }, + }, + }, + }, +}, +``` + +In Firefox, `javascript.enabled` takes a Boolean value rather than a number. + +The test then starts directly in a browser with JavaScript disabled: + +```typescript +it("shows the content without JavaScript", async ({ browser }) => { + await browser.url("/"); + + await expect(browser.$("[data-testid='no-js-message']")).toBeDisplayed(); +}); +``` + +## State and isolation {/* #state-and-isolation */} + +Some environment settings persist until the end of the WebDriver session and may affect subsequent tests. + +Restore emulation in the same test where you enabled it or in `afterEach`. Do not postpone [`restore()`][restore] until the next test: that test receives a different `browser` object, and the command no longer works. For a device profile, use the function returned by `emulate("device")`, because [`restore("device")`][restore] is not supported. + +If the same emulation is required in multiple tests, enable and restore it in hooks: + +```typescript +describe("dark color scheme", () => { + beforeEach(async ({ browser }) => { + await browser.emulate("colorScheme", "dark"); + }); + + afterEach(async ({ browser }) => { + await browser.restore("colorScheme"); + }); + + it("shows the image for the dark color scheme", async ({ browser }) => { + await browser.url("/"); + + // ... + }); +}); +``` + +If a setting must remain active for the entire session, create a separate browser configuration for it. This approach is more convenient for setting the browser language, running tests with JavaScript disabled, and setting a fixed window size with [`windowSize`][window-size]. + +[emulate]: https://webdriver.io/docs/api/browser/emulate +[webdriver-bidi]: https://w3c.github.io/webdriver-bidi/ +[set-viewport]: https://webdriver.io/docs/api/browser/setViewport +[set-window-size]: ../commands/browser/setWindowSize.mdx +[window-size]: ../reference/config/browsers.mdx#window_size +[how-to-use-cdp]: ../guides/how-to-use-cdp.mdx +[emulate-device]: https://webdriver.io/docs/emulation#device +[emulate-user-agent]: https://webdriver.io/docs/emulation#user-agent +[get-puppeteer]: ../commands/browser/getPuppeteer.mdx +[page-set-user-agent]: https://pptr.dev/api/puppeteer.page.setuseragent +[cdp-set-locale-override]: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setLocaleOverride +[page-emulate-timezone]: https://pptr.dev/api/puppeteer.page.emulatetimezone +[emulate-clock]: https://webdriver.io/docs/emulation#clock +[clock-tick]: https://webdriver.io/docs/api/clock/tick +[emulate-color-scheme]: https://webdriver.io/docs/emulation#color-scheme +[cdp-set-emulated-media]: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setEmulatedMedia +[throttle-network]: https://webdriver.io/docs/api/browser/throttleNetwork +[emulate-online]: https://webdriver.io/docs/emulation#online-property +[throttle-cpu]: https://webdriver.io/docs/api/browser/throttleCPU +[emulate-geolocation]: https://webdriver.io/docs/emulation#geolocation +[set-permissions]: https://webdriver.io/docs/api/webdriver#setpermissions +[restore]: https://webdriver.io/docs/api/browser/restore +[testing-library-guide]: ../guides/how-to-add-testing-library.mdx