Перейти к основному содержимому

Эмуляция среды пользователя

Что вы узнаете
  • Как эмулировать устройство и viewport, цветовую схему, время, геолокацию и разрешения
  • Как задать язык интерфейса и отключить JavaScript
  • Как проверить работу приложения при медленной сети и слабом CPU

Введение​

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

В Testplane одни параметры среды можно менять прямо во время теста, а другие нужно заранее задать в настройках браузера.

Для команд browser.emulate() и setViewport() требуется WebDriver BiDi. В конфигурации браузера включите webSocketUrl:

browsers: {
chrome: {
desiredCapabilities: {
browserName: "chrome",
webSocketUrl: true,
},
},
},

В одной сессии Chrome с webSocketUrl: true можно использовать и browser.emulate(), и команды, которые работают через Chrome DevTools Protocol: getPuppeteer(), throttleNetwork() и throttleCPU(). Создавать для них отдельную конфигурацию без BiDi не нужно.

Минимальная версия Chrome с поддержкой BiDi — 128, Firefox — 119.

В большинстве случаев браузер применяет настройки emulate() при открытии страницы. Поэтому сначала вызовите команду, а затем переходите на нужную страницу. Исключения указаны в соответствующих разделах.

Чтобы искать элементы через getByTestId и findByTestId, как в примерах из этой статьи, установите и подключите @testplane/testing-library. В тесте с отключенным JavaScript эти команды недоступны, поэтому в нем используется $().

Не все команды работают в Firefox

throttleNetwork(), throttleCPU() и команды, которые используются через getPuppeteer(), работают поверх Chrome DevTools Protocol и доступны только в Chromium.

Экран и устройство​

Viewport​

setViewport() задает размер области отрисовки. С помощью команды можно проверять адаптивную верстку и поведение интерфейса на разных брейкпойнтах. Чтобы изменить размер всего окна браузера, а не viewport, используйте setWindowSize().

it("показывает мобильную навигацию на узком экране", async ({ browser }) => {
await browser.setViewport({
width: 390,
height: 844,
});

await browser.url("/");

const mobileMenu = await browser.getByTestId("mobile-menu");
await expect(mobileMenu).toBeDisplayed();
});

Размер, заданный через setViewport(), сохраняется до конца сессии. Отдельной команды для отката нет.

Профиль устройства​

emulate("device") применяет готовый профиль устройства: viewport, DPR и navigator.userAgent. Viewport меняется сразу, а user agent — только в документах, созданных после вызова команды. Поэтому сначала включите эмуляцию, а затем переходите на нужную страницу.

Профиль устройства подменяет user agent и размеры, но не превращает десктопный браузер в мобильный. Например, дескриптор iPhone 15 содержит параметры isMobile и hasTouch, но команда их не применяет. Тач-события и navigator.maxTouchPoints не эмулируются, движок остается Chromium вместо WebKit/iOS, а системные шрифты, экранная клавиатура, адресная строка и производительность не меняются. Поэтому такая эмуляция не заменяет проверку на реальном устройстве.

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

const installGuide = await browser.getByTestId("ios-install-guide");
await expect(installGuide).toBeDisplayed();
});

В этом примере эмуляция не снимается и остается до конца сессии. Если после него в той же сессии идут другие тесты, сохраните функцию, которую вернул emulate("device"), и вызовите ее в том же тесте или в afterEach.

Функция отката убирает подмену user agent и устанавливает viewport профиля Desktop Chrome — 1280 × 720 с DPR 1. Исходный размер viewport она не восстанавливает. Если следующим тестам нужен другой размер, после отката вызовите setViewport() с константой.

User agent​

User agent можно проверять в двух разных местах: клиентский код читает navigator.userAgent, а сервер получает HTTP-заголовок User-Agent.

emulate("userAgent") меняет значение, доступное клиентскому JavaScript через navigator.userAgent.

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("/");

const installGuide = await browser.getByTestId("ios-install-guide");
await expect(installGuide).toBeDisplayed();
});

Эмуляция сохраняется до конца сессии, поэтому снимайте ее в том же тесте или в afterEach. После restore() новые страницы открываются уже без эмуляции. Страница, которая уже открыта, не меняется, пока ее не перезагрузить. Подробнее — в разделе «Состояние и изоляция».

HTTP User-Agent​

Если приложение определяет тип клиента на сервере по заголовку User-Agent, используйте browser.getPuppeteer() и Puppeteer page.setUserAgent().

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.

Локаль​

Языковые предпочтения браузера и локаль Intl задаются отдельно. Приложение получает языковые предпочтения из navigator.language и заголовка Accept-Language, а локаль Intl определяет формат чисел и дат. Настройка intl.accept_languages меняет только языковые предпочтения. Команда Emulation.setLocaleOverride, наоборот, меняет локаль Intl, но не языковые предпочтения браузера.

В Chrome на macOS аргумент запуска --lang не дает нужного эффекта: браузер принимает его без ошибки, но продолжает сообщать системный язык.

Язык интерфейса​

Если приложение выбирает язык по настройкам браузера или заголовку Accept-Language, задайте intl.accept_languages в конфигурации браузера.

Для Chrome:

browsers: {
"chrome-de": {
desiredCapabilities: {
browserName: "chrome",
"goog:chromeOptions": {
prefs: {
"intl.accept_languages": "de-DE,de",
},
},
},
},
},

Для Firefox:

browsers: {
"firefox-de": {
desiredCapabilities: {
browserName: "firefox",
"moz:firefoxOptions": {
prefs: {
"intl.accept_languages": "de-DE,de",
},
},
},
},
},

После этого в тесте можно проверить интерфейс на нужном языке:

it("показывает интерфейс на немецком", async ({ browser }) => {
await browser.url("/");

const pageTitle = await browser.getByTestId("page-title");
await expect(pageTitle).toHaveText("Bestellungen");
});

Форматирование через Intl​

Если приложение форматирует числа или даты через Intl, измените локаль Intl через Puppeteer.

Например, так можно проверить форматирование числа для немецкой локали:

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("/");

const averageValue = await browser.getByTestId("average-value");
await expect(averageValue).toHaveText("1.234,56");
});

В этом примере приложение форматирует значение 1234.56 через Intl.NumberFormat. Для локали de-DE результат выглядит как 1.234,56.

Часовой пояс​

Если отображение дат и времени зависит от часового пояса пользователя, задайте нужный часовой пояс через Puppeteer page.emulateTimezone().

it("показывает время события в часовом поясе пользователя", 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");
});

В этом примере время события — 2024-09-04T11:00:00Z. В часовом поясе America/New_York страница показывает его как 07:00.

Команды через CDP применяются и к уже открытой странице, поэтому часовой пояс можно изменить в середине теста.

Время и таймеры​

Когда поведение интерфейса зависит от текущего времени или таймеров, используйте browser.emulate("clock").

По умолчанию emulate("clock") подменяет не только дату, но и setTimeout, setInterval, requestAnimationFrame, performance и другие API, связанные со временем. Таймеры становятся виртуальными и сами по себе не срабатывают: время продвигается только после вызова tick(). Если в тесте нужно изменить лишь дату, ограничьте подмену с помощью toFake.

В отличие от остальных настроек emulate(), время можно подменить и на уже открытой странице. Вызов clock.restore() также восстанавливает время на текущей странице.

Фиксированное время​

Например, так можно проверить состояние страницы в определенный момент:

it("показывает активную акцию в заданный период", 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("Акция началась");
} finally {
await clock.restore();
}
});

Таймеры​

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

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);

const notification = await browser.getByTestId("notification");
await expect(notification).not.toBeDisplayed();
} finally {
await clock.restore();
}
});

Цветовая схема​

Если приложение определяет цветовую схему через window.matchMedia(), используйте browser.emulate("colorScheme").

Например, так можно проверить выбор изображения для темной цветовой схемы:

it("показывает изображение для темной цветовой схемы", 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");
});

Эмуляция сохраняется до конца сессии, поэтому снимайте ее в том же тесте или в afterEach. После restore() новые страницы открываются уже без эмуляции. Страница, которая уже открыта, не меняется, пока ее не перезагрузить. Подробнее — в разделе «Состояние и изоляция».

browser.emulate("colorScheme") меняет результат matchMedia() для prefers-color-scheme, но не влияет на CSS. Чтобы проверить стили из @media (prefers-color-scheme), используйте Emulation.setEmulatedMedia.

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 themeBox = await browser.getByTestId("theme-box");
const background = await themeBox.getCSSProperty("background-color");

expect(background.value).toBe("rgba(0,0,0,1)");
});

Сеть​

Отсутствие сети​

Для проверки работы приложения без сети используйте browser.throttleNetwork("offline").

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

it("показывает сообщение при отсутствии сети", 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("Нет подключения к сети");

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

Сначала загрузите страницу, а затем отключите сеть перед действием, которое отправляет запрос. В отличие от emulate(), throttleNetwork() нужно вызывать после навигации. Чтобы вернуть обычный сетевой режим, используйте профиль "online".

Медленное соединение​

Для проверки интерфейса при медленном соединении передайте параметры сети в browser.throttleNetwork():

it("показывает состояние загрузки при медленной сети", 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");
});

Объект содержит четыре поля: offline, задержку latency в миллисекундах, а также скорости загрузки и отправки данных downloadThroughput и uploadThroughput в байтах в секунду.

Для типовых условий параметры можно не задавать вручную. Вместо объекта передайте имя готового профиля, например "Good3G" или "offline".

Если приложение определяет состояние подключения по navigator.onLine, используйте browser.emulate("onLine"):

it("показывает офлайн-режим", async ({ browser }) => {
await browser.emulate("onLine", false);
await browser.url("/");

const connectionStatus = await browser.getByTestId("connection-status");
await expect(connectionStatus).toHaveText("Офлайн");
});

Эмуляция сохраняется до конца сессии, поэтому снимайте ее в том же тесте или в afterEach. После restore() новые страницы открываются уже без эмуляции. Страница, которая уже открыта, не меняется, пока ее не перезагрузить. Подробнее — в разделе «Состояние и изоляция».

browser.emulate("onLine", false) только меняет значение navigator.onLine, но не отключает сеть: HTTP-запросы продолжают выполняться.

Производительность CPU​

Для проверки интерфейса при ограниченной производительности процессора используйте browser.throttleCPU().

Например, так можно проверить сценарий при четырехкратном замедлении CPU:

it("работает при замедленном CPU", async ({ browser }) => {
await browser.throttleCPU(4);

await browser.url("/");

// ...

await browser.throttleCPU(1);
});

Чем больше коэффициент, тем медленнее выполняется код. Значение 1 отключает замедление.

Геолокация​

Если приложение использует координаты пользователя, задайте их через browser.emulate("geolocation").

Например, так можно проверить поиск ближайшего пункта выдачи для пользователя в Берлине:

it("показывает ближайший пункт выдачи", 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("Пункт выдачи на Alexanderplatz");
});

Эмуляция сохраняется до конца сессии, поэтому снимайте ее в том же тесте или в afterEach. После restore() новые страницы открываются уже без эмуляции. Страница, которая уже открыта, не меняется, пока ее не перезагрузить. Подробнее — в разделе «Состояние и изоляция».

browser.emulate("geolocation") подменяет координаты, которые приложение получает через navigator.geolocation.getCurrentPosition(). Отдельно настраивать разрешение на геолокацию не нужно.

Разрешения браузера​

Если поведение приложения зависит от разрешений браузера, используйте browser.setPermissions().

Например, так можно проверить статус уведомлений:

it("показывает статус уведомлений", 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("Уведомления включены");
});

Вызывайте browser.setPermissions() после перехода на страницу приложения. Разрешение привязывается к адресу текущей страницы. До навигации открыта пустая страница, поэтому команда завершится с ошибкой.

JavaScript​

Если нужно проверить работу страницы без JavaScript, отключите его в конфигурации браузера.

Для Chrome:

browsers: {
"chrome-no-js": {
desiredCapabilities: {
browserName: "chrome",
"goog:chromeOptions": {
prefs: {
"profile.managed_default_content_settings.javascript": 2,
},
},
},
},
},

Значение 1 в profile.managed_default_content_settings.javascript разрешает JavaScript, а 2 блокирует его.

Для Firefox:

browsers: {
"firefox-no-js": {
desiredCapabilities: {
browserName: "firefox",
"moz:firefoxOptions": {
prefs: {
"javascript.enabled": false,
},
},
},
},
},

В Firefox javascript.enabled принимает логическое значение, а не число.

После этого тест сразу запускается в браузере с отключенным JavaScript:

it("показывает содержимое без JavaScript", async ({ browser }) => {
await browser.url("/");

await expect(browser.$("[data-testid='no-js-message']")).toBeDisplayed();
});

Состояние и изоляция​

Некоторые настройки среды сохраняются до конца WebDriver-сессии и могут повлиять на следующие тесты.

Снимайте эмуляцию в том же тесте, где ее включили, или в afterEach. Не откладывайте restore() до следующего теста: он получит другой объект browser, и команда уже не сработает. Для профиля устройства используйте функцию, которую вернул emulate("device"), потому что restore("device") не поддерживается.

Если одна и та же эмуляция нужна в нескольких тестах, задавайте и снимайте ее в хуках:

describe("темная цветовая схема", () => {
beforeEach(async ({ browser }) => {
await browser.emulate("colorScheme", "dark");
});

afterEach(async ({ browser }) => {
await browser.restore("colorScheme");
});

it("показывает изображение для темной схемы", async ({ browser }) => {
await browser.url("/");

// ...
});
});

Если настройка должна действовать всю сессию, создайте для нее отдельную конфигурацию браузера. Так удобнее задавать язык браузера, запускать тесты с отключенным JavaScript и фиксировать размер окна с помощью windowSize.