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

Мокирование 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');
});