模拟浏览器 API
简介
Playwright 为大多数浏览器功能提供了原生支持。然而,有一些实验性 API 和尚未被所有浏览器完全支持的 API。在这种情况下,Playwright 通常不会提供专门的自动化 API。您可以使用模拟(mock)来测试应用程序在这些情况下的行为。本指南提供了一些示例。
让我们考虑一个使用 电池 API 来显示设备电池状态的 Web 应用。我们将模拟电池 API 并检查页面是否正确显示了电池状态。
创建模拟对象
由于页面可能在加载过程中很早就调用 API,因此必须在页面开始加载前设置好所有模拟对象。最简单的方法是调用 page.addInitScript():
await page.addInitScript(() => {
const mockBattery = {
level: 0.75,
charging: true,
chargingTime: 1800,
dischargingTime: Infinity,
addEventListener: () => { }
};
// 重写方法以始终返回模拟电池信息
window.navigator.getBattery = async () => mockBattery;
});
完成此操作后,您可以导航页面并检查其 UI 状态:
// 在每个测试前配置模拟 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('电源适配器');
await expect(page.locator('.battery-fully')).toHaveText('00:30');
});
模拟只读 API
某些 API 是只读的,因此您无法直接为 navigator 属性赋值。例如:
// 以下代码不会生效
navigator.cookieEnabled = true;
但是,如果该属性是 可配置的,您仍然可以使用纯 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');
});