From 6a04e1f0265c0e6cfa17106fa8715f6af3ac68ef Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 30 Dec 2019 14:09:54 -0800 Subject: [PATCH] feat(offline+auth): enable those in webkit, make them a part of the core API (#346) --- docs/api.md | 35 +++++++++++-------------- package.json | 2 +- src/chromium/crApi.ts | 1 - src/chromium/crNetworkManager.ts | 5 ++-- src/chromium/crPage.ts | 11 +++++--- src/chromium/features/crInterception.ts | 20 -------------- src/firefox/ffPage.ts | 8 ++++++ src/page.ts | 20 +++++++++++++- src/types.ts | 5 ++++ src/webkit/wkNetworkManager.ts | 17 +++++++----- src/webkit/wkPage.ts | 10 ++++++- test/interception.spec.js | 22 ++++++++-------- test/screenshot.spec.js | 2 +- 13 files changed, 91 insertions(+), 67 deletions(-) delete mode 100644 src/chromium/features/crInterception.ts diff --git a/docs/api.md b/docs/api.md index a3ec8be2df..7658d8ae7a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -144,6 +144,7 @@ * [page.$x(expression)](#pagexexpression) * [page.addScriptTag(options)](#pageaddscripttagoptions) * [page.addStyleTag(options)](#pageaddstyletagoptions) + * [page.authenticate(credentials)](#pageauthenticatecredentials) * [page.browserContext()](#pagebrowsercontext) * [page.click(selector[, options])](#pageclickselector-options) * [page.close([options])](#pagecloseoptions) @@ -173,6 +174,7 @@ * [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) * [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) * [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) + * [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled) * [page.setRequestInterception(enabled)](#pagesetrequestinterceptionenabled) * [page.setViewport(viewport)](#pagesetviewportviewport) * [page.title()](#pagetitle) @@ -233,9 +235,6 @@ * [chromiumCoverage.startJSCoverage([options])](#chromiumcoveragestartjscoverageoptions) * [chromiumCoverage.stopCSSCoverage()](#chromiumcoveragestopcsscoverage) * [chromiumCoverage.stopJSCoverage()](#chromiumcoveragestopjscoverage) -- [class: ChromiumInterception](#class-chromiuminterception) - * [chromiumInterception.authenticate(credentials)](#chromiuminterceptionauthenticatecredentials) - * [chromiumInterception.setOfflineMode(enabled)](#chromiuminterceptionsetofflinemodeenabled) - [class: ChromiumOverrides](#class-chromiumoverrides) * [chromiumOverrides.setGeolocation(options)](#chromiumoverridessetgeolocationoptions) - [class: ChromiumPlaywright](#class-chromiumplaywright) @@ -1916,6 +1915,16 @@ Adds a `` tag into the page with the desired url or a ` + - `username` <[string]> + - `password` <[string]> +- returns: <[Promise]> + +Provide credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). + +To disable authentication, pass `null`. + #### page.browserContext() - returns: <[BrowserContext]> @@ -2402,6 +2411,10 @@ The extra HTTP headers will be sent with every request the page initiates. > **NOTE** page.setExtraHTTPHeaders does not guarantee the order of headers in the outgoing requests. +#### page.setOfflineMode(enabled) +- `enabled` <[boolean]> When `true`, enables offline mode for the page. +- returns: <[Promise]> + #### page.setRequestInterception(enabled) - `enabled` <[boolean]> Whether to enable request interception. - returns: <[Promise]> @@ -3134,22 +3147,6 @@ _To output coverage in a form consumable by [Istanbul](https://github.com/istanb > **NOTE** JavaScript Coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are reported. -### class: ChromiumInterception - -#### chromiumInterception.authenticate(credentials) -- `credentials` - - `username` <[string]> - - `password` <[string]> -- returns: <[Promise]> - -Provide credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). - -To disable authentication, pass `null`. - -#### chromiumInterception.setOfflineMode(enabled) -- `enabled` <[boolean]> When `true`, enables offline mode for the page. -- returns: <[Promise]> - ### class: ChromiumOverrides #### chromiumOverrides.setGeolocation(options) diff --git a/package.json b/package.json index 15af0df881..17f31f7b24 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "playwright": { "chromium_revision": "724623", "firefox_revision": "1008", - "webkit_revision": "1053" + "webkit_revision": "1055" }, "scripts": { "unit": "node test/test.js", diff --git a/src/chromium/crApi.ts b/src/chromium/crApi.ts index 7732e2325f..f9647824ad 100644 --- a/src/chromium/crApi.ts +++ b/src/chromium/crApi.ts @@ -8,6 +8,5 @@ export { CRPlaywright as ChromiumPlaywright } from './crPlaywright'; export { CRTarget as ChromiumTarget } from './crTarget'; export { CRAccessibility as ChromiumAccessibility } from './features/crAccessibility'; export { CRCoverage as ChromiumCoverage } from './features/crCoverage'; -export { CRInterception as ChromiumInterception } from './features/crInterception'; export { CROverrides as ChromiumOverrides } from './features/crOverrides'; export { CRWorker as ChromiumWorker } from './features/crWorkers'; diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index a9f85507c3..94a4a2bcdf 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -21,6 +21,7 @@ import { assert, debugError, helper, RegisteredListener } from '../helper'; import { Protocol } from './protocol'; import * as network from '../network'; import * as frames from '../frames'; +import { Credentials } from '../types'; export class CRNetworkManager { private _client: CRSession; @@ -58,14 +59,12 @@ export class CRNetworkManager { helper.removeEventListeners(this._eventListeners); } - async authenticate(credentials: { username: string; password: string; } | null) { + async authenticate(credentials: Credentials | null) { this._credentials = credentials; await this._updateProtocolRequestInterception(); } async setOfflineMode(value: boolean) { - if (this._offline === value) - return; this._offline = value; await this._client.send('Network.emulateNetworkConditions', { offline: this._offline, diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 27112ceac3..33dec12721 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -33,7 +33,6 @@ import { CRAccessibility } from './features/crAccessibility'; import { CRCoverage } from './features/crCoverage'; import { CRPDF, PDFOptions } from './features/crPdf'; import { CRWorkers, CRWorker } from './features/crWorkers'; -import { CRInterception } from './features/crInterception'; import { CRBrowser } from './crBrowser'; import { BrowserContext } from '../browserContext'; import * as types from '../types'; @@ -302,6 +301,14 @@ export class CRPage implements PageDelegate { await this._networkManager.setRequestInterception(enabled); } + async setOfflineMode(value: boolean) { + await this._networkManager.setOfflineMode(value); + } + + async authenticate(credentials: types.Credentials | null) { + await this._networkManager.authenticate(credentials); + } + async reload(): Promise { await this._client.send('Page.reload'); } @@ -456,7 +463,6 @@ export class CRPage implements PageDelegate { export class ChromiumPage extends Page { readonly accessibility: CRAccessibility; readonly coverage: CRCoverage; - readonly interception: CRInterception; private _pdf: CRPDF; private _workers: CRWorkers; _networkManager: CRNetworkManager; @@ -468,7 +474,6 @@ export class ChromiumPage extends Page { this._pdf = new CRPDF(client); this._workers = new CRWorkers(client, this, this._addConsoleMessage.bind(this), error => this.emit(Events.Page.PageError, error)); this._networkManager = new CRNetworkManager(client, this); - this.interception = new CRInterception(this._networkManager); } async pdf(options?: PDFOptions): Promise { diff --git a/src/chromium/features/crInterception.ts b/src/chromium/features/crInterception.ts deleted file mode 100644 index 2731bf1d69..0000000000 --- a/src/chromium/features/crInterception.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { CRNetworkManager } from '../crNetworkManager'; - -export class CRInterception { - private _networkManager: CRNetworkManager; - - constructor(networkManager: CRNetworkManager) { - this._networkManager = networkManager; - } - - setOfflineMode(enabled: boolean) { - return this._networkManager.setOfflineMode(enabled); - } - - async authenticate(credentials: { username: string; password: string; } | null) { - return this._networkManager.authenticate(credentials); - } -} diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 9db8f0252d..bc8330b545 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -216,6 +216,14 @@ export class FFPage implements PageDelegate { await this._networkManager.setRequestInterception(enabled); } + async setOfflineMode(enabled: boolean): Promise { + throw new Error('Offline mode not implemented in Firefox'); + } + + async authenticate(credentials: types.Credentials): Promise { + throw new Error('Offline mode not implemented in Firefox'); + } + async reload(): Promise { await this._session.send('Page.reload', { frameId: this._page.mainFrame()._id }); } diff --git a/src/page.ts b/src/page.ts index 8f9ab144eb..55c2ec00d9 100644 --- a/src/page.ts +++ b/src/page.ts @@ -49,6 +49,8 @@ export interface PageDelegate { setEmulateMedia(mediaType: input.MediaType | null, colorScheme: input.ColorScheme | null): Promise; setCacheEnabled(enabled: boolean): Promise; setRequestInterception(enabled: boolean): Promise; + setOfflineMode(enabled: boolean): Promise; + authenticate(credentials: types.Credentials | null): Promise; getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise; canScreenshotOutsideViewport(): boolean; @@ -73,6 +75,8 @@ type PageState = { extraHTTPHeaders: network.Headers | null; cacheEnabled: boolean | null; interceptNetwork: boolean | null; + offlineMode: boolean | null; + credentials: types.Credentials | null; }; export type FileChooser = { @@ -109,7 +113,9 @@ export class Page extends EventEmitter { colorScheme: browserContext._options.colorScheme || null, extraHTTPHeaders: null, cacheEnabled: null, - interceptNetwork: null + interceptNetwork: null, + offlineMode: null, + credentials: null }; this.keyboard = new input.Keyboard(delegate.rawKeyboard); this.mouse = new input.Mouse(delegate.rawMouse, this.keyboard); @@ -401,6 +407,18 @@ export class Page extends EventEmitter { await this._delegate.setRequestInterception(enabled); } + async setOfflineMode(enabled: boolean) { + if (this._state.offlineMode === enabled) + return; + this._state.offlineMode = enabled; + await this._delegate.setOfflineMode(enabled); + } + + async authenticate(credentials: types.Credentials | null) { + this._state.credentials = credentials; + await this._delegate.authenticate(credentials); + } + async screenshot(options?: types.ScreenshotOptions): Promise { return this._screenshotter.screenshotPage(options); } diff --git a/src/types.ts b/src/types.ts index 53ebcb15b3..d6c94d714e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,3 +51,8 @@ export type Viewport = { }; export type URLMatch = string | RegExp | ((url: kurl.URL) => boolean); + +export type Credentials = { + username: string; + password: string; +} diff --git a/src/webkit/wkNetworkManager.ts b/src/webkit/wkNetworkManager.ts index c28c58920f..707904125c 100644 --- a/src/webkit/wkNetworkManager.ts +++ b/src/webkit/wkNetworkManager.ts @@ -21,6 +21,7 @@ import { helper, RegisteredListener, assert } from '../helper'; import { Protocol } from './protocol'; import * as network from '../network'; import * as frames from '../frames'; +import * as types from '../types'; export class WKNetworkManager { private _session: WKTargetSession; @@ -45,11 +46,15 @@ export class WKNetworkManager { ]; } - async initializeSession(session: WKTargetSession, enableInterception: boolean) { + async initializeSession(session: WKTargetSession, interceptNetwork: boolean | null, offlineMode: boolean | null, credentials: types.Credentials | null) { const promises = []; promises.push(session.send('Network.enable')); - if (enableInterception) + if (interceptNetwork) promises.push(session.send('Network.setInterceptionEnabled', { enabled: true })); + if (offlineMode) + promises.push(session.send('Network.setEmulateOfflineState', { offline: true })); + if (credentials) + promises.push(session.send('Emulation.setAuthCredentials', { ...credentials })); await Promise.all(promises); } @@ -151,12 +156,12 @@ export class WKNetworkManager { this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled')); } - authenticate(credentials: { username: string; password: string; }) { - throw new Error('Not implemented'); + async authenticate(credentials: types.Credentials | null) { + await this._session.send('Emulation.setAuthCredentials', { ...(credentials || {}) }); } - setOfflineMode(enabled: boolean) { - throw new Error('Not implemented'); + async setOfflineMode(value: boolean): Promise { + await this._session.send('Network.setEmulateOfflineState', { offline: value }); } } diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 952b3e94b6..ddc167bcc6 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -85,7 +85,7 @@ export class WKPage implements PageDelegate { session.send('Runtime.enable').then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)), session.send('Console.enable'), session.send('Page.setInterceptFileChooserDialog', { enabled: true }), - this._networkManager.initializeSession(session, this._page._state.interceptNetwork), + this._networkManager.initializeSession(session, this._page._state.interceptNetwork, this._page._state.offlineMode, this._page._state.credentials), ]; if (!session.isProvisional()) { // FIXME: move dialog agent to web process. @@ -309,6 +309,14 @@ export class WKPage implements PageDelegate { return this._networkManager.setRequestInterception(enabled); } + async setOfflineMode(value: boolean) { + await this._networkManager.setOfflineMode(value); + } + + async authenticate(credentials: types.Credentials | null) { + await this._networkManager.authenticate(credentials); + } + async reload(): Promise { await this._session.send('Page.reload'); } diff --git a/test/interception.spec.js b/test/interception.spec.js index 10109ae4e3..8aee868cc1 100644 --- a/test/interception.spec.js +++ b/test/interception.spec.js @@ -495,12 +495,12 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); }); - describe.skip(FFOX || WEBKIT)('Interception.authenticate', function() { + describe.skip(FFOX)('Interception.authenticate', function() { it('should work', async({page, server}) => { server.setAuth('/empty.html', 'user', 'pass'); let response = await page.goto(server.EMPTY_PAGE); expect(response.status()).toBe(401); - await page.interception.authenticate({ + await page.authenticate({ username: 'user', password: 'pass' }); @@ -510,7 +510,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p it('should fail if wrong credentials', async({page, server}) => { // Use unique user/password since Chrome caches credentials per origin. server.setAuth('/empty.html', 'user2', 'pass2'); - await page.interception.authenticate({ + await page.authenticate({ username: 'foo', password: 'bar' }); @@ -520,34 +520,34 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p it('should allow disable authentication', async({page, server}) => { // Use unique user/password since Chrome caches credentials per origin. server.setAuth('/empty.html', 'user3', 'pass3'); - await page.interception.authenticate({ + await page.authenticate({ username: 'user3', password: 'pass3' }); let response = await page.goto(server.EMPTY_PAGE); expect(response.status()).toBe(200); - await page.interception.authenticate(null); + await page.authenticate(null); // Navigate to a different origin to bust Chrome's credential caching. response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); expect(response.status()).toBe(401); }); }); - describe.skip(FFOX || WEBKIT)('Interception.setOfflineMode', function() { + describe.skip(FFOX)('Interception.setOfflineMode', function() { it('should work', async({page, server}) => { - await page.interception.setOfflineMode(true); + await page.setOfflineMode(true); let error = null; await page.goto(server.EMPTY_PAGE).catch(e => error = e); expect(error).toBeTruthy(); - await page.interception.setOfflineMode(false); - const response = await page.reload(); + await page.setOfflineMode(false); + const response = await page.goto(server.EMPTY_PAGE); expect(response.status()).toBe(200); }); it('should emulate navigator.onLine', async({page, server}) => { expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); - await page.interception.setOfflineMode(true); + await page.setOfflineMode(true); expect(await page.evaluate(() => window.navigator.onLine)).toBe(false); - await page.interception.setOfflineMode(false); + await page.setOfflineMode(false); expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); }); }); diff --git a/test/screenshot.spec.js b/test/screenshot.spec.js index 75e37ab85c..7e3664018b 100644 --- a/test/screenshot.spec.js +++ b/test/screenshot.spec.js @@ -201,7 +201,7 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROME, W expect(await page.evaluate(() => ({ w: window.innerWidth, h: window.innerHeight }))).toEqual({ w: 500, h: 500 }); }); // Fails on GTK due to async setViewport. - it.skip(WEBKIT)('should capture full element when larger than viewport', async({page, server}) => { + it('should capture full element when larger than viewport', async({page, server}) => { await page.setViewport({width: 500, height: 500}); await page.setContent(`