diff --git a/docs/basic-guides/user-environment-emulation.mdx b/docs/basic-guides/user-environment-emulation.mdx new file mode 100644 index 0000000..e725cc0 --- /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 0000000..a9f8db6 --- /dev/null +++ b/i18n/ru/docusaurus-plugin-content-docs/current/basic-guides/user-environment-emulation.mdx @@ -0,0 +1,572 @@ +import Admonition from "@theme/Admonition"; + +# Эмуляция среды пользователя + + + +- Как эмулировать устройство и viewport, цветовую схему, время, геолокацию, разрешения +- Как задать язык интерфейса и отключенный JavaScript +- Как проверить поведение приложения при медленной сети и слабом CPU + + + +## Введение + +Пользовательская среда влияет на то, как приложение выглядит и работает. Один и тот же интерфейс может по-разному вести себя на мобильном экране, при другой локали, в темной теме, без доступа к геолокации или при медленном соединении. + +В Testplane часть таких условий можно менять во время теста, а часть — задавать в настройках браузера. + +Для команд [`browser.emulate()`][emulate] и [`setViewport()`][set-viewport] требуется [WebDriver BiDi][webdriver-bidi]. В конфигурации браузера включите `webSocketUrl`: + +```typescript +browsers: { + chrome: { + desiredCapabilities: { + browserName: "chrome", + webSocketUrl: true, + }, + }, +}, +``` + +В одной 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. + + + +`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(); +}); +``` + +Размер, который задает `setViewport()`, действует до конца сессии, отдельной команды отката нет. + +### Профиль устройства + +[`emulate("device")`][emulate-device] применяет готовый профиль устройства: viewport, DPR и `navigator.userAgent`. + +Например, несколько тестов для iPhone можно объединить одним профилем: + +```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("/"); + + await expect(browser.$("[data-testid='ios-install-guide']")).toBeDisplayed(); + }); +}); +``` + +Функция, которую возвращает `emulate("device")`, снимает подмененный user agent, но исходный viewport не возвращает: вместо него ставится профиль Desktop Chrome — 1280 × 720, DPR 1. Поэтому в примере размер запоминается в `before` и после каждого теста возвращается через [`setViewport()`][set-viewport]. Замерять нужно на странице приложения: на `about:blank` значения будут другими. Сам `emulate("device")` по-прежнему вызывается до навигации. + +### User agent + +Для user agent есть два разных сценария: клиентский код может читать `navigator.userAgent`, а сервер — HTTP-заголовок `User-Agent`. + +#### navigator.userAgent + +[`emulate("userAgent")`][emulate-user-agent] меняет значение, доступное клиентскому JavaScript через `navigator.userAgent`. + +```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", + ); + + await browser.url("/"); + + await expect(browser.$("[data-testid='ios-install-guide']")).toBeDisplayed(); +}); +``` + +#### 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]. + +```typescript +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("/"); + // ... +}); +``` + +`page.setUserAgent()` меняет HTTP `User-Agent` и одновременно меняет `navigator.userAgent`. + +## Локаль + +Язык, который приложение читает в `navigator.language` и в заголовке `Accept-Language`, и локаль, по которой `Intl` форматирует числа и даты, задаются по отдельности. Настройка `intl.accept_languages` меняет языковые предпочтения и не трогает `Intl`. [`Emulation.setLocaleOverride`][cdp-set-locale-override] меняет локаль `Intl`, но не языковые предпочтения браузера. + +В Chrome на macOS аргумент запуска `--lang` принимается и молча игнорируется, браузер продолжает сообщать системный язык. + +### Язык интерфейса + +Если приложение выбирает язык по языковым предпочтениям браузера или заголовку `Accept-Language`, задайте `intl.accept_languages` в конфигурации браузера. + +Для Chrome: + +```typescript +browsers: { + "chrome-de": { + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + prefs: { + "intl.accept_languages": "de-DE,de", + }, + }, + }, + }, +}, +``` + +Для 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. + +Например, так можно проверить форматирование числа для немецкой локали: + +```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 browser.url("/"); + + await expect(browser.$("[data-testid='average-value']")).toHaveText("1.234,56"); +}); +``` + +В этом сценарии приложение форматирует значение `1234.56` через `Intl.NumberFormat`, поэтому при локали `de-DE` оно отображается как `1.234,56`. + +## Часовой пояс + +Если отображение дат и времени зависит от часового пояса пользователя, задайте нужный часовой пояс через Puppeteer [`page.emulateTimezone()`][page-emulate-timezone]. + +```typescript +it("показывает время события в часовом поясе пользователя", async ({ browser }) => { + const puppeteer = await browser.getPuppeteer(); + const [page] = await puppeteer.pages(); + + await page.emulateTimezone("America/New_York"); + await browser.url("/"); + + await expect(browser.$("[data-testid='event-time']")).toHaveText("07:00"); +}); +``` + +В этом примере страница показывает время события `2024-09-04T11:00:00Z`, в зоне `America/New_York` это 07:00. + +## Время и таймеры + +Когда поведение интерфейса зависит от текущего времени или таймеров, используйте [`browser.emulate("clock")`][emulate-clock]. + +### Фиксированное время + +Например, так можно проверить состояние страницы в определенный момент: + +```typescript +it("показывает активную акцию в заданный период", async ({ browser }) => { + const clock = await browser.emulate("clock", { + now: new Date("2024-09-04T12:30:00Z"), + }); + + try { + await browser.url("/"); + + await expect(browser.$("[data-testid='promo-status']")).toHaveText("Акция началась"); + } finally { + await clock.restore(); + } +}); +``` + +### Таймеры + +[`tick(ms)`][clock-tick] продвигает виртуальное время на указанное количество миллисекунд и запускает таймеры, которые должны сработать. + +```typescript +it("скрывает уведомление через 5 секунд", async ({ browser }) => { + const clock = await browser.emulate("clock", { + now: new Date("2024-09-04T12:30:00Z"), + }); + + try { + await browser.url("/"); + + await clock.tick(5000); + + await expect(browser.$("[data-testid='notification']")).not.toBeDisplayed(); + } finally { + await clock.restore(); + } +}); +``` + +## Цветовая схема + +Если приложение определяет цветовую схему через `window.matchMedia()`, используйте [`browser.emulate("colorScheme")`][emulate-color-scheme]. + +Например, так можно проверить выбор изображения для темной цветовой схемы: + +```typescript +it("показывает изображение для темной цветовой схемы", async ({ browser }) => { + await browser.emulate("colorScheme", "dark"); + await browser.url("/"); + + await expect(browser.$("[data-testid='theme-image']")).toHaveAttribute( + "src", + "/images/night.svg", + ); +}); +``` + +`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(); + + await client.send("Emulation.setEmulatedMedia", { + features: [ + { + name: "prefers-color-scheme", + value: "dark", + }, + ], + }); + + await browser.url("/"); + + const background = await browser + .$("[data-testid='theme-box']") + .getCSSProperty("background-color"); + + expect(background.value).toBe("rgba(0,0,0,1)"); +}); +``` + +## Сеть + +### Отсутствие сети + +Для проверки работы приложения без сети используйте [`browser.throttleNetwork("offline")`][throttle-network]. + +Например, так можно проверить сообщение об ошибке при сетевом запросе: + +```typescript +it("показывает сообщение при отсутствии сети", async ({ browser }) => { + await browser.url("/"); + + await browser.throttleNetwork("offline"); + + await browser.$("[data-testid='load-orders']").click(); + + await expect(browser.$("[data-testid='network-error']")).toHaveText("Нет подключения к сети"); + + await browser.throttleNetwork("online"); +}); +``` + +Сначала загрузите страницу, а затем отключите сеть перед действием, которое отправляет запрос. В отличие от `emulate()`, эту команду нужно вызывать после навигации. Для возврата к обычному сетевому режиму используйте профиль `"online"`. + +### Медленное соединение + +Для проверки интерфейса при медленном соединении передайте параметры сети в [`browser.throttleNetwork()`][throttle-network]: + +```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(); + + await browser.throttleNetwork("online"); +}); +``` + +В объекте четыре поля: `offline`, `latency` в миллисекундах и `downloadThroughput` / `uploadThroughput` — скорость в байтах в секунду. + +Для типовых условий объект не нужен: можно передать имя профиля, например `"Good3G"` или `"offline"`. + +### navigator.onLine + +Если приложение определяет состояние подключения по `navigator.onLine`, используйте [`browser.emulate("onLine")`][emulate-online]: + +```typescript +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 + +Для проверки интерфейса при ограниченной производительности процессора используйте [`browser.throttleCPU()`][throttle-cpu]. + +Например, так можно запустить сценарий с четырехкратным замедлением CPU: + +```typescript +it("работает при замедленном CPU", async ({ browser }) => { + await browser.throttleCPU(4); + + await browser.url("/"); + + // ... + + await browser.throttleCPU(1); +}); +``` + +Чем больше коэффициент, тем сильнее замедляется выполнение. Значение `1` отключает throttling. + +## Геолокация + +Если приложение использует координаты пользователя, задайте их через [`browser.emulate("geolocation")`][emulate-geolocation]. + +Например, так можно проверить поиск ближайшего пункта выдачи для пользователя в Берлине: + +```typescript +it("показывает ближайший пункт выдачи", async ({ browser }) => { + await browser.emulate("geolocation", { + latitude: 52.52, + longitude: 13.405, + }); + + await browser.url("/"); + + await expect(browser.$("[data-testid='nearest-point']")).toHaveText( + "Пункт выдачи на Alexanderplatz", + ); +}); +``` + +`browser.emulate("geolocation")` подменяет координаты, которые приложение получает через `navigator.geolocation.getCurrentPosition()`, для этого не требуется отдельно настраивать разрешение на геолокацию. + +## Разрешения браузера + +Если поведение приложения зависит от разрешений браузера, используйте [`browser.setPermissions()`][set-permissions]. + +Например, так можно проверить статус уведомлений: + +```typescript +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( + "Уведомления включены", + ); +}); +``` + +Вызывайте `browser.setPermissions()` после перехода на страницу приложения: разрешение привязывается к адресу открытой страницы, а до навигации она будет пустой, и команда упадет с ошибкой. + +## JavaScript + +Если нужно проверить работу страницы без JavaScript, отключите его в конфигурации браузера. + +Для Chrome: + +```typescript +browsers: { + "chrome-no-js": { + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + prefs: { + "profile.managed_default_content_settings.javascript": 2, + }, + }, + }, + }, +}, +``` + +Для Firefox: + +```typescript +browsers: { + "firefox-no-js": { + desiredCapabilities: { + browserName: "firefox", + "moz:firefoxOptions": { + prefs: { + "javascript.enabled": false, + }, + }, + }, + }, +}, +``` + +После этого тест запускается сразу в браузере с отключенным JavaScript: + +```typescript +it("показывает содержимое без JavaScript", async ({ browser }) => { + await browser.url("/"); + + await expect(browser.$("[data-testid='no-js-message']")).toBeDisplayed(); +}); +``` + +## Состояние и изоляция + +Некоторые настройки среды сохраняются в рамках WebDriver-сессии и могут повлиять на следующие тесты. + +Снимайте эмуляцию в том же тесте, где ее включили, или в `afterEach`. Вызов [`restore()`][restore] из следующего теста уже не сработает: у нового теста другой объект `browser`. + +Если одна и та же эмуляция нужна в нескольких тестах, задавайте и снимайте ее в хуках: + +```typescript +describe("темная цветовая схема", () => { + beforeEach(async ({ browser }) => { + await browser.emulate("colorScheme", "dark"); + }); + + afterEach(async ({ browser }) => { + await browser.restore("colorScheme"); + }); + + it("показывает изображение для темной схемы", async ({ browser }) => { + await browser.url("/"); + + // ... + }); +}); +``` + +Для настроек, которые должны действовать всю сессию, используйте отдельную конфигурацию браузера. Например, так удобнее задавать язык браузера, запускать тесты с отключенным 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