From a16eaf584aaf9b0cecacdff44865602a00217e15 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 18 Feb 2022 18:28:03 -0800 Subject: [PATCH] docs: mock guide (#12241) --- docs/src/mock-js.md | 158 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/src/mock-js.md diff --git a/docs/src/mock-js.md b/docs/src/mock-js.md new file mode 100644 index 0000000000..6e6f0f7615 --- /dev/null +++ b/docs/src/mock-js.md @@ -0,0 +1,158 @@ +--- +id: mock +title: "Mock APIs" +--- + +Playwright provides native support for most of the browser features. However, there are some experimental APIs +and APIs which are not (yet) fully supported by all browsers. Playwright usually doesn't provide dedicated +atomation APIs in such cases. You can use mocks to test behavior of your application in such cases. This guide +gives a few examples. + + + +## Introduction + +Let's consider a web app that uses [battery API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getBattery) +to show your device's battery status. We'll mock the battery API and check that the page correctly displays the +battery status. + +## Creating mocks + +Since the page may be calling the API very early while loading it's important to setup all the mocks before the +page started loading. The easiest way to achieve that is to call [`method: Page.addInitScript`]: + +```js +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; +}); +``` + +Once this is done you can navigate the page and check its UI state: + +```js +// 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'); +}); + +``` + +## Verifying API calls + +Sometimes it is useful to check if the page made all expected APIs calls. You can +record all API method invocations and then compare them with golden result. +[`method: Page.exposeFunction`] may come in handy for passing message from +the page back to the test code: + +```js +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' + ]); +}); +``` + +## Updating mock + +To test that the app correctly reflects battery status updates it's important to +make sure that the mock battery object fires same events that the browser implementation +would. The following test demonstrates how to achieve that: + +```js +test('update battery status (no golden)', async ({ page }) => { + await page.addInitScript(() => { + // Mock class that will notify corresponding listners 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'); +}); +```