From b3817aab2ac98d38b98400ad433232b82941ccc1 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 9 Dec 2019 09:48:54 -0800 Subject: [PATCH] chore(webkit): remove most session usages from Page (#181) These are moved to FrameManager, so that we can reuse Page between browsers. --- src/webkit/FrameManager.ts | 68 ++++++++++++++++++++- src/webkit/Page.ts | 119 +++++++++++-------------------------- src/webkit/Target.ts | 7 ++- 3 files changed, 107 insertions(+), 87 deletions(-) diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 350e6d3dfc..bc8f00d45a 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -30,6 +30,7 @@ import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; import { Page } from './Page'; import { Protocol } from './protocol'; import { DOMWorldDelegate } from './JSHandle'; +import * as dialog from '../dialog'; export const FrameManagerEvents = { FrameNavigatedWithinDocument: Symbol('FrameNavigatedWithinDocument'), @@ -77,8 +78,17 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { this._handleFrameTree(frameTree); await Promise.all([ this._session.send('Runtime.enable'), + this._session.send('Console.enable'), + this._session.send('Dialog.enable'), + this._session.send('Page.setInterceptFileChooserDialog', { enabled: true }), this._networkManager.initialize(), ]); + if (this._page._userAgent !== null) + await this._session.send('Page.overrideUserAgent', { value: this._page._userAgent }); + if (this._page._emulatedMediaType !== undefined) + await this._session.send('Page.setEmulatedMedia', { media: this._page._emulatedMediaType || '' }); + if (!this._page._javascriptEnabled) + await this._session.send('Emulation.setJavaScriptEnabled', { enabled: this._page._javascriptEnabled }); } _addSessionListeners() { @@ -88,17 +98,22 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { helper.addEventListener(this._session, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)), helper.addEventListener(this._session, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)), helper.addEventListener(this._session, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), + helper.addEventListener(this._session, 'Page.loadEventFired', event => this._page.emit(Events.Page.Load)), + helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)), + helper.addEventListener(this._session, 'Page.domContentEventFired', event => this._page.emit(Events.Page.DOMContentLoaded)), + helper.addEventListener(this._session, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)), + helper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)) ]; } - _swapSessionOnNavigation(newSession) { + async _swapSessionOnNavigation(newSession: TargetSession) { helper.removeEventListeners(this._sessionListeners); this.disconnectFromTarget(); this._session = newSession; this._addSessionListeners(); this._networkManager.setSession(newSession); this.emit(FrameManagerEvents.TargetSwappedOnNavigation); - // this.initialize() will be called by page. + await this.initialize(); } disconnectFromTarget() { @@ -284,6 +299,55 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { }, html); await watchDog.waitForNavigation(); } + + async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) { + const { type, level, text, parameters, url, line: lineNumber, column: columnNumber } = event.message; + let derivedType: string = type; + if (type === 'log') + derivedType = level; + else if (type === 'timing') + derivedType = 'timeEnd'; + const mainFrameContext = await this.mainFrame().executionContext(); + const handles = (parameters || []).map(p => { + let context: js.ExecutionContext | null = null; + if (p.objectId) { + const objectId = JSON.parse(p.objectId); + context = this._contextIdToContext.get(objectId.injectedScriptId); + } else { + context = mainFrameContext; + } + return context._createHandle(p); + }); + this._page._addConsoleMessage(derivedType, handles, { url, lineNumber, columnNumber }, handles.length ? undefined : text); + } + + _onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) { + this._page.emit(Events.Page.Dialog, new dialog.Dialog( + event.type as dialog.DialogType, + event.message, + async (accept: boolean, promptText?: string) => { + await this._session.send('Dialog.handleJavaScriptDialog', { accept, promptText }); + }, + event.defaultPrompt)); + } + + async _onFileChooserOpened(event: {frameId: Protocol.Network.FrameId, element: Protocol.Runtime.RemoteObject}) { + const context = await this.frame(event.frameId)._utilityContext(); + const handle = context._createHandle(event.element).asElement()!; + this._page._onFileChooserOpened(handle); + } + + async setUserAgent(userAgent: string) { + await this._session.send('Page.overrideUserAgent', { value: userAgent }); + } + + async setEmulatedMedia(type?: string | null) { + await this._session.send('Page.setEmulatedMedia', { media: type || '' }); + } + + async setJavaScriptEnabled(enabled: boolean) { + await this._session.send('Emulation.setJavaScriptEnabled', { enabled }); + } } /** diff --git a/src/webkit/Page.ts b/src/webkit/Page.ts index 662f487451..54cd468ff9 100644 --- a/src/webkit/Page.ts +++ b/src/webkit/Page.ts @@ -17,10 +17,9 @@ import { EventEmitter } from 'events'; import * as console from '../console'; -import * as dialog from '../dialog'; import * as dom from '../dom'; import * as frames from '../frames'; -import { assert, helper, RegisteredListener } from '../helper'; +import { assert, helper } from '../helper'; import * as input from '../input'; import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions } from '../input'; import * as js from '../javascript'; @@ -29,7 +28,7 @@ import { Screenshotter } from '../screenshotter'; import { TimeoutSettings } from '../TimeoutSettings'; import * as types from '../types'; import { Browser, BrowserContext } from './Browser'; -import { TargetSession, TargetSessionEvents } from './Connection'; +import { TargetSession } from './Connection'; import { Events } from './events'; import { FrameManager, FrameManagerEvents } from './FrameManager'; import { RawKeyboardImpl, RawMouseImpl } from './Input'; @@ -41,6 +40,9 @@ export class Page extends EventEmitter { private _closed = false; private _closedCallback: () => void; private _closedPromise: Promise; + private _disconnected = false; + private _disconnectedCallback: (e: Error) => void; + private _disconnectedPromise: Promise; _session: TargetSession; private _browserContext: BrowserContext; private _keyboard: input.Keyboard; @@ -49,18 +51,16 @@ export class Page extends EventEmitter { private _frameManager: FrameManager; private _bootstrapScripts: string[] = []; _javascriptEnabled = true; - private _userAgent: string | null = null; + _userAgent: string | null = null; + _emulatedMediaType: string | undefined; private _viewport: types.Viewport | null = null; _screenshotter: Screenshotter; - private _workers = new Map(); - private _disconnectPromise: Promise | undefined; - private _sessionListeners: RegisteredListener[] = []; - private _emulatedMediaType: string | undefined; private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); constructor(session: TargetSession, browserContext: BrowserContext) { super(); this._closedPromise = new Promise(f => this._closedCallback = f); + this._disconnectedPromise = new Promise(f => this._disconnectedCallback = f); this._keyboard = new input.Keyboard(new RawKeyboardImpl(session)); this._mouse = new input.Mouse(new RawMouseImpl(session), this._keyboard); this._timeoutSettings = new TimeoutSettings(); @@ -68,7 +68,7 @@ export class Page extends EventEmitter { this._screenshotter = new Screenshotter(this, new WKScreenshotDelegate(session), browserContext.browser()); - this._setSession(session); + this._session = session; this._browserContext = browserContext; this._frameManager.on(FrameManagerEvents.FrameAttached, event => this.emit(Events.Page.FrameAttached, event)); @@ -89,46 +89,20 @@ export class Page extends EventEmitter { this._closedCallback(); } - async _initialize() { - await Promise.all([ - this._frameManager.initialize(), - this._session.send('Console.enable'), - this._session.send('Dialog.enable'), - this._session.send('Page.setInterceptFileChooserDialog', { enabled: true }), - ]); - if (this._userAgent !== null) - await this._session.send('Page.overrideUserAgent', { value: this._userAgent }); - if (this._emulatedMediaType !== undefined) - await this._session.send('Page.setEmulatedMedia', { media: this._emulatedMediaType || '' }); + _didDisconnect() { + assert(!this._disconnected, 'Page disconnected twice'); + this._disconnected = true; + this._frameManager.disconnectFromTarget(); + this._disconnectedCallback(new Error('Target closed')); } - _setSession(newSession: TargetSession) { - helper.removeEventListeners(this._sessionListeners); - this._session = newSession; - this._sessionListeners = [ - helper.addEventListener(this._session, TargetSessionEvents.Disconnected, () => this._frameManager.disconnectFromTarget()), - helper.addEventListener(this._session, 'Page.loadEventFired', event => this.emit(Events.Page.Load)), - helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)), - helper.addEventListener(this._session, 'Page.domContentEventFired', event => this.emit(Events.Page.DOMContentLoaded)), - helper.addEventListener(this._session, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)), - helper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)) - ]; - } - - _onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) { - this.emit(Events.Page.Dialog, new dialog.Dialog( - event.type as dialog.DialogType, - event.message, - async (accept: boolean, promptText?: string) => { - await this._session.send('Dialog.handleJavaScriptDialog', { accept, promptText }); - }, - event.defaultPrompt)); + _initialize() { + return this._frameManager.initialize(); } async _swapSessionOnNavigation(newSession: TargetSession) { - this._setSession(newSession); - this._frameManager._swapSessionOnNavigation(newSession); - await this._initialize(); + this._session = newSession; + await this._frameManager._swapSessionOnNavigation(newSession); } browser(): Browser { @@ -143,25 +117,12 @@ export class Page extends EventEmitter { this.emit('error', new Error('Page crashed!')); } - async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) { - const { type, level, text, parameters, url, line: lineNumber, column: columnNumber } = event.message; - let derivedType: string = type; - if (type === 'log') - derivedType = level; - else if (type === 'timing') - derivedType = 'timeEnd'; - const mainFrameContext = await this.mainFrame().executionContext(); - const handles = (parameters || []).map(p => { - let context: js.ExecutionContext | null = null; - if (p.objectId) { - const objectId = JSON.parse(p.objectId); - context = this._frameManager._contextIdToContext.get(objectId.injectedScriptId); - } else { - context = mainFrameContext; - } - return context._createHandle(p); - }); - this.emit(Events.Page.Console, new console.ConsoleMessage(derivedType, handles.length ? undefined : text, handles, { url, lineNumber, columnNumber })); + _addConsoleMessage(type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation, text?: string) { + if (!this.listenerCount(Events.Page.Console)) { + args.forEach(arg => arg.dispose()); + return; + } + this.emit(Events.Page.Console, new console.ConsoleMessage(type, text, args, location)); } mainFrame(): frames.Frame { @@ -176,11 +137,6 @@ export class Page extends EventEmitter { return this._frameManager.frames(); } - workers(): Worker[] { - return Array.from(this._workers.values()); - } - - setDefaultNavigationTimeout(timeout: number) { this._timeoutSettings.setDefaultNavigationTimeout(timeout); } @@ -228,7 +184,7 @@ export class Page extends EventEmitter { async setUserAgent(userAgent: string) { this._userAgent = userAgent; - await this._session.send('Page.overrideUserAgent', { value: userAgent }); + this._frameManager.setUserAgent(userAgent); } url(): string { @@ -279,12 +235,6 @@ export class Page extends EventEmitter { return await this._frameManager.mainFrame().waitForNavigation(); } - _sessionClosePromise() { - if (!this._disconnectPromise) - this._disconnectPromise = new Promise(fulfill => this._session.once(TargetSessionEvents.Disconnected, () => fulfill(new Error('Target closed')))); - return this._disconnectPromise; - } - async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise { const { timeout = this._timeoutSettings.timeout(), @@ -295,7 +245,7 @@ export class Page extends EventEmitter { if (typeof urlOrPredicate === 'function') return !!(urlOrPredicate(request)); return false; - }, timeout, this._sessionClosePromise()); + }, timeout, this._disconnectedPromise); } async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise { @@ -308,7 +258,7 @@ export class Page extends EventEmitter { if (typeof urlOrPredicate === 'function') return !!(urlOrPredicate(response)); return false; - }, timeout, this._sessionClosePromise()); + }, timeout, this._disconnectedPromise); } async emulate(options: { viewport: types.Viewport; userAgent: string; }) { @@ -325,7 +275,7 @@ export class Page extends EventEmitter { assert(!options.colorScheme || mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); assert(!options.colorScheme, 'Media feature emulation is not supported'); this._emulatedMediaType = typeof options.type === 'undefined' ? this._emulatedMediaType : options.type; - await this._session.send('Page.setEmulatedMedia', { media: this._emulatedMediaType || '' }); + this._frameManager.setEmulatedMedia(this._emulatedMediaType); } async setViewport(viewport: types.Viewport) { @@ -351,11 +301,11 @@ export class Page extends EventEmitter { await this._session.send('Page.setBootstrapScript', { source }); } - async setJavaScriptEnabled(enabled: boolean) { + setJavaScriptEnabled(enabled: boolean) { if (this._javascriptEnabled === enabled) return; this._javascriptEnabled = enabled; - await this._session.send('Emulation.setJavaScriptEnabled', { enabled }); + return this._frameManager.setJavaScriptEnabled(enabled); } async setCacheEnabled(enabled: boolean = true) { @@ -371,6 +321,7 @@ export class Page extends EventEmitter { } async close() { + assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.'); this.browser()._closePage(this); await this._closedPromise; } @@ -392,11 +343,11 @@ export class Page extends EventEmitter { }); } - async _onFileChooserOpened(event: {frameId: Protocol.Network.FrameId, element: Protocol.Runtime.RemoteObject}) { - if (!this._fileChooserInterceptors.size) + async _onFileChooserOpened(handle: dom.ElementHandle) { + if (!this._fileChooserInterceptors.size) { + await handle.dispose(); return; - const context = await this._frameManager.frame(event.frameId)._utilityContext(); - const handle = context._createHandle(event.element).asElement()!; + } const interceptors = Array.from(this._fileChooserInterceptors); this._fileChooserInterceptors.clear(); const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple); diff --git a/src/webkit/Target.ts b/src/webkit/Target.ts index ddfcbb41b9..4a1764f0b9 100644 --- a/src/webkit/Target.ts +++ b/src/webkit/Target.ts @@ -19,7 +19,7 @@ import { RegisteredListener } from '../helper'; import { Browser, BrowserContext } from './Browser'; import { Page } from './Page'; import { Protocol } from './protocol'; -import { isSwappedOutError, TargetSession } from './Connection'; +import { isSwappedOutError, TargetSession, TargetSessionEvents } from './Connection'; const targetSymbol = Symbol('target'); @@ -74,6 +74,11 @@ export class Target { if (this.browser()._defaultViewport) await page.setViewport(this.browser()._defaultViewport); (page as any)[targetSymbol] = this; + session.once(TargetSessionEvents.Disconnected, () => { + // Check that this target has not been swapped out. + if ((page as any)[targetSymbol] === this) + page._didDisconnect(); + }); f(page); }); }