Mock APIs
Playwright 为大多数浏览器功能提供原生支持。但是,有些实验性 API 和并非所有浏览器都(尚未)完全支持的 API。在这种情况下,Playwright 通常不提供专用的自动化 API。您可以使用 mock 来测试应用程序在这种情况下的行为。本指南给出了几个示例。
简介
让我们考虑一个使用 电池 API 显示设备电池状态的 Web 应用程序。我们将 mock 电池 API 并检查页面是否正确显示电池状态。
创建 mock
由于页面可能会在加载时很早调用 API,因此在页面开始加载之前设置所有 mock 非常重要。实现此目的的最简单方法是调用 page.addInitScript(script[, arg]):
await page.addInitScript(() => {
const mockBattery = {
level: 0.75,
charging: true,
chargingTime: 1800,
dischargingTime: Infinity,
addEventListener: () => { }
};
// 覆盖该方法以始终返回 mock 电池信息。
window.navigator.getBattery = async () => mockBattery;
});
完成后,您可以导航页面并检查其 UI 状态:
// 在每个测试之前配置 mock API。
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const mockBattery = {
level: 0.90,
charging: true,
chargingTime: 1800, // 秒
dischargingTime: Infinity,
addEventListener: () => { }
};
// 覆盖该方法以始终返回 mock 电池信息。
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 调用很有用。您可以记录所有 API 方法调用,然后将它们与黄金结果进行比较。page.exposeFunction(name, callback) 对于将消息从页面传回测试代码可能会派上用场:
test('log battery calls', 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}`)
};
// 覆盖该方法以始终返回 mock 电池信息。
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'
]);
});
更新 mock
要测试应用程序是否正确反映电池状态更新,重要的是要确保 mock 电池对象触发与浏览器实现相同的事件。以下测试演示了如何实现这一点:
test('update battery status (no golden)', async ({ page }) => {
await page.addInitScript(() => {
// Mock 类,当电池状态更改时将通知相应的侦听器。
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();
// 覆盖该方法以始终返回 mock 电池信息。
window.navigator.getBattery = async () => mockBattery;
// 将 mock 对象保存在 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');
});