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