Skip to content

docs(emulation): user environment emulation guide (ru) - #139

Open
swordqueen wants to merge 3 commits into
masterfrom
users/kseniataranov/DOCORG-8371.emulation
Open

docs(emulation): user environment emulation guide (ru)#139
swordqueen wants to merge 3 commits into
masterfrom
users/kseniataranov/DOCORG-8371.emulation

Conversation

@swordqueen

Copy link
Copy Markdown
Collaborator

Adds a new "Basic guides" article on emulating the user environment in Testplane.
Russian version only; the English page is a stub for now.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

✅ Successfully deployed static


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

В Testplane эти условия настраиваются несколькими способами: через WebDriver BiDi, обычные WebDriver-команды, CDP и capabilities браузера. Выбор механизма зависит от того, какое свойство среды нужно изменить и в каком браузере выполняется тест.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь речь идет о 4 способах эмуляции, а ниже таблица уже с тремя способами.
я бы здесь капабилити просто вынес на уровень выше как общий способ задать донастройку браузеру и добавил, что какие-то части эмулации (например, user agent) лучше выносить в настройку браузера, чтобы не применять ее к каждому тесту. Собственно, в текущей документации сейчас вообще ничего про это нет, только упоминание про capabilities


<Admonition type="caution" title="Сохраняйте функцию отката">

`browser.restore()` возвращает user agent, но не viewport, установленный через `emulate("device")`. Для полного отката вызывайте функцию, которую вернул `emulate("device")`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

про функцию restoreDevice я бы написал сразу в примере, т.к. сразу не понятно что вообще мы сохранили в переменную. и только в самом конце мы получаем эту информацию

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

При этом, у нас внезапно появляется уже второй раз метод restore(), но мы все еще не знаем что он делает в подробностях


`browser.restore()` возвращает user agent, но не viewport, установленный через `emulate("device")`. Для полного отката вызывайте функцию, которую вернул `emulate("device")`.

Viewport при этом вернётся к профилю `Desktop Chrome`, а не к исходному размеру.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

так а делать то что с этим? :)
если мы пишем проблему, то сразу нужно приводить и пример ее решения. Например, заранее сохранить размеры окна, чтобы их потом применить

});
```

Команда применяется к текущему контексту и не возвращает функцию отката. Чтобы восстановить viewport, вызовите `setViewport()` повторно с нужными значениями.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь не хватает полного примера с сохранением и восстановлением размеров вьюпорта


### User agent

Если клиентский код выбирает мобильный интерфейс по `navigator.userAgent`, задайте значение отдельно:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

отдельно от чего?


Значение `1` соответствует обычной скорости, `2` замедляет CPU вдвое, `4` — вчетверо.

Команда поддерживается только в Chromium-браузерах с доступным CDP-подключением. В Firefox она завершается ошибкой до применения ограничений.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Предварительно выдавать разрешение не нужно.

Чтобы проверить обработку ошибки, передайте объект `Error`:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь нужно явно указать какую именно ошибку мы хотим проверить


</Admonition>

Способ предназначен для Chromium-браузеров, поскольку использует `goog:chromeOptions`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

это вообще никак не связано. просто для каждого типа браузеров свое обозначение. Для chromium это goog:chromeOptions, для остальных браузеров она другая. например,

"moz:firefoxOptions": {
    prefs: {
        "javascript.enabled": false,
    },
}


Повторяющиеся сценарии удобно оформлять как отдельные браузерные профили или вспомогательные функции: например, `mobile`, `dark-theme` и `slow-network`.

Настройки, специфичные для одного сценария, оставляйте явными в самом тесте. Состояние `emulate()` может перейти в следующий тест даже при `isolation: true`, поэтому восстанавливайте его в том же тесте:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Состояние emulate() может перейти в следующий тест даже при isolation: true

хм... звучит сомнительно. есть примеры?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, есть живой пример. На стенде, который я написала для проверки утверждений из статьи, emulate() протекал в следующий it даже при isolation: true. В следующем тесте sessionId оставался тем же, но объект browser был уже другим, при этом эмулированное состояние продолжало действовать. Я отдельно проверила cleanup через afterEach, он снимает состояние корректно. Поэтому формулировку в статье уточню и добавлю пример cleanup.

Для гарантированно чистой сессии в каждом тесте задайте для браузера:

```typescript
testsPerSession: 1,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

это очень плохой совет, т.к. на создание новой сессии может уходить много времени

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

let restoreDevice: (() => Promise<void>) | undefined;
let viewport: { width: number; height: number; devicePixelRatio: number };

before(async ({ browser }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а как ты проверяла этот пример? в testplane нет хука before, есть только beforeEach.
Глобальный before можно указать только в самом конфиге. Если указать такое в тесте, то твой запуск упадет еще на этапе парсинга теста с ошибкой Error: "before" and "after" hooks are forbidden, use "beforeEach" and "afterEach" hooks instead

it("показывает инструкцию для iOS на профиле iPhone 15", async ({ browser }) => {
await browser.url("/");

await expect(browser.$("[data-testid='ios-install-guide']")).toBeDisplayed();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

я бы везде в тестах использовал browser.getByTestId('ios-install-guide') — читается проще


[`emulate("device")`][emulate-device] применяет готовый профиль устройства: viewport, DPR и `navigator.userAgent`.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ты пишешь о примере с несколькими тестами, но ниже приведен пример с одним тестом

restoreDevice = await browser.emulate("device", "iPhone 15");
});

afterEach(async ({ browser }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

});
```

Функция, которую возвращает `emulate("device")`, снимает подмененный user agent, но исходный viewport не возвращает: вместо него ставится профиль Desktop Chrome — 1280 × 720, DPR 1. Поэтому в примере размер запоминается в `before` и после каждого теста возвращается через [`setViewport()`][set-viewport]. Замерять нужно на странице приложения: на `about:blank` значения будут другими. Сам `emulate("device")` по-прежнему вызывается до навигации.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь ты правильно пишешь о том, что нужно вернуть состояние браузера. Но ниже (например, для ua) такой рекомендации нет

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

я бы еще добавил, что эмуляция iPhone 15 — это не точная копия интерфейса мобильного устройства. Т.е. у пользователя могут возникнуть иллюзии о том, что он сейчас и мобильное устройство протестирует без реального девайса, но это не так.


### Порядок вызовов

Команда `emulate()` работает через preload-скрипты BiDi: браузер применяет их при создании документа. Поэтому вызывать ее нужно до навигации: на уже открытой странице она ничего не изменит. Команда `restore()` снимает эмуляцию только для следующих документов: текущая страница останется как была, пока ее не открыть заново.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Поэтому вызывать ее нужно до навигации: на уже открытой странице она ничего не изменит

это не совсем так. Например, если я вызову emulate("clock"), то изменения применятся на этой же странице без перезагрузки страницы. Т.е. поведение разное в зависимости от указанных аргументов

await browser.emulate("colorScheme", "dark");
await browser.url("/");

await expect(browser.$("[data-testid='theme-image']")).toHaveAttribute(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут можно поправить название testid на что-то типа theme-logo, чтобы была ассоциация с тем, что мы ищем именно изображение

Например, так можно проверить сообщение об ошибке при сетевом запросе:

```typescript
it("показывает сообщение при отсутствии сети", async ({ browser }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

такие примеры как будто хочется сделать рабочими, чтобы пользователь мог скопировать пример и запустить у себя. Например

it("docs search test", async ({browser}) => {
    await browser.url("https://testplane.io/docs/");

    await browser.throttleNetwork("offline");
    await browser.$(".navbar__brand").click();

    await browser.assertView("page"); // можно оставить и твой пример с ассертом. Просто на скриншотной проверки хорошо видно отсутствие интернета. Но пример должен пониматься и без запуска, да

    await browser.throttleNetwork("online");
});

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Согласна, что копируемый пример полезнее. Но сейчас все примеры в статье
построены на вымышленном приложении с data-testid, и часть сценариев
на публичном сайте не воспроизвести: немецкая локаль, геолокация с пунктом
выдачи, промо-акция с порогом по времени, страница с отключённым JavaScript.

Предлагаю сделать так: примеры, которые реально запускаются на публичном
сайте, перевести на нег: offline с assertView (как в твоём примере),
throttleCPU, colorScheme и viewport. Остальные оставить на вымышленном
приложении, но переписать их так, чтобы они были понятны без запуска.

Одна оговорка по примеру с assertView: у читателя нет эталонных скриншотов,
и первый прогон скопированного примера упадёт с ошибкой об отсутствии
референса, пока он не запустит с --update-refs. Если берём этот пример,
я добавлю про это строку в текст?

Такое частичное переписывание примеров подойдёт?

browserName: "chrome",
"goog:chromeOptions": {
prefs: {
"profile.managed_default_content_settings.javascript": 2,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь бы пояснение добавить почему именно 2 (со ссылкой на спеку)

```typescript
it("показывает активную акцию в заданный период", async ({ browser }) => {
const clock = await browser.emulate("clock", {
now: new Date("2024-09-04T12:30:00Z"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь не хватает важного уточнения, что при таком вызове подменяется не только время на странице, но стабаются еще и таймеры. Т.е. если на странице используется логика с setTimeout, то она не выполнится пока не будет вызван clock.tick(555).
Если пример показывает только стаб даты, то нужно явно прокинуть и параметр toFake: ["Date"]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants