diff --git a/src/browserContext.ts b/src/browserContext.ts index 346a9d92f38f0..e0744e2f2a2d6 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -19,26 +19,8 @@ import { Page } from './page'; import * as network from './network'; import * as types from './types'; import { helper } from './helper'; -import * as platform from './platform'; -import { Events } from './events'; import { TimeoutSettings } from './timeoutSettings'; -export interface BrowserContextDelegate { - pages(): Promise; - existingPages(): Page[]; - newPage(): Promise; - close(): Promise; - - cookies(): Promise; - setCookies(cookies: network.SetNetworkCookieParam[]): Promise; - clearCookies(): Promise; - - setPermissions(origin: string, permissions: string[]): Promise; - clearPermissions(): Promise; - - setGeolocation(geolocation: types.Geolocation | null): Promise; -} - export type BrowserContextOptions = { viewport?: types.Viewport | null, ignoreHTTPSErrors?: boolean, @@ -51,106 +33,44 @@ export type BrowserContextOptions = { permissions?: { [key: string]: string[] }; }; -export class BrowserContext extends platform.EventEmitter { - private readonly _delegate: BrowserContextDelegate; - readonly _options: BrowserContextOptions; - readonly _timeoutSettings: TimeoutSettings; - private _closed = false; - - constructor(delegate: BrowserContextDelegate, options: BrowserContextOptions) { - super(); - this._delegate = delegate; - this._timeoutSettings = new TimeoutSettings(); - this._options = { ...options }; - if (!this._options.viewport && this._options.viewport !== null) - this._options.viewport = { width: 1280, height: 720 }; - if (this._options.viewport) - this._options.viewport = { ...this._options.viewport }; - if (this._options.geolocation) - this._options.geolocation = verifyGeolocation(this._options.geolocation); - } - - async _initialize() { - const entries = Object.entries(this._options.permissions || {}); - await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); - if (this._options.geolocation) - await this.setGeolocation(this._options.geolocation); - } - - _existingPages(): Page[] { - return this._delegate.existingPages(); - } - - setDefaultNavigationTimeout(timeout: number) { - this._timeoutSettings.setDefaultNavigationTimeout(timeout); - } - - setDefaultTimeout(timeout: number) { - this._timeoutSettings.setDefaultTimeout(timeout); - } - - async pages(): Promise { - return this._delegate.pages(); - } - - async newPage(): Promise { - const pages = this._delegate.existingPages(); - for (const page of pages) { - if (page._ownedContext) - throw new Error('Please use browser.newContext() for multi-page scripts that share the context.'); - } - return this._delegate.newPage(); - } - - async cookies(...urls: string[]): Promise { - return network.filterCookies(await this._delegate.cookies(), urls); - } - - async setCookies(cookies: network.SetNetworkCookieParam[]) { - await this._delegate.setCookies(network.rewriteCookies(cookies)); - } - - async clearCookies() { - await this._delegate.clearCookies(); - } - - async setPermissions(origin: string, permissions: string[]): Promise { - await this._delegate.setPermissions(origin, permissions); - } - - async clearPermissions() { - await this._delegate.clearPermissions(); - } - - async setGeolocation(geolocation: types.Geolocation | null): Promise { - if (geolocation) - geolocation = verifyGeolocation(geolocation); - this._options.geolocation = geolocation || undefined; - await this._delegate.setGeolocation(geolocation); - } +export interface BrowserContext { + setDefaultNavigationTimeout(timeout: number): void; + setDefaultTimeout(timeout: number): void; + pages(): Promise; + newPage(): Promise; + cookies(...urls: string[]): Promise; + setCookies(cookies: network.SetNetworkCookieParam[]): Promise; + clearCookies(): Promise; + setPermissions(origin: string, permissions: string[]): Promise; + clearPermissions(): Promise; + setGeolocation(geolocation: types.Geolocation | null): Promise; + close(): Promise; - async close() { - if (this._closed) - return; - await this._delegate.close(); - this._closed = true; - this.emit(Events.BrowserContext.Close); - } + _existingPages(): Page[]; + readonly _timeoutSettings: TimeoutSettings; + readonly _options: BrowserContextOptions; +} - static validateOptions(options: BrowserContextOptions) { - if (options.geolocation) - verifyGeolocation(options.geolocation); +export function assertBrowserContextIsNotOwned(context: BrowserContext) { + const pages = context._existingPages(); + for (const page of pages) { + if (page._ownedContext) + throw new Error('Please use browser.newContext() for multi-page scripts that share the context.'); } +} - _browserClosed() { - this._closed = true; - for (const page of this._delegate.existingPages()) - page._didClose(); - this.emit(Events.BrowserContext.Close); - } +export function validateBrowserContextOptions(options: BrowserContextOptions): BrowserContextOptions { + const result = { ...options }; + if (!result.viewport && result.viewport !== null) + result.viewport = { width: 1280, height: 720 }; + if (result.viewport) + result.viewport = { ...result.viewport }; + if (result.geolocation) + result.geolocation = verifyGeolocation(result.geolocation); + return result; } -function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation { +export function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation { const result = { ...geolocation }; result.accuracy = result.accuracy || 0; const { longitude, latitude, accuracy } = result; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index d04843a346b65..15d1e6519838c 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -18,7 +18,7 @@ import { Events } from './events'; import { Events as CommonEvents } from '../events'; import { assert, helper } from '../helper'; -import { BrowserContext, BrowserContextOptions } from '../browserContext'; +import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext'; import { CRConnection, ConnectionEvents, CRSession } from './crConnection'; import { Page } from '../page'; import { CRTarget } from './crTarget'; @@ -30,12 +30,13 @@ import * as types from '../types'; import * as platform from '../platform'; import { readProtocolStream } from './crProtocolHelper'; import { ConnectionTransport, SlowMoTransport } from '../transport'; +import { TimeoutSettings } from '../timeoutSettings'; export class CRBrowser extends platform.EventEmitter implements Browser { _connection: CRConnection; _client: CRSession; readonly _defaultContext: BrowserContext; - private _contexts = new Map(); + readonly _contexts = new Map(); _targets = new Map(); private _tracingRecording = false; @@ -54,9 +55,9 @@ export class CRBrowser extends platform.EventEmitter implements Browser { this._connection = connection; this._client = connection.rootSession; - this._defaultContext = this._createBrowserContext(null, {}); + this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({})); this._connection.on(ConnectionEvents.Disconnected, () => { - for (const context of this.contexts()) + for (const context of this._contexts.values()) context._browserClosed(); this.emit(CommonEvents.Browser.Disconnected); }); @@ -65,99 +66,10 @@ export class CRBrowser extends platform.EventEmitter implements Browser { this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); } - _createBrowserContext(contextId: string | null, options: BrowserContextOptions): BrowserContext { - const context = new BrowserContext({ - pages: async (): Promise => { - const targets = this._allTargets().filter(target => target.context() === context && target.type() === 'page'); - const pages = await Promise.all(targets.map(target => target.page())); - return pages.filter(page => !!page) as Page[]; - }, - - existingPages: (): Page[] => { - const pages: Page[] = []; - for (const target of this._allTargets()) { - if (target.context() === context && target._crPage) - pages.push(target._crPage.page()); - } - return pages; - }, - - newPage: async (): Promise => { - const { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined }); - const target = this._targets.get(targetId)!; - assert(await target._initializedPromise, 'Failed to create target for page'); - const page = await target.page(); - return page!; - }, - - close: async (): Promise => { - assert(contextId, 'Non-incognito profiles cannot be closed!'); - await this._client.send('Target.disposeBrowserContext', { browserContextId: contextId }); - this._contexts.delete(contextId); - }, - - cookies: async (): Promise => { - const { cookies } = await this._client.send('Storage.getCookies', { browserContextId: contextId || undefined }); - return cookies.map(c => { - const copy: any = { sameSite: 'None', ...c }; - delete copy.size; - delete copy.priority; - return copy as network.NetworkCookie; - }); - }, - - clearCookies: async (): Promise => { - await this._client.send('Storage.clearCookies', { browserContextId: contextId || undefined }); - }, - - setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { - await this._client.send('Storage.setCookies', { cookies, browserContextId: contextId || undefined }); - }, - - setPermissions: async (origin: string, permissions: string[]): Promise => { - const webPermissionToProtocol = new Map([ - ['geolocation', 'geolocation'], - ['midi', 'midi'], - ['notifications', 'notifications'], - ['camera', 'videoCapture'], - ['microphone', 'audioCapture'], - ['background-sync', 'backgroundSync'], - ['ambient-light-sensor', 'sensors'], - ['accelerometer', 'sensors'], - ['gyroscope', 'sensors'], - ['magnetometer', 'sensors'], - ['accessibility-events', 'accessibilityEvents'], - ['clipboard-read', 'clipboardReadWrite'], - ['clipboard-write', 'clipboardSanitizedWrite'], - ['payment-handler', 'paymentHandler'], - // chrome-specific permissions we have. - ['midi-sysex', 'midiSysex'], - ]); - const filtered = permissions.map(permission => { - const protocolPermission = webPermissionToProtocol.get(permission); - if (!protocolPermission) - throw new Error('Unknown permission: ' + permission); - return protocolPermission; - }); - await this._client.send('Browser.grantPermissions', { origin, browserContextId: contextId || undefined, permissions: filtered }); - }, - - clearPermissions: async () => { - await this._client.send('Browser.resetPermissions', { browserContextId: contextId || undefined }); - }, - - setGeolocation: async (geolocation: types.Geolocation | null): Promise => { - for (const page of await context.pages()) - await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {}); - } - }, options); - return context; - } - async newContext(options: BrowserContextOptions = {}): Promise { - BrowserContext.validateOptions(options); + options = validateBrowserContextOptions(options); const { browserContextId } = await this._client.send('Target.createBrowserContext'); - const context = this._createBrowserContext(browserContextId, options); + const context = new CRBrowserContext(this, browserContextId, options); await context._initialize(); this._contexts.set(browserContextId, context); return context; @@ -304,3 +216,133 @@ export class CRBrowser extends platform.EventEmitter implements Browser { this._connection._debugProtocol = debugFunction; } } + +export class CRBrowserContext extends platform.EventEmitter implements BrowserContext { + readonly _browser: CRBrowser; + readonly _browserContextId: string | null; + readonly _options: BrowserContextOptions; + readonly _timeoutSettings: TimeoutSettings; + private _closed = false; + + constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) { + super(); + this._browser = browser; + this._browserContextId = browserContextId; + this._timeoutSettings = new TimeoutSettings(); + this._options = options; + } + + async _initialize() { + const entries = Object.entries(this._options.permissions || {}); + await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.geolocation) + await this.setGeolocation(this._options.geolocation); + } + + _existingPages(): Page[] { + const pages: Page[] = []; + for (const target of this._browser._allTargets()) { + if (target.context() === this && target._crPage) + pages.push(target._crPage.page()); + } + return pages; + } + + setDefaultNavigationTimeout(timeout: number) { + this._timeoutSettings.setDefaultNavigationTimeout(timeout); + } + + setDefaultTimeout(timeout: number) { + this._timeoutSettings.setDefaultTimeout(timeout); + } + + async pages(): Promise { + const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page'); + const pages = await Promise.all(targets.map(target => target.page())); + return pages.filter(page => !!page) as Page[]; + } + + async newPage(): Promise { + assertBrowserContextIsNotOwned(this); + const { targetId } = await this._browser._client.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined }); + const target = this._browser._targets.get(targetId)!; + assert(await target._initializedPromise, 'Failed to create target for page'); + const page = await target.page(); + return page!; + } + + async cookies(...urls: string[]): Promise { + const { cookies } = await this._browser._client.send('Storage.getCookies', { browserContextId: this._browserContextId || undefined }); + return network.filterCookies(cookies.map(c => { + const copy: any = { sameSite: 'None', ...c }; + delete copy.size; + delete copy.priority; + return copy as network.NetworkCookie; + }), urls); + } + + async setCookies(cookies: network.SetNetworkCookieParam[]) { + await this._browser._client.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId || undefined }); + } + + async clearCookies() { + await this._browser._client.send('Storage.clearCookies', { browserContextId: this._browserContextId || undefined }); + } + + async setPermissions(origin: string, permissions: string[]): Promise { + const webPermissionToProtocol = new Map([ + ['geolocation', 'geolocation'], + ['midi', 'midi'], + ['notifications', 'notifications'], + ['camera', 'videoCapture'], + ['microphone', 'audioCapture'], + ['background-sync', 'backgroundSync'], + ['ambient-light-sensor', 'sensors'], + ['accelerometer', 'sensors'], + ['gyroscope', 'sensors'], + ['magnetometer', 'sensors'], + ['accessibility-events', 'accessibilityEvents'], + ['clipboard-read', 'clipboardReadWrite'], + ['clipboard-write', 'clipboardSanitizedWrite'], + ['payment-handler', 'paymentHandler'], + // chrome-specific permissions we have. + ['midi-sysex', 'midiSysex'], + ]); + const filtered = permissions.map(permission => { + const protocolPermission = webPermissionToProtocol.get(permission); + if (!protocolPermission) + throw new Error('Unknown permission: ' + permission); + return protocolPermission; + }); + await this._browser._client.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId || undefined, permissions: filtered }); + } + + async clearPermissions() { + await this._browser._client.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined }); + } + + async setGeolocation(geolocation: types.Geolocation | null): Promise { + if (geolocation) + geolocation = verifyGeolocation(geolocation); + this._options.geolocation = geolocation || undefined; + for (const page of this._existingPages()) + await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {}); + } + + async close() { + if (this._closed) + return; + assert(this._browserContextId, 'Non-incognito profiles cannot be closed!'); + await this._browser._client.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId }); + this._browser._contexts.delete(this._browserContextId); + this._closed = true; + this.emit(CommonEvents.BrowserContext.Close); + } + + _browserClosed() { + this._closed = true; + for (const page of this._existingPages()) + page._didClose(); + this.emit(CommonEvents.BrowserContext.Close); + } +} diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index a684efd336971..e3141753c4650 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -16,7 +16,7 @@ */ import { Browser, createPageInNewContext } from '../browser'; -import { BrowserContext, BrowserContextOptions } from '../browserContext'; +import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned } from '../browserContext'; import { Events } from '../events'; import { assert, helper, RegisteredListener, debugError } from '../helper'; import * as network from '../network'; @@ -27,12 +27,13 @@ import { FFPage } from './ffPage'; import * as platform from '../platform'; import { Protocol } from './protocol'; import { ConnectionTransport, SlowMoTransport } from '../transport'; +import { TimeoutSettings } from '../timeoutSettings'; export class FFBrowser extends platform.EventEmitter implements Browser { _connection: FFConnection; _targets: Map; readonly _defaultContext: BrowserContext; - private _contexts: Map; + readonly _contexts: Map; private _eventListeners: RegisteredListener[]; static async connect(transport: ConnectionTransport, slowMo?: number): Promise { @@ -47,10 +48,10 @@ export class FFBrowser extends platform.EventEmitter implements Browser { this._connection = connection; this._targets = new Map(); - this._defaultContext = this._createBrowserContext(null, {}); + this._defaultContext = new FFBrowserContext(this, null, validateBrowserContextOptions({})); this._contexts = new Map(); this._connection.on(ConnectionEvents.Disconnected, () => { - for (const context of this.contexts()) + for (const context of this._contexts.values()) context._browserClosed(); this.emit(Events.Browser.Disconnected); }); @@ -67,6 +68,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser { } async newContext(options: BrowserContextOptions = {}): Promise { + options = validateBrowserContextOptions(options); let viewport; if (options.viewport) { viewport = { @@ -92,7 +94,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser { // TODO: move ignoreHTTPSErrors to browser context level. if (options.ignoreHTTPSErrors) await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true }); - const context = this._createBrowserContext(browserContextId, options); + const context = new FFBrowserContext(this, browserContextId, options); await context._initialize(); this._contexts.set(browserContextId, context); return context; @@ -176,82 +178,6 @@ export class FFBrowser extends platform.EventEmitter implements Browser { await disconnected; } - _createBrowserContext(browserContextId: string | null, options: BrowserContextOptions): BrowserContext { - BrowserContext.validateOptions(options); - const context = new BrowserContext({ - pages: async (): Promise => { - const targets = this._allTargets().filter(target => target.context() === context && target.type() === 'page'); - const pages = await Promise.all(targets.map(target => target.page())); - return pages.filter(page => !!page); - }, - - existingPages: (): Page[] => { - const pages: Page[] = []; - for (const target of this._allTargets()) { - if (target.context() === context && target._ffPage) - pages.push(target._ffPage._page); - } - return pages; - }, - - newPage: async (): Promise => { - const {targetId} = await this._connection.send('Target.newPage', { - browserContextId: browserContextId || undefined - }); - const target = this._targets.get(targetId)!; - return target.page(); - }, - - close: async (): Promise => { - assert(browserContextId, 'Non-incognito profiles cannot be closed!'); - await this._connection.send('Target.removeBrowserContext', { browserContextId }); - this._contexts.delete(browserContextId); - }, - - cookies: async (): Promise => { - const { cookies } = await this._connection.send('Browser.getCookies', { browserContextId: browserContextId || undefined }); - return cookies.map(c => { - const copy: any = { ... c }; - delete copy.size; - return copy as network.NetworkCookie; - }); - }, - - clearCookies: async (): Promise => { - await this._connection.send('Browser.clearCookies', { browserContextId: browserContextId || undefined }); - }, - - setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { - await this._connection.send('Browser.setCookies', { browserContextId: browserContextId || undefined, cookies }); - }, - - setPermissions: async (origin: string, permissions: string[]): Promise => { - const webPermissionToProtocol = new Map([ - ['geolocation', 'geo'], - ['microphone', 'microphone'], - ['camera', 'camera'], - ['notifications', 'desktop-notifications'], - ]); - const filtered = permissions.map(permission => { - const protocolPermission = webPermissionToProtocol.get(permission); - if (!protocolPermission) - throw new Error('Unknown permission: ' + permission); - return protocolPermission; - }); - await this._connection.send('Browser.grantPermissions', {origin, browserContextId: browserContextId || undefined, permissions: filtered}); - }, - - clearPermissions: async () => { - await this._connection.send('Browser.resetPermissions', { browserContextId: browserContextId || undefined }); - }, - - setGeolocation: async (geolocation: types.Geolocation | null): Promise => { - throw new Error('Geolocation emulation is not supported in Firefox'); - } - }, options); - return context; - } - _setDebugFunction(debugFunction: (message: string) => void) { this._connection._debugProtocol = debugFunction; } @@ -326,3 +252,116 @@ class Target { return this._browser; } } + +export class FFBrowserContext extends platform.EventEmitter implements BrowserContext { + readonly _browser: FFBrowser; + readonly _browserContextId: string | null; + readonly _options: BrowserContextOptions; + readonly _timeoutSettings: TimeoutSettings; + private _closed = false; + + constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) { + super(); + this._browser = browser; + this._browserContextId = browserContextId; + this._timeoutSettings = new TimeoutSettings(); + this._options = options; + } + + async _initialize() { + const entries = Object.entries(this._options.permissions || {}); + await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.geolocation) + await this.setGeolocation(this._options.geolocation); + } + + _existingPages(): Page[] { + const pages: Page[] = []; + for (const target of this._browser._allTargets()) { + if (target.context() === this && target._ffPage) + pages.push(target._ffPage._page); + } + return pages; + } + + setDefaultNavigationTimeout(timeout: number) { + this._timeoutSettings.setDefaultNavigationTimeout(timeout); + } + + setDefaultTimeout(timeout: number) { + this._timeoutSettings.setDefaultTimeout(timeout); + } + + async pages(): Promise { + const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page'); + const pages = await Promise.all(targets.map(target => target.page())); + return pages.filter(page => !!page); + } + + async newPage(): Promise { + assertBrowserContextIsNotOwned(this); + const {targetId} = await this._browser._connection.send('Target.newPage', { + browserContextId: this._browserContextId || undefined + }); + const target = this._browser._targets.get(targetId)!; + return target.page(); + } + + async cookies(...urls: string[]): Promise { + const { cookies } = await this._browser._connection.send('Browser.getCookies', { browserContextId: this._browserContextId || undefined }); + return network.filterCookies(cookies.map(c => { + const copy: any = { ... c }; + delete copy.size; + return copy as network.NetworkCookie; + }), urls); + } + + async setCookies(cookies: network.SetNetworkCookieParam[]) { + await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId || undefined, cookies: network.rewriteCookies(cookies) }); + } + + async clearCookies() { + await this._browser._connection.send('Browser.clearCookies', { browserContextId: this._browserContextId || undefined }); + } + + async setPermissions(origin: string, permissions: string[]): Promise { + const webPermissionToProtocol = new Map([ + ['geolocation', 'geo'], + ['microphone', 'microphone'], + ['camera', 'camera'], + ['notifications', 'desktop-notifications'], + ]); + const filtered = permissions.map(permission => { + const protocolPermission = webPermissionToProtocol.get(permission); + if (!protocolPermission) + throw new Error('Unknown permission: ' + permission); + return protocolPermission; + }); + await this._browser._connection.send('Browser.grantPermissions', {origin, browserContextId: this._browserContextId || undefined, permissions: filtered}); + } + + async clearPermissions() { + await this._browser._connection.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined }); + } + + async setGeolocation(geolocation: types.Geolocation | null): Promise { + throw new Error('Geolocation emulation is not supported in Firefox'); + } + + async close() { + if (this._closed) + return; + assert(this._browserContextId, 'Non-incognito profiles cannot be closed!'); + await this._browser._connection.send('Target.removeBrowserContext', { browserContextId: this._browserContextId }); + this._browser._contexts.delete(this._browserContextId); + this._closed = true; + this.emit(Events.BrowserContext.Close); + } + + _browserClosed() { + this._closed = true; + for (const page of this._existingPages()) + page._didClose(); + this.emit(Events.BrowserContext.Close); + } +} diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 3b93574441777..2a62a6f3256ce 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -16,7 +16,7 @@ */ import { Browser, createPageInNewContext } from '../browser'; -import { BrowserContext, BrowserContextOptions } from '../browserContext'; +import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext'; import { assert, helper, RegisteredListener } from '../helper'; import * as network from '../network'; import { Page } from '../page'; @@ -27,15 +27,16 @@ import { Protocol } from './protocol'; import { WKConnection, WKSession, kPageProxyMessageReceived, PageProxyMessageReceivedPayload } from './wkConnection'; import { WKPageProxy } from './wkPageProxy'; import * as platform from '../platform'; +import { TimeoutSettings } from '../timeoutSettings'; const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15'; export class WKBrowser extends platform.EventEmitter implements Browser { private readonly _connection: WKConnection; - private readonly _browserSession: WKSession; + readonly _browserSession: WKSession; readonly _defaultContext: BrowserContext; - private readonly _contexts = new Map(); - private readonly _pageProxies = new Map(); + readonly _contexts = new Map(); + readonly _pageProxies = new Map(); private readonly _eventListeners: RegisteredListener[]; private _firstPageProxyCallback?: () => void; @@ -51,7 +52,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser { this._connection = new WKConnection(transport, this._onDisconnect.bind(this)); this._browserSession = this._connection.browserSession; - this._defaultContext = this._createBrowserContext(undefined, {}); + this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({})); this._eventListeners = [ helper.addEventListener(this._browserSession, 'Browser.pageProxyCreated', this._onPageProxyCreated.bind(this)), @@ -64,7 +65,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser { } _onDisconnect() { - for (const context of this.contexts()) + for (const context of this._contexts.values()) context._browserClosed(); for (const pageProxy of this._pageProxies.values()) pageProxy.dispose(); @@ -73,13 +74,10 @@ export class WKBrowser extends platform.EventEmitter implements Browser { } async newContext(options: BrowserContextOptions = {}): Promise { + options = validateBrowserContextOptions(options); const { browserContextId } = await this._browserSession.send('Browser.createContext'); options.userAgent = options.userAgent || DEFAULT_USER_AGENT; - const context = this._createBrowserContext(browserContextId, options); - if (options.ignoreHTTPSErrors) - await this._browserSession.send('Browser.setIgnoreCertificateErrors', { browserContextId, ignore: true }); - if (options.locale) - await this._browserSession.send('Browser.setLanguages', { browserContextId, languages: [options.locale] }); + const context = new WKBrowserContext(this, browserContextId, options); await context._initialize(); this._contexts.set(browserContextId, context); return context; @@ -166,81 +164,125 @@ export class WKBrowser extends platform.EventEmitter implements Browser { await disconnected; } - _createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext { - BrowserContext.validateOptions(options); - const context = new BrowserContext({ - pages: async (): Promise => { - const pageProxies = Array.from(this._pageProxies.values()).filter(proxy => proxy._browserContext === context); - return await Promise.all(pageProxies.map(proxy => proxy.page())); - }, - - existingPages: (): Page[] => { - const pages: Page[] = []; - for (const pageProxy of this._pageProxies.values()) { - if (pageProxy._browserContext !== context) - continue; - const page = pageProxy.existingPage(); - if (page) - pages.push(page); - } - return pages; - }, - - newPage: async (): Promise => { - const { pageProxyId } = await this._browserSession.send('Browser.createPage', { browserContextId }); - const pageProxy = this._pageProxies.get(pageProxyId)!; - return await pageProxy.page(); - }, - - close: async (): Promise => { - assert(browserContextId, 'Non-incognito profiles cannot be closed!'); - await this._browserSession.send('Browser.deleteContext', { browserContextId: browserContextId }); - this._contexts.delete(browserContextId); - }, - - cookies: async (): Promise => { - const { cookies } = await this._browserSession.send('Browser.getAllCookies', { browserContextId }); - return cookies.map((c: network.NetworkCookie) => ({ - ...c, - expires: c.expires === 0 ? -1 : c.expires - })); - }, - - clearCookies: async (): Promise => { - await this._browserSession.send('Browser.deleteAllCookies', { browserContextId }); - }, - - setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { - const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[]; - await this._browserSession.send('Browser.setCookies', { cookies: cc, browserContextId }); - }, - - setPermissions: async (origin: string, permissions: string[]): Promise => { - const webPermissionToProtocol = new Map([ - ['geolocation', 'geolocation'], - ]); - const filtered = permissions.map(permission => { - const protocolPermission = webPermissionToProtocol.get(permission); - if (!protocolPermission) - throw new Error('Unknown permission: ' + permission); - return protocolPermission; - }); - await this._browserSession.send('Browser.grantPermissions', { origin, browserContextId, permissions: filtered }); - }, - - clearPermissions: async () => { - await this._browserSession.send('Browser.resetPermissions', { browserContextId }); - }, - - setGeolocation: async (geolocation: types.Geolocation | null): Promise => { - const payload: any = geolocation ? { ...geolocation, timestamp: Date.now() } : undefined; - await this._browserSession.send('Browser.setGeolocationOverride', { browserContextId, geolocation: payload }); - } - }, options); - return context; - } - _setDebugFunction(debugFunction: (message: string) => void) { this._connection._debugFunction = debugFunction; } } + +export class WKBrowserContext extends platform.EventEmitter implements BrowserContext { + readonly _browser: WKBrowser; + readonly _browserContextId: string | undefined; + readonly _options: BrowserContextOptions; + readonly _timeoutSettings: TimeoutSettings; + private _closed = false; + + constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) { + super(); + this._browser = browser; + this._browserContextId = browserContextId; + this._timeoutSettings = new TimeoutSettings(); + this._options = options; + } + + async _initialize() { + if (this._options.ignoreHTTPSErrors) + await this._browser._browserSession.send('Browser.setIgnoreCertificateErrors', { browserContextId: this._browserContextId, ignore: true }); + if (this._options.locale) + await this._browser._browserSession.send('Browser.setLanguages', { browserContextId: this._browserContextId, languages: [this._options.locale] }); + const entries = Object.entries(this._options.permissions || {}); + await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.geolocation) + await this.setGeolocation(this._options.geolocation); + } + + _existingPages(): Page[] { + const pages: Page[] = []; + for (const pageProxy of this._browser._pageProxies.values()) { + if (pageProxy._browserContext !== this) + continue; + const page = pageProxy.existingPage(); + if (page) + pages.push(page); + } + return pages; + } + + setDefaultNavigationTimeout(timeout: number) { + this._timeoutSettings.setDefaultNavigationTimeout(timeout); + } + + setDefaultTimeout(timeout: number) { + this._timeoutSettings.setDefaultTimeout(timeout); + } + + async pages(): Promise { + const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this); + return await Promise.all(pageProxies.map(proxy => proxy.page())); + } + + async newPage(): Promise { + assertBrowserContextIsNotOwned(this); + const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId }); + const pageProxy = this._browser._pageProxies.get(pageProxyId)!; + return await pageProxy.page(); + } + + async cookies(...urls: string[]): Promise { + const { cookies } = await this._browser._browserSession.send('Browser.getAllCookies', { browserContextId: this._browserContextId }); + return network.filterCookies(cookies.map((c: network.NetworkCookie) => ({ + ...c, + expires: c.expires === 0 ? -1 : c.expires + })), urls); + } + + async setCookies(cookies: network.SetNetworkCookieParam[]) { + const cc = network.rewriteCookies(cookies).map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[]; + await this._browser._browserSession.send('Browser.setCookies', { cookies: cc, browserContextId: this._browserContextId }); + } + + async clearCookies() { + await this._browser._browserSession.send('Browser.deleteAllCookies', { browserContextId: this._browserContextId }); + } + + async setPermissions(origin: string, permissions: string[]): Promise { + const webPermissionToProtocol = new Map([ + ['geolocation', 'geolocation'], + ]); + const filtered = permissions.map(permission => { + const protocolPermission = webPermissionToProtocol.get(permission); + if (!protocolPermission) + throw new Error('Unknown permission: ' + permission); + return protocolPermission; + }); + await this._browser._browserSession.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId, permissions: filtered }); + } + + async clearPermissions() { + await this._browser._browserSession.send('Browser.resetPermissions', { browserContextId: this._browserContextId }); + } + + async setGeolocation(geolocation: types.Geolocation | null): Promise { + if (geolocation) + geolocation = verifyGeolocation(geolocation); + this._options.geolocation = geolocation || undefined; + const payload: any = geolocation ? { ...geolocation, timestamp: Date.now() } : undefined; + await this._browser._browserSession.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload }); + } + + async close() { + if (this._closed) + return; + assert(this._browserContextId, 'Non-incognito profiles cannot be closed!'); + await this._browser._browserSession.send('Browser.deleteContext', { browserContextId: this._browserContextId }); + this._browser._contexts.delete(this._browserContextId); + this._closed = true; + this.emit(Events.BrowserContext.Close); + } + + _browserClosed() { + this._closed = true; + for (const page of this._existingPages()) + page._didClose(); + this.emit(Events.BrowserContext.Close); + } +}