Эмуляция среды пользователя
- Как эмулировать устройство и 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.
throttleNetwork(), throttleCPU() и команды, которые используются через getPuppeteer(), работают поверх Chrome DevTools Protocol и доступны только в Chromium.
Порядок вызовов
Команда emulate() работает через preload-скрипты BiDi: браузер применяет их при создании документа. Поэтому вызывать ее нужно до навигации: на уже открытой странице она ничего не изменит. Команда restore() снимает эмуляцию только для следующих документов: текущая страница останется как была, пока ее не открыть заново.
На команды через CDP это не распространяется: например, page.emulateTimezone() переключает зону и в уже открытом документе.
Экран и устройство
Viewport
setViewport() задает размер области отрисовки. Команда подходит для проверки адаптивной верстки и поведения интерфейса на разных брейкпойнтах. Если нужно менять не область отрисовки, а размер окна браузера, используйте setWindowSize().
it("показывает мобильную навигацию на узком экране", async ({ browser }) => {
await browser.setViewport({
width: 390,
height: 844,
});
await browser.url("/");
await expect(browser.$("[data-testid='mobile-menu']")).toBeDisplayed();
});
Размер, который задает setViewport(), действует до конца сессии, отдельной команды отката нет.
Профиль устройства
emulate("device") применяет готовый профиль устройства: viewport, DPR и navigator.userAgent.
Например, несколько тестов для iPhone можно объединить одним профилем:
describe("iPhone 15", () => {
let restoreDevice: (() => Promise<void>) | 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(). Замерять нужно на странице приложения: на about:blank значения будут другими. Сам emulate("device") по-прежнему вызывается до навигации.
User agent
Для user agent есть два разных сценария: клиентский код может читать navigator.userAgent, а сервер — HTTP-заголовок User-Agent.
navigator.userAgent
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("/");
await expect(browser.$("[data-testid='ios-install-guide']")).toBeDisplayed();
});
HTTP User-Agent
Если приложение определяет тип клиента на сервере по заголовку User-Agent, используйте browser.getPuppeteer() и Puppeteer page.setUserAgent(). Подробнее о работе с Puppeteer и CDP — в разделе «Как использовать Chrome DevTools Protocol в Testplane».
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 меняет локаль 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("/");
await expect(browser.$("[data-testid='page-title']")).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("/");
await expect(browser.$("[data-testid='average-value']")).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("/");
await expect(browser.$("[data-testid='event-time']")).toHaveText("07:00");
});
В этом примере страница показывает время события 2024-09-04T11:00:00Z, в зоне America/New_York это 07:00.
Время и таймеры
Когда поведение интерфейса зависит от текущего времени или таймеров, используйте browser.emulate("clock").
Фиксированное время
Например, так можно проверить состояние страницы в определенный момент:
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) продвигает виртуальное время на указанное количество миллисекунд и запускает таймеры, которые должны сработать.
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").
Например, так можно проверить выбор изображения для темной цветовой схемы:
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.
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").
Например, так можно проверить сообщение об ошибке при сетевом запросе:
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():
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"):
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().
Например, так можно запустить сценарий с четырехкратным замедлением CPU:
it("работает при замедленном CPU", async ({ browser }) => {
await browser.throttleCPU(4);
await browser.url("/");
// ...
await browser.throttleCPU(1);
});
Чем больше коэффициент, тем сильнее замедляется выполнение. Значение 1 отключает throttling.
Геолокация
Если приложение использует координаты пользователя, задайте их через browser.emulate("geolocation").
Например, так можно проверить поиск ближайшего пункта выдачи для пользователя в Берлине:
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().
Например, так можно проверить статус уведомлений:
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:
browsers: {
"chrome-no-js": {
desiredCapabilities: {
browserName: "chrome",
"goog:chromeOptions": {
prefs: {
"profile.managed_default_content_settings.javascript": 2,
},
},
},
},
},
Для Firefox:
browsers: {
"firefox-no-js": {
desiredCapabilities: {
browserName: "firefox",
"moz:firefoxOptions": {
prefs: {
"javascript.enabled": false,
},
},
},
},
},
После этого тест запускается сразу в браузере с отключенным JavaScript:
it("показывает содержимое без JavaScript", async ({ browser }) => {
await browser.url("/");
await expect(browser.$("[data-testid='no-js-message']")).toBeDisplayed();
});
Состояние и изоляция
Некоторые настройки среды сохраняются в рамках WebDriver-сессии и могут повлиять на следующие тесты.
Снимайте эмуляцию в том же тесте, где ее включили, или в afterEach. Вызов restore() из следующего теста уже не сработает: у нового теста другой объект browser.
Если одна и та же эмуляция нужна в нескольких тестах, задавайте и снимайте ее в хуках:
describe("темная цветовая схема", () => {
beforeEach(async ({ browser }) => {
await browser.emulate("colorScheme", "dark");
});
afterEach(async ({ browser }) => {
await browser.restore("colorScheme");
});
it("показывает изображение для темной схемы", async ({ browser }) => {
await browser.url("/");
// ...
});
});
Для настроек, которые должны действовать всю сессию, используйте отдельную конфигурацию браузера. Например, так удобнее задавать язык браузера, запускать тесты с отключенным JavaScript или фиксировать размер окна опцией windowSize.