Skip to main content

模擬瀏覽器 APIs

簡介

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: () => { }
};
// 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 是唯讀的,因此您無法指派給 navigator 屬性。例如,

// 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');
});