chore(chromium): remove CRTarget, use CRPage and CRServiceWorker instead (#1436)

This commit is contained in:
Dmitry Gozman 2020-03-23 21:48:32 -07:00 committed by GitHub
parent 5a93872063
commit 7ef394b345
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 201 deletions

View File

@ -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<CRSession> | null = null;
readonly _defaultContext: CRBrowserContext;
readonly _contexts = new Map<string, CRBrowserContext>();
_targets = new Map<string, CRTarget>();
_crPages = new Map<string, CRPage>();
_backgroundPages = new Map<string, CRPage>();
_serviceWorkers = new Map<string, CRServiceWorker>();
readonly _firstPagePromise: Promise<void>;
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<Page> {
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<CRSession> {
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)!;

View File

@ -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.

View File

@ -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<number, dom.FrameExecutionContext>();
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<void>;
private _firstNonInitialNavigationCommittedCallback = () => {};
private readonly _pagePromise: Promise<Page | Error>;
_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<Page | Error> {
return this._pagePromise;
}
private async _initialize() {
let lifecycleEventsEnabled: Promise<any>;
const promises: Promise<any>[] = [
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<Page | null> {
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 {

View File

@ -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<Page | Error> | null = null;
readonly _crPage: CRPage | null = null;
_initializedPage: Page | null = null;
private readonly _workerPromise: Promise<Worker | Error> | 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<Page | Error> {
if (CRTarget.isPageType(this.type()))
return this._pagePromise!;
throw new Error('Not a page.');
}
private async _initializeServiceWorker(session: CRSession): Promise<Worker | Error> {
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<Worker | Error> {
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)!;
}
}