From 7ef394b345c8a58abdb1017b2540b01853163f04 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 23 Mar 2020 21:48:32 -0700 Subject: [PATCH] chore(chromium): remove CRTarget, use CRPage and CRServiceWorker instead (#1436) --- src/chromium/crBrowser.ts | 159 +++++++++++++++++++------------ src/chromium/crNetworkManager.ts | 4 +- src/chromium/crPage.ts | 38 ++++---- src/chromium/crTarget.ts | 121 ----------------------- 4 files changed, 121 insertions(+), 201 deletions(-) delete mode 100644 src/chromium/crTarget.ts diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 02fcbc45e1..dca2340f3e 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -27,9 +27,9 @@ import * as types from '../types'; import { ConnectionEvents, CRConnection, CRSession } from './crConnection'; import { CRPage } from './crPage'; import { readProtocolStream } from './crProtocolHelper'; -import { CRTarget } from './crTarget'; import { Events } from './events'; import { Protocol } from './protocol'; +import { CRExecutionContext } from './crExecutionContext'; export class CRBrowser extends platform.EventEmitter implements Browser { readonly _connection: CRConnection; @@ -37,7 +37,9 @@ export class CRBrowser extends platform.EventEmitter implements Browser { private _clientRootSessionPromise: Promise | null = null; readonly _defaultContext: CRBrowserContext; readonly _contexts = new Map(); - _targets = new Map(); + _crPages = new Map(); + _backgroundPages = new Map(); + _serviceWorkers = new Map(); readonly _firstPagePromise: Promise; private _firstPageCallback = () => {}; @@ -59,7 +61,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser { // First page and background pages in the persistent context are created automatically // and may be initialized before we enable auto-attach. function attachToExistingPage({targetInfo}: Protocol.Target.targetCreatedPayload) { - if (!CRTarget.isPageType(targetInfo.type)) + if (targetInfo.type !== 'page' && targetInfo.type !== 'background_page') return; existingPageAttachPromises.push(session.send('Target.attachToTarget', {targetId: targetInfo.targetId, flatten: true})); } @@ -106,65 +108,79 @@ export class CRBrowser extends platform.EventEmitter implements Browser { _onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) { const session = this._connection.session(sessionId)!; - if (!CRTarget.isPageType(targetInfo.type) && targetInfo.type !== 'service_worker') { - assert(targetInfo.type === 'browser' || targetInfo.type === 'other'); - if (waitingForDebugger) { - // Ideally, detaching should resume any target, but there is a bug in the backend. - session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => { - this._session.send('Target.detachFromTarget', { sessionId }).catch(debugError); - }); - } + const context = (targetInfo.browserContextId && this._contexts.has(targetInfo.browserContextId)) ? + this._contexts.get(targetInfo.browserContextId)! : this._defaultContext; + + assert(!this._crPages.has(targetInfo.targetId), 'Duplicate target ' + targetInfo.targetId); + assert(!this._backgroundPages.has(targetInfo.targetId), 'Duplicate target ' + targetInfo.targetId); + assert(!this._serviceWorkers.has(targetInfo.targetId), 'Duplicate target ' + targetInfo.targetId); + + if (targetInfo.type === 'background_page') { + const backgroundPage = new CRPage(session, targetInfo.targetId, context, null); + this._backgroundPages.set(targetInfo.targetId, backgroundPage); + backgroundPage.pageOrError().then(() => { + context.emit(Events.CRBrowserContext.BackgroundPage, backgroundPage._page); + }); return; } - const { context, target } = this._createTarget(targetInfo, session); - if (CRTarget.isPageType(targetInfo.type)) { - target.pageOrError().then(async () => { - const page = target._crPage!.page(); - if (targetInfo.type === 'page') { - this._firstPageCallback(); - context.emit(CommonEvents.BrowserContext.Page, page); - const opener = target.opener(); - if (!opener) - return; - // Opener page must have been initialized already and resumed in order to - // create this popup but there is a chance that not all responses have been - // received yet so we cannot use opener._crPage?.page() - const openerPage = await opener.pageOrError(); - if (openerPage instanceof Page && !openerPage.isClosed()) - openerPage.emit(CommonEvents.Page.Popup, page); - } else if (targetInfo.type === 'background_page') { - context.emit(Events.CRBrowserContext.BackgroundPage, page); + if (targetInfo.type === 'page') { + const opener = targetInfo.openerId ? this._crPages.get(targetInfo.openerId) || null : null; + const crPage = new CRPage(session, targetInfo.targetId, context, opener); + this._crPages.set(targetInfo.targetId, crPage); + crPage.pageOrError().then(() => { + this._firstPageCallback(); + context.emit(CommonEvents.BrowserContext.Page, crPage._page); + if (opener) { + opener.pageOrError().then(openerPage => { + if (openerPage instanceof Page && !openerPage.isClosed()) + openerPage.emit(CommonEvents.Page.Popup, crPage._page); + }); } }); return; } - assert(targetInfo.type === 'service_worker'); - target.serviceWorkerOrError().then(workerOrError => { - if (workerOrError instanceof Worker) - context.emit(Events.CRBrowserContext.ServiceWorker, workerOrError); - }); - } - private _createTarget(targetInfo: Protocol.Target.TargetInfo, session: CRSession) { - const {browserContextId} = targetInfo; - const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId)! : this._defaultContext; - const target = new CRTarget(this, targetInfo, context, session); - assert(!this._targets.has(targetInfo.targetId), 'Target should not exist before targetCreated'); - this._targets.set(targetInfo.targetId, target); - return { context, target }; - } - - _onDetachedFromTarget({targetId}: Protocol.Target.detachFromTargetParameters) { - const target = this._targets.get(targetId!)!; - if (!target) + if (targetInfo.type === 'service_worker') { + const serviceWorker = new CRServiceWorker(context, session, targetInfo.url); + this._serviceWorkers.set(targetInfo.targetId, serviceWorker); + context.emit(Events.CRBrowserContext.ServiceWorker, serviceWorker); return; - this._targets.delete(targetId!); - target._didClose(); + } + + assert(targetInfo.type === 'browser' || targetInfo.type === 'other'); + if (waitingForDebugger) { + // Ideally, detaching should resume any target, but there is a bug in the backend. + session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => { + this._session.send('Target.detachFromTarget', { sessionId }).catch(debugError); + }); + } } - async _closePage(page: Page) { - await this._session.send('Target.closeTarget', { targetId: CRTarget.fromPage(page)._targetId }); + _onDetachedFromTarget(payload: Protocol.Target.detachFromTargetParameters) { + const targetId = payload.targetId!; + const crPage = this._crPages.get(targetId); + if (crPage) { + this._crPages.delete(targetId); + crPage.didClose(); + return; + } + const backgroundPage = this._backgroundPages.get(targetId); + if (backgroundPage) { + this._backgroundPages.delete(targetId); + backgroundPage.didClose(); + return; + } + const serviceWorker = this._serviceWorkers.get(targetId); + if (serviceWorker) { + this._serviceWorkers.delete(targetId); + serviceWorker.emit(CommonEvents.Worker.Close); + return; + } + } + + async _closePage(crPage: CRPage) { + await this._session.send('Target.closeTarget', { targetId: crPage._targetId }); } async close() { @@ -232,6 +248,21 @@ export class CRBrowser extends platform.EventEmitter implements Browser { } } +class CRServiceWorker extends Worker { + readonly _browserContext: CRBrowserContext; + + constructor(browserContext: CRBrowserContext, session: CRSession, url: string) { + super(url); + this._browserContext = browserContext; + session.once('Runtime.executionContextCreated', event => { + this._createExecutionContext(new CRExecutionContext(session, event.context)); + }); + // This might fail if the target is closed before we receive all execution contexts. + session.send('Runtime.enable', {}).catch(e => {}); + session.send('Runtime.runIfWaitingForDebugger').catch(e => {}); + } +} + export class CRBrowserContext extends BrowserContextBase { readonly _browser: CRBrowser; readonly _browserContextId: string | null; @@ -253,19 +284,20 @@ export class CRBrowserContext extends BrowserContextBase { await this.setHTTPCredentials(this._options.httpCredentials); } - _targets(): CRTarget[] { - return Array.from(this._browser._targets.values()).filter(target => target.context() === this); - } - pages(): Page[] { - return this._targets().filter(target => target.type() === 'page').map(target => target._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[]; + const result: Page[] = []; + for (const crPage of this._browser._crPages.values()) { + if (crPage._browserContext === this && crPage._initializedPage) + result.push(crPage._initializedPage); + } + return result; } async newPage(): Promise { assertBrowserContextIsNotOwned(this); const { targetId } = await this._browser._session.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined }); - const target = this._browser._targets.get(targetId)!; - const result = await target.pageOrError(); + const crPage = this._browser._crPages.get(targetId)!; + const result = await crPage.pageOrError(); if (result instanceof Page) { if (result.isClosed()) throw new Error('Page has been closed.'); @@ -392,15 +424,20 @@ export class CRBrowserContext extends BrowserContextBase { } backgroundPages(): Page[] { - return this._targets().filter(target => target.type() === 'background_page').map(target => target._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[]; + const result: Page[] = []; + for (const backgroundPage of this._browser._backgroundPages.values()) { + if (backgroundPage._browserContext === this && backgroundPage._initializedPage) + result.push(backgroundPage._initializedPage); + } + return result; } serviceWorkers(): Worker[] { - return this._targets().filter(target => target.type() === 'service_worker').map(target => target._initializedWorker).filter(workerOrNull => !!workerOrNull) as any as Worker[]; + return Array.from(this._browser._serviceWorkers.values()).filter(serviceWorker => serviceWorker._browserContext === this); } async newCDPSession(page: Page): Promise { - const targetId = CRTarget.fromPage(page)._targetId; + const targetId = (page._delegate as CRPage)._targetId; const rootSession = await this._browser._clientRootSession(); const { sessionId } = await rootSession.send('Target.attachToTarget', { targetId, flatten: true }); return this._browser._connection.session(sessionId)!; diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index fa2743f707..5032f55d1f 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -23,7 +23,7 @@ import * as network from '../network'; import * as frames from '../frames'; import * as platform from '../platform'; import { Credentials } from '../types'; -import { CRTarget } from './crTarget'; +import { CRPage } from './crPage'; export class CRNetworkManager { private _client: CRSession; @@ -169,7 +169,7 @@ export class CRNetworkManager { let frame = event.frameId ? this._page._frameManager.frame(event.frameId) : workerFrame; // Check if it's main resource request interception (targetId === main frame id). - if (!frame && interceptionId && event.frameId === CRTarget.fromPage(this._page)._targetId) { + if (!frame && interceptionId && event.frameId === (this._page._delegate as CRPage)._targetId) { // Main resource request for the page is being intercepted so the Frame is not created // yet. Precreate it here for the purposes of request interception. It will be updated // later as soon as the request contnues and we receive frame tree from the page. diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 18857f5472..17294221f7 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -20,7 +20,7 @@ import * as js from '../javascript'; import * as frames from '../frames'; import { debugError, helper, RegisteredListener, assert } from '../helper'; import * as network from '../network'; -import { CRSession, CRConnection } from './crConnection'; +import { CRSession, CRConnection, CRSessionEvents } from './crConnection'; import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext'; import { CRNetworkManager } from './crNetworkManager'; import { Page, Worker, PageBinding } from '../page'; @@ -33,32 +33,35 @@ import { RawMouseImpl, RawKeyboardImpl } from './crInput'; import { getAccessibilityTree } from './crAccessibility'; import { CRCoverage } from './crCoverage'; import { CRPDF } from './crPdf'; -import { CRBrowser, CRBrowserContext } from './crBrowser'; +import { CRBrowserContext } from './crBrowser'; import * as types from '../types'; import { ConsoleMessage } from '../console'; import * as platform from '../platform'; -import { CRTarget } from './crTarget'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; export class CRPage implements PageDelegate { readonly _client: CRSession; - private readonly _page: Page; + readonly _page: Page; readonly _networkManager: CRNetworkManager; private readonly _contextIdToContext = new Map(); private _eventListeners: RegisteredListener[] = []; readonly rawMouse: RawMouseImpl; readonly rawKeyboard: RawKeyboardImpl; - private readonly _browser: CRBrowser; + readonly _targetId: string; + private readonly _opener: CRPage | null; private readonly _pdf: CRPDF; private readonly _coverage: CRCoverage; - private readonly _browserContext: CRBrowserContext; + readonly _browserContext: CRBrowserContext; private _firstNonInitialNavigationCommittedPromise: Promise; private _firstNonInitialNavigationCommittedCallback = () => {}; + private readonly _pagePromise: Promise; + _initializedPage: Page | null = null; - constructor(client: CRSession, browser: CRBrowser, browserContext: CRBrowserContext) { + constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null) { this._client = client; - this._browser = browser; + this._targetId = targetId; + this._opener = opener; this.rawKeyboard = new RawKeyboardImpl(client); this.rawMouse = new RawMouseImpl(client); this._pdf = new CRPDF(client); @@ -67,9 +70,15 @@ export class CRPage implements PageDelegate { this._page = new Page(this, browserContext); this._networkManager = new CRNetworkManager(client, this._page); this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f); + client.once(CRSessionEvents.Disconnected, () => this._page._didDisconnect()); + this._pagePromise = this._initialize().then(() => this._initializedPage = this._page).catch(e => e); } - async initialize() { + async pageOrError(): Promise { + return this._pagePromise; + } + + private async _initialize() { let lifecycleEventsEnabled: Promise; const promises: Promise[] = [ this._client.send('Page.enable'), @@ -195,10 +204,6 @@ export class CRPage implements PageDelegate { this._handleFrameTree(child); } - page(): Page { - return this._page; - } - _onFrameAttached(frameId: string, parentFrameId: string | null) { this._page._frameManager.frameAttached(frameId, parentFrameId); } @@ -402,10 +407,9 @@ export class CRPage implements PageDelegate { } async opener(): Promise { - const openerTarget = CRTarget.fromPage(this._page).opener(); - if (!openerTarget) + if (!this._opener) return null; - const openerPage = await openerTarget.pageOrError(); + const openerPage = await this._opener.pageOrError(); if (openerPage instanceof Page && !openerPage.isClosed()) return openerPage; return null; @@ -440,7 +444,7 @@ export class CRPage implements PageDelegate { if (runBeforeUnload) await this._client.send('Page.close'); else - await this._browser._closePage(this._page); + await this._browserContext._browser._closePage(this); } canScreenshotOutsideViewport(): boolean { diff --git a/src/chromium/crTarget.ts b/src/chromium/crTarget.ts deleted file mode 100644 index 4075902b05..0000000000 --- a/src/chromium/crTarget.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright 2019 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 { assert } from '../helper'; -import { Page, Worker } from '../page'; -import { CRBrowser, CRBrowserContext } from './crBrowser'; -import { CRSession, CRSessionEvents } from './crConnection'; -import { CRExecutionContext } from './crExecutionContext'; -import { CRPage } from './crPage'; -import { Protocol } from './protocol'; - -const targetSymbol = Symbol('target'); - -export class CRTarget { - private readonly _targetInfo: Protocol.Target.TargetInfo; - private readonly _browser: CRBrowser; - private readonly _browserContext: CRBrowserContext; - readonly _targetId: string; - private readonly _pagePromise: Promise | null = null; - readonly _crPage: CRPage | null = null; - _initializedPage: Page | null = null; - private readonly _workerPromise: Promise | null = null; - _initializedWorker: Worker | null = null; - - static fromPage(page: Page): CRTarget { - return (page as any)[targetSymbol]; - } - - static isPageType(type: string): boolean { - return type === 'page' || type === 'background_page'; - } - - constructor( - browser: CRBrowser, - targetInfo: Protocol.Target.TargetInfo, - browserContext: CRBrowserContext, - session: CRSession) { - this._targetInfo = targetInfo; - this._browser = browser; - this._browserContext = browserContext; - this._targetId = targetInfo.targetId; - if (CRTarget.isPageType(targetInfo.type)) { - this._crPage = new CRPage(session, this._browser, this._browserContext); - const page = this._crPage.page(); - (page as any)[targetSymbol] = this; - session.once(CRSessionEvents.Disconnected, () => page._didDisconnect()); - this._pagePromise = this._crPage.initialize().then(() => this._initializedPage = page).catch(e => e); - } else if (targetInfo.type === 'service_worker') { - this._workerPromise = this._initializeServiceWorker(session); - } else { - assert(false, 'Unsupported target type: ' + targetInfo.type); - } - } - - _didClose() { - if (this._crPage) - this._crPage.didClose(); - } - - async pageOrError(): Promise { - if (CRTarget.isPageType(this.type())) - return this._pagePromise!; - throw new Error('Not a page.'); - } - - private async _initializeServiceWorker(session: CRSession): Promise { - const worker = new Worker(this._targetInfo.url); - session.once('Runtime.executionContextCreated', event => { - worker._createExecutionContext(new CRExecutionContext(session, event.context)); - }); - try { - // This might fail if the target is closed before we receive all execution contexts. - await Promise.all([ - session.send('Runtime.enable', {}), - session.send('Runtime.runIfWaitingForDebugger'), - ]); - this._initializedWorker = worker; - return worker; - } catch (error) { - return error; - } - } - - serviceWorkerOrError(): Promise { - if (this.type() === 'service_worker') - return this._workerPromise!; - throw new Error('Not a service worker.'); - } - - type(): 'page' | 'background_page' | 'service_worker' | 'shared_worker' | 'other' | 'browser' { - const type = this._targetInfo.type; - if (type === 'page' || type === 'background_page' || type === 'service_worker' || type === 'shared_worker' || type === 'browser') - return type; - return 'other'; - } - - context(): CRBrowserContext { - return this._browserContext; - } - - opener(): CRTarget | null { - const { openerId } = this._targetInfo; - if (!openerId) - return null; - return this._browser._targets.get(openerId)!; - } -}