Skip to main content

Мокирование API браузера

Введение

Playwright предоставляет нативную поддержку для большинства функций браузера. Однако существуют некоторые экспериментальные API и API, которые еще не полностью поддерживаются всеми браузерами. В таких случаях Playwright обычно не предоставляет специализированные API для автоматизации. Вы можете использовать моки, чтобы протестировать поведение вашего приложения в таких случаях. Это руководство дает несколько примеров.

Введение

Рассмотрим веб-приложение, которое использует battery API для отображения статуса батареи вашего устройства. Мы замокируем battery API и проверим, что страница правильно отображает статус батареи.

Создание моков

Поскольку страница может вызывать API очень рано во время загрузки, важно настроить все моки до того, как страница начнет загружаться. Самый простой способ достичь этого — вызвать page.addInitScript():

await page.addInitScript(() => {
const mockBattery = {
level: 0.75,
charging: true,
chargingTime: 1800,
dischargingTime: Infinity,
addEventListener: () => { }
};
// Переопределяем метод, чтобы всегда возвращать информацию о мокированной батарее.
window.navigator.getBattery = async () => mockBattery;
});

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

// Настройка мок API перед каждым тестом.
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const mockBattery = {
level: 0.90,
charging: true,
chargingTime: 1800, // секунды
dischargingTime: Infinity,
addEventListener: () => { }
};
// Переопределяем метод, чтобы всегда возвращать информацию о мокированной батарее.
window.navigator.getBattery = async () => mockBattery;
});
});

test('показать статус батареи', async ({ page }) => {
await page.goto('/');
await expect(page.locator('.battery-percentage')).toHaveText('90%');
await expect(page.locator('.battery-status')).toHaveText('Adapter');
await expect(page.locator('.battery-fully')).toHaveText('00:30');
});

Мокирование только для чтения API

Некоторые API доступны только для чтения, поэтому вы не сможете присвоить значение свойству navigator. Например,

// Следующая строка не будет иметь эффекта.
navigator.cookieEnabled = true;

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

await page.addInitScript(() => {
Object.defineProperty(Object.getPrototypeOf(navigator), 'cookieEnabled', { value: false });
});

Проверка вызовов API

Иногда полезно проверить, сделала ли страница все ожидаемые вызовы API. Вы можете записать все вызовы методов API, а затем сравнить их с эталонным результатом. page.exposeFunction() может быть полезен для передачи сообщений со страницы обратно в тестовый код:

test('логирование вызовов батареи', async ({ page }) => {
const log = [];
// Экспонируем функцию для добавления сообщений в скрипт Node.js.
await page.exposeFunction('logCall', msg => log.push(msg));
await page.addInitScript(() => {
const mockBattery = {
level: 0.75,
charging: true,
chargingTime: 1800,
dischargingTime: Infinity,
// Логирование вызовов addEventListener.
addEventListener: (name, cb) => logCall(`addEventListener:${name}`)
};
// Переопределяем метод, чтобы всегда возвращать информацию о мокированной батарее.
window.navigator.getBattery = async () => {
logCall('getBattery');
return mockBattery;
};
});

await page.goto('/');
await expect(page.locator('.battery-percentage')).toHaveText('75%');

// Сравниваем фактические вызовы с эталонными.
expect(log).toEqual([
'getBattery',
'addEventListener:chargingchange',
'addEventListener:levelchange'
]);
});

Обновление мока

Чтобы протестировать, что приложение правильно отражает обновления статуса батареи, важно убедиться, что объект мокированной батареи генерирует те же события, что и реализация браузера. Следующий тест демонстрирует, как этого достичь:

test('обновление статуса батареи (без эталона)', async ({ page }) => {
await page.addInitScript(() => {
// Мок-класс, который будет уведомлять соответствующих слушателей при изменении статуса батареи.
class BatteryMock {
level = 0.10;
charging = false;
chargingTime = 1800;
dischargingTime = Infinity;
_chargingListeners = [];
_levelListeners = [];
addEventListener(eventName, listener) {
if (eventName === 'chargingchange')
this._chargingListeners.push(listener);
if (eventName === 'levelchange')
this._levelListeners.push(listener);
}
// Будет вызвано тестом.
_setLevel(value) {
this.level = value;
this._levelListeners.forEach(cb => cb());
}
_setCharging(value) {
this.charging = value;
this._chargingListeners.forEach(cb => cb());
}
}
const mockBattery = new BatteryMock();
// Переопределяем метод, чтобы всегда возвращать информацию о мокированной батарее.
window.navigator.getBattery = async () => mockBattery;
// Сохраняем объект мока в window для более легкого доступа.
window.mockBattery = mockBattery;
});

await page.goto('/');
await expect(page.locator('.battery-percentage')).toHaveText('10%');

// Обновляем уровень до 27.5%
await page.evaluate(() => window.mockBattery._setLevel(0.275));
await expect(page.locator('.battery-percentage')).toHaveText('27.5%');
await expect(page.locator('.battery-status')).toHaveText('Battery');

// Эмулируем подключение адаптера
await page.evaluate(() => window.mockBattery._setCharging(true));
await expect(page.locator('.battery-status')).toHaveText('Adapter');
await expect(page.locator('.battery-fully')).toHaveText('00:30');
});