/** * Copyright 2018 Google Inc. All rights reserved. * Modifications copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as browser from '../browser'; import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { Events } from '../events'; import { assert, helper, RegisteredListener } from '../helper'; import * as network from '../network'; import { Page } from '../page'; import { ConnectionTransport } from '../transport'; import { ConnectionEvents, FFConnection, FFSessionEvents } from './ffConnection'; import { FFFrameManager } from './ffFrameManager'; export class FFBrowser extends browser.Browser { _connection: FFConnection; _targets: Map; private _defaultContext: BrowserContext; private _contexts: Map; private _eventListeners: RegisteredListener[]; static async create(transport: ConnectionTransport) { const connection = new FFConnection(transport); const {browserContextIds} = await connection.send('Target.getBrowserContexts'); const browser = new FFBrowser(connection, browserContextIds); await connection.send('Target.enable'); return browser; } constructor(connection: FFConnection, browserContextIds: Array) { super(); this._connection = connection; this._targets = new Map(); this._defaultContext = this._createBrowserContext(null, {}); this._contexts = new Map(); for (const browserContextId of browserContextIds) this._contexts.set(browserContextId, this._createBrowserContext(browserContextId, {})); this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); this._eventListeners = [ helper.addEventListener(this._connection, 'Target.targetCreated', this._onTargetCreated.bind(this)), helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), helper.addEventListener(this._connection, 'Target.targetInfoChanged', this._onTargetInfoChanged.bind(this)), ]; } disconnect() { this._connection.dispose(); } isConnected(): boolean { return !this._connection._closed; } async newContext(options: BrowserContextOptions = {}): Promise { const {browserContextId} = await this._connection.send('Target.createBrowserContext'); // TODO: move ignoreHTTPSErrors to browser context level. if (options.ignoreHTTPSErrors) await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true }); const context = this._createBrowserContext(browserContextId, options); this._contexts.set(browserContextId, context); return context; } browserContexts(): Array { return [this._defaultContext, ...Array.from(this._contexts.values())]; } defaultContext() { return this._defaultContext; } async _waitForTarget(predicate: (target: Target) => boolean, options: { timeout?: number; } = {}): Promise { const { timeout = 30000 } = options; const existingTarget = this._allTargets().find(predicate); if (existingTarget) return existingTarget; let resolve: (t: Target) => void; const targetPromise = new Promise(x => resolve = x); this.on('targetchanged', check); try { if (!timeout) return await targetPromise; return await helper.waitWithTimeout(targetPromise, 'target', timeout); } finally { this.removeListener('targetchanged', check); } function check(target: Target) { if (predicate(target)) resolve(target); } } _allTargets() { return Array.from(this._targets.values()); } async _onTargetCreated({targetId, url, browserContextId, openerId, type}) { const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext; const target = new Target(this._connection, this, context, targetId, type, url, openerId); this._targets.set(targetId, target); if (target.opener() && target.opener()._pagePromise) { const openerPage = await target.opener()._pagePromise; if (openerPage.listenerCount(Events.Page.Popup)) { const popupPage = await target.page(); openerPage.emit(Events.Page.Popup, popupPage); } } } _onTargetDestroyed({targetId}) { const target = this._targets.get(targetId); this._targets.delete(targetId); target._didClose(); } _onTargetInfoChanged({targetId, url}) { const target = this._targets.get(targetId); target._url = url; } async close() { helper.removeEventListeners(this._eventListeners); await this._connection.send('Browser.close'); } _createBrowserContext(browserContextId: string | null, options: BrowserContextOptions): BrowserContext { const context = new BrowserContext({ pages: async (): Promise => { const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page'); const pages = await Promise.all(targets.map(target => target.page())); return pages.filter(page => !!page); }, newPage: async (): Promise => { const {targetId} = await this._connection.send('Target.newPage', { browserContextId: browserContextId || undefined }); const target = this._targets.get(targetId); const page = await target.page(); const session = (page._delegate as FFFrameManager)._session; const promises: Promise[] = []; if (options.viewport) promises.push(page._delegate.setViewport(options.viewport)); if (options.bypassCSP) promises.push(session.send('Page.setBypassCSP', { enabled: true })); if (options.javaScriptEnabled === false) promises.push(session.send('Page.setJavascriptEnabled', { enabled: false })); if (options.userAgent) promises.push(session.send('Page.setUserAgent', { userAgent: options.userAgent })); if (options.mediaType || options.colorScheme) promises.push(session.send('Page.setEmulatedMedia', { type: options.mediaType, colorScheme: options.colorScheme })); await Promise.all(promises); return 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 }); } }, options); return context; } } export class Target { _pagePromise?: Promise; private _frameManager: FFFrameManager | null = null; private _browser: FFBrowser; _context: BrowserContext; private _connection: FFConnection; private _targetId: string; private _type: 'page' | 'browser'; _url: string; private _openerId: string; constructor(connection: any, browser: FFBrowser, context: BrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) { this._browser = browser; this._context = context; this._connection = connection; this._targetId = targetId; this._type = type; this._url = url; this._openerId = openerId; } _didClose() { if (this._frameManager) this._frameManager.didClose(); } opener(): Target | null { return this._openerId ? this._browser._targets.get(this._openerId) : null; } type(): 'page' | 'browser' { return this._type; } url() { return this._url; } browserContext(): BrowserContext { return this._context; } page(): Promise { if (this._type === 'page' && !this._pagePromise) { this._pagePromise = new Promise(async f => { const session = await this._connection.createSession(this._targetId); this._frameManager = new FFFrameManager(session, this._context); const page = this._frameManager._page; session.once(FFSessionEvents.Disconnected, () => page._didDisconnect()); await this._frameManager._initialize(); f(page); }); } return this._pagePromise; } browser() { return this._browser; } }