メインコンテンツへスキップ

ブラウザAPIのモック

はじめに

Playwrightは、ほとんどのブラウザ機能にネイティブサポートを提供しています。しかし、まだすべてのブラウザで完全にサポートされていない実験的なAPIやAPIもあります。Playwrightは通常、そのような場合に専用の自動化APIを提供しません。そのような場合、モックを使用してアプリケーションの動作をテストできます。このガイドではいくつかの例を示します。

デバイスのバッテリー状態を表示するためにバッテリーAPIを使用するウェブアプリを考えてみましょう。バッテリー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;

ただし、プロパティがconfigurableである場合、プレーンな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');
});