跳至主要内容

模拟浏览器 API

简介

Playwright 原生支持大多数浏览器功能。但是,某些实验性 API 和 API 尚未得到所有浏览器的完全支持。在这种情况下,Playwright 通常不会提供专门的自动化 API。您可以使用模拟来测试应用程序在这种情况下 的行为。本指南提供了一些示例。

简介

让我们考虑一个使用 电池 API 显示设备电池状态的 Web 应用程序。我们将模拟电池 API 并检查页面是否正确显示电池状态。

创建模拟

由于页面可能在加载时非常早地调用 API,因此在页面开始加载之前设置所有模拟非常重要。实现此目的最简单的方法是调用 page.addInitScript()

await page.addInitScript(() => {
const mockBattery = {
level: 0.75,
charging: true,
chargingTime: 1800,
dischargingTime: Infinity,
addEventListener: () => { }
};
// Override the method to always return mock battery info.
window.navigator.getBattery = async () => mockBattery;
});

完成后,您可以导航到页面并检查其 UI 状态。

// Configure mock API before each test.
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const mockBattery = {
level: 0.90,
charging: true,
chargingTime: 1800, // seconds
dischargingTime: Infinity,
addEventListener: () => { }
};
// Override the method to always return mock battery info.
window.navigator.getBattery = async () => mockBattery;
});
});

test('show battery status', 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 是只读的,因此您无法分配给导航器属性。例如,

// Following line will have no effect.
navigator.cookieEnabled = true;

但是,如果属性是 可配置的,您仍然可以使用纯 JavaScript 覆盖它。

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

验证 API 调用

有时需要检查页面是否进行了所有预期的 API 调用。您可以记录所有 API 方法调用,然后将其与黄金结果进行比较。 page.exposeFunction() 可能有助于将消息从页面传递回测试代码。

test('log battery calls', async ({ page }) => {
const log = [];
// Expose function for pushing messages to the Node.js script.
await page.exposeFunction('logCall', msg => log.push(msg));
await page.addInitScript(() => {
const mockBattery = {
level: 0.75,
charging: true,
chargingTime: 1800,
dischargingTime: Infinity,
// Log addEventListener calls.
addEventListener: (name, cb) => logCall(`addEventListener:${name}`)
};
// Override the method to always return mock battery info.
window.navigator.getBattery = async () => {
logCall('getBattery');
return mockBattery;
};
});

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

// Compare actual calls with golden.
expect(log).toEqual([
'getBattery',
'addEventListener:chargingchange',
'addEventListener:levelchange'
]);
});

更新模拟

为了测试应用程序是否正确反映电池状态更新,务必确保模拟电池对象触发与浏览器实现相同的事件。以下测试演示了如何实现此目的。

test('update battery status (no golden)', async ({ page }) => {
await page.addInitScript(() => {
// Mock class that will notify corresponding listeners when battery status changes.
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);
}
// Will be called by the test.
_setLevel(value) {
this.level = value;
this._levelListeners.forEach(cb => cb());
}
_setCharging(value) {
this.charging = value;
this._chargingListeners.forEach(cb => cb());
}
}
const mockBattery = new BatteryMock();
// Override the method to always return mock battery info.
window.navigator.getBattery = async () => mockBattery;
// Save the mock object on window for easier access.
window.mockBattery = mockBattery;
});

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

// Update level to 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');

// Emulate connected adapter
await page.evaluate(() => window.mockBattery._setCharging(true));
await expect(page.locator('.battery-status')).toHaveText('Adapter');
await expect(page.locator('.battery-fully')).toHaveText('00:30');
});