From 4b7a01745621ce55d0b54d6156a1550dbeb4fa22 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 16 Dec 2019 15:56:11 -0800 Subject: [PATCH] chore: introduce FrameManager to be reused between browsers (#261) --- package.json | 2 +- src/chromium/FrameManager.ts | 100 +++---------------- src/chromium/NetworkManager.ts | 10 +- src/firefox/FrameManager.ts | 92 +++++------------- src/firefox/NetworkManager.ts | 10 +- src/frames.ts | 172 +++++++++++++++++++++++++-------- src/network.ts | 18 ++-- src/page.ts | 9 +- src/webkit/FrameManager.ts | 98 +++---------------- src/webkit/NetworkManager.ts | 10 +- 10 files changed, 214 insertions(+), 307 deletions(-) diff --git a/package.json b/package.json index ed39676486..3955582b83 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "main": "index.js", "playwright": { "chromium_revision": "724623", - "firefox_revision": "1005", + "firefox_revision": "1007", "webkit_revision": "1038" }, "scripts": { diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index f2009056de..b2f60c117f 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -49,10 +49,8 @@ export class FrameManager extends EventEmitter implements PageDelegate { _client: CDPSession; private _page: Page; private _networkManager: NetworkManager; - private _frames = new Map(); private _contextIdToContext = new Map(); private _isolatedWorlds = new Set(); - private _mainFrame: frames.Frame; rawMouse: RawMouseImpl; rawKeyboard: RawKeyboardImpl; @@ -61,8 +59,8 @@ export class FrameManager extends EventEmitter implements PageDelegate { this._client = client; this.rawKeyboard = new RawKeyboardImpl(client); this.rawMouse = new RawMouseImpl(client); - this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); this._page = new Page(this, browserContext); + this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this._page); (this._page as any).accessibility = new Accessibility(client); (this._page as any).coverage = new Coverage(client); (this._page as any).pdf = new PDF(client); @@ -77,7 +75,6 @@ export class FrameManager extends EventEmitter implements PageDelegate { this._client.on('Inspector.targetCrashed', event => this._onTargetCrashed()); this._client.on('Log.entryAdded', event => this._onLogEntryAdded(event)); - this._client.on('Page.domContentEventFired', event => this._page.emit(Events.Page.DOMContentLoaded)); this._client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event)); this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)); this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId)); @@ -85,7 +82,6 @@ export class FrameManager extends EventEmitter implements PageDelegate { this._client.on('Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)); this._client.on('Page.javascriptDialogOpening', event => this._onDialog(event)); this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event)); - this._client.on('Page.loadEventFired', event => this._page.emit(Events.Page.Load)); this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)); this._client.on('Runtime.bindingCalled', event => this._onBindingCalled(event)); this._client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); @@ -196,28 +192,20 @@ export class FrameManager extends EventEmitter implements PageDelegate { } _onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) { - const frame = this._frames.get(event.frameId); - if (!frame) - return; if (event.name === 'init') - frame._firedLifecycleEvents.clear(); + this._page._frameManager.frameLifecycleEvent(event.frameId, 'clear'); else if (event.name === 'load') - frame._lifecycleEvent('load'); + this._page._frameManager.frameLifecycleEvent(event.frameId, 'load'); else if (event.name === 'DOMContentLoaded') - frame._lifecycleEvent('domcontentloaded'); + this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded'); } _onFrameStoppedLoading(frameId: string) { - const frame = this._frames.get(frameId); - if (!frame) - return; - frame._lifecycleEvent('domcontentloaded'); - frame._lifecycleEvent('load'); + this._page._frameManager.frameStoppedLoading(frameId); } _handleFrameTree(frameTree: Protocol.Page.FrameTree) { - if (frameTree.frame.parentId) - this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId); + this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId); this._onFrameNavigated(frameTree.frame, true); if (!frameTree.childFrames) return; @@ -230,56 +218,12 @@ export class FrameManager extends EventEmitter implements PageDelegate { return this._page; } - mainFrame(): frames.Frame { - return this._mainFrame; - } - - frames(): frames.Frame[] { - return Array.from(this._frames.values()); - } - - frame(frameId: string): frames.Frame | null { - return this._frames.get(frameId) || null; - } - _onFrameAttached(frameId: string, parentFrameId: string | null) { - if (this._frames.has(frameId)) - return; - assert(parentFrameId); - const parentFrame = this._frames.get(parentFrameId); - const frame = new frames.Frame(this._page, frameId, parentFrame); - this._frames.set(frameId, frame); - this._page.emit(Events.Page.FrameAttached, frame); + this._page._frameManager.frameAttached(frameId, parentFrameId); } _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) { - const isMainFrame = !framePayload.parentId; - let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id); - assert(isMainFrame || frame, 'We either navigate top level or have old version of the navigated frame'); - - // Detach all child frames first. - if (frame) { - for (const child of frame.childFrames()) - this._removeFramesRecursively(child); - } - - // Update or create main frame. - if (isMainFrame) { - if (frame) { - // Update frame id to retain frame identity on cross-process navigation. - this._frames.delete(frame._id); - frame._id = framePayload.id; - } else { - // Initial main frame navigation. - frame = new frames.Frame(this._page, framePayload.id, null); - } - this._frames.set(framePayload.id, frame); - this._mainFrame = frame; - } - - frame._onCommittedNewDocumentNavigation(framePayload.url, framePayload.name, framePayload.loaderId, initial); - - this._page.emit(Events.Page.FrameNavigated, frame); + this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial); } async _ensureIsolatedWorld(name: string) { @@ -290,7 +234,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`, worldName: name, }); - await Promise.all(this.frames().map(frame => this._client.send('Page.createIsolatedWorld', { + await Promise.all(this._page.frames().map(frame => this._client.send('Page.createIsolatedWorld', { frameId: frame._id, grantUniveralAccess: true, worldName: name, @@ -298,22 +242,16 @@ export class FrameManager extends EventEmitter implements PageDelegate { } _onFrameNavigatedWithinDocument(frameId: string, url: string) { - const frame = this._frames.get(frameId); - if (!frame) - return; - frame._onCommittedSameDocumentNavigation(url); - this._page.emit(Events.Page.FrameNavigated, frame); + this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url); } _onFrameDetached(frameId: string) { - const frame = this._frames.get(frameId); - if (frame) - this._removeFramesRecursively(frame); + this._page._frameManager.frameDetached(frameId); } _onExecutionContextCreated(contextPayload: Protocol.Runtime.ExecutionContextDescription) { const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null; - const frame = this._frames.get(frameId) || null; + const frame = this._page._frameManager.frame(frameId); if (contextPayload.auxData && contextPayload.auxData.type === 'isolated') this._isolatedWorlds.add(contextPayload.name); const delegate = new ExecutionContextDelegate(this._client, contextPayload); @@ -349,14 +287,6 @@ export class FrameManager extends EventEmitter implements PageDelegate { return context; } - _removeFramesRecursively(frame: frames.Frame) { - for (const child of frame.childFrames()) - this._removeFramesRecursively(child); - frame._onDetached(); - this._frames.delete(frame._id); - this._page.emit(Events.Page.FrameDetached, frame); - } - async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) { if (event.executionContextId === 0) { // DevTools protocol stores the last 1000 console messages. These @@ -382,7 +312,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { async exposeBinding(name: string, bindingFunction: string) { await this._client.send('Runtime.addBinding', {name: name}); await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction}); - await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); + await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); } _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { @@ -417,7 +347,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { } async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) { - const frame = this.frame(event.frameId); + const frame = this._page._frameManager.frame(event.frameId); const utilityContext = await frame._utilityContext(); const handle = await this.adoptBackendNodeId(event.backendNodeId, utilityContext); this._page._onFileChooserOpened(handle); @@ -539,7 +469,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { }); if (typeof nodeInfo.node.frameId !== 'string') return null; - return this.frame(nodeInfo.node.frameId); + return this._page._frameManager.frame(nodeInfo.node.frameId); } isElementHandle(remoteObject: any): boolean { diff --git a/src/chromium/NetworkManager.ts b/src/chromium/NetworkManager.ts index 798a6b27d6..159d745538 100644 --- a/src/chromium/NetworkManager.ts +++ b/src/chromium/NetworkManager.ts @@ -17,7 +17,7 @@ import { EventEmitter } from 'events'; import { CDPSession } from './Connection'; -import { FrameManager } from './FrameManager'; +import { Page } from '../page'; import { assert, debugError, helper } from '../helper'; import { Protocol } from './protocol'; import * as network from '../network'; @@ -33,7 +33,7 @@ export const NetworkManagerEvents = { export class NetworkManager extends EventEmitter { private _client: CDPSession; private _ignoreHTTPSErrors: boolean; - private _frameManager: FrameManager; + private _page: Page; private _requestIdToRequest = new Map(); private _requestIdToRequestWillBeSentEvent = new Map(); private _extraHTTPHeaders: network.Headers = {}; @@ -45,11 +45,11 @@ export class NetworkManager extends EventEmitter { private _userCacheDisabled = false; private _requestIdToInterceptionId = new Map(); - constructor(client: CDPSession, ignoreHTTPSErrors: boolean, frameManager: FrameManager) { + constructor(client: CDPSession, ignoreHTTPSErrors: boolean, page: Page) { super(); this._client = client; this._ignoreHTTPSErrors = ignoreHTTPSErrors; - this._frameManager = frameManager; + this._page = page; this._client.on('Fetch.requestPaused', this._onRequestPaused.bind(this)); this._client.on('Fetch.authRequired', this._onAuthRequired.bind(this)); @@ -198,7 +198,7 @@ export class NetworkManager extends EventEmitter { } } // TODO: how can frame be null here? - const frame = event.frameId ? this._frameManager.frame(event.frameId) : null; + const frame = event.frameId ? this._page._frameManager.frame(event.frameId) : null; const isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document'; const documentId = isNavigationRequest ? event.loaderId : undefined; const request = new InterceptableRequest(this._client, frame, interceptionId, documentId, this._userRequestInterceptionEnabled, event, redirectChain); diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index f7fdc015bd..a0a680bf9d 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -41,8 +41,6 @@ export class FrameManager extends EventEmitter implements PageDelegate { readonly _session: JugglerSession; readonly _page: Page; private readonly _networkManager: NetworkManager; - private _mainFrame: frames.Frame; - private readonly _frames: Map; private readonly _contextIdToContext: Map; private _eventListeners: RegisteredListener[]; @@ -51,10 +49,9 @@ export class FrameManager extends EventEmitter implements PageDelegate { this._session = session; this.rawKeyboard = new RawKeyboardImpl(session); this.rawMouse = new RawMouseImpl(session); - this._networkManager = new NetworkManager(session, this); - this._mainFrame = null; - this._frames = new Map(); this._contextIdToContext = new Map(); + this._page = new Page(this, browserContext); + this._networkManager = new NetworkManager(session, this._page); this._eventListeners = [ helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)), helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)), @@ -75,7 +72,6 @@ export class FrameManager extends EventEmitter implements PageDelegate { helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFinished, request => this._page.emit(Events.Page.RequestFinished, request)), helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this._page.emit(Events.Page.RequestFailed, request)), ]; - this._page = new Page(this, browserContext); (this._page as any).interception = new Interception(this._networkManager); (this._page as any).accessibility = new Accessibility(session); } @@ -95,7 +91,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { _onExecutionContextCreated({executionContextId, auxData}) { const frameId = auxData ? auxData.frameId : null; - const frame = this._frames.get(frameId) || null; + const frame = this._page._frameManager.frame(frameId); const delegate = new ExecutionContextDelegate(this._session, executionContextId); if (frame) { const context = new dom.FrameExecutionContext(delegate, frame); @@ -116,76 +112,36 @@ export class FrameManager extends EventEmitter implements PageDelegate { context.frame()._contextDestroyed(context as dom.FrameExecutionContext); } - frame(frameId: string): frames.Frame { - return this._frames.get(frameId); - } - - mainFrame(): frames.Frame { - return this._mainFrame; - } - - frames() { - const frames: Array = []; - collect(this._mainFrame); - return frames; - - function collect(frame: frames.Frame) { - frames.push(frame); - for (const subframe of frame.childFrames()) - collect(subframe); - } - } - _onNavigationStarted(params) { } _onNavigationAborted(params) { - const frame = this._frames.get(params.frameId); - frame._onAbortedNewDocumentNavigation(params.navigationId, params.errorText); + const frame = this._page._frameManager.frame(params.frameId); + for (const watcher of this._page._frameManager._lifecycleWatchers) + watcher._onAbortedNewDocumentNavigation(frame, params.navigationId, params.errorText); } - _onNavigationCommitted(params) { - const frame = this._frames.get(params.frameId); - frame._onCommittedNewDocumentNavigation(params.url, params.name, params.navigationId, false); - this._page.emit(Events.Page.FrameNavigated, frame); + _onNavigationCommitted(params: Protocol.Page.navigationCommittedPayload) { + this._page._frameManager.frameCommittedNewDocumentNavigation(params.frameId, params.url, params.name || '', params.navigationId, false); } - _onSameDocumentNavigation(params) { - const frame = this._frames.get(params.frameId); - frame._onCommittedSameDocumentNavigation(params.url); - this._page.emit(Events.Page.FrameNavigated, frame); + _onSameDocumentNavigation(params: Protocol.Page.sameDocumentNavigationPayload) { + this._page._frameManager.frameCommittedSameDocumentNavigation(params.frameId, params.url); } - _onFrameAttached(params) { - const parentFrame = this._frames.get(params.parentFrameId) || null; - const frame = new frames.Frame(this._page, params.frameId, parentFrame); - if (!parentFrame) { - assert(!this._mainFrame, 'INTERNAL ERROR: re-attaching main frame!'); - this._mainFrame = frame; - } - this._frames.set(params.frameId, frame); - this._page.emit(Events.Page.FrameAttached, frame); + _onFrameAttached(params: Protocol.Page.frameAttachedPayload) { + this._page._frameManager.frameAttached(params.frameId, params.parentFrameId); } - _onFrameDetached(params) { - const frame = this._frames.get(params.frameId); - this._frames.delete(params.frameId); - frame._onDetached(); - this._page.emit(Events.Page.FrameDetached, frame); + _onFrameDetached(params: Protocol.Page.frameDetachedPayload) { + this._page._frameManager.frameDetached(params.frameId); } _onEventFired({frameId, name}) { - const frame = this._frames.get(frameId); - if (name === 'load') { - frame._lifecycleEvent('load'); - if (frame === this._mainFrame) - this._page.emit(Events.Page.Load); - } - if (name === 'DOMContentLoaded') { - frame._lifecycleEvent('domcontentloaded'); - if (frame === this._mainFrame) - this._page.emit(Events.Page.DOMContentLoaded); - } + if (name === 'load') + this._page._frameManager.frameLifecycleEvent(frameId, 'load'); + if (name === 'DOMContentLoaded') + this._page._frameManager.frameLifecycleEvent(frameId, 'domcontentloaded'); } _onUncaughtError(params) { @@ -223,7 +179,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { async exposeBinding(name: string, bindingFunction: string): Promise { await this._session.send('Page.addBinding', {name: name}); await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction}); - await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); + await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); } didClose() { @@ -340,7 +296,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { timeout = this._page._timeoutSettings.navigationTimeout(), waitUntil = (['load'] as frames.LifecycleEvent[]), } = options; - const frame = this.mainFrame(); + const frame = this._page.mainFrame(); const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); const { navigationId } = await action(); if (navigationId === null) { @@ -360,15 +316,15 @@ export class FrameManager extends EventEmitter implements PageDelegate { } reload(options?: frames.NavigateOptions): Promise { - return this._go(() => this._session.send('Page.reload', { frameId: this.mainFrame()._id }), options); + return this._go(() => this._session.send('Page.reload', { frameId: this._page.mainFrame()._id }), options); } goBack(options?: frames.NavigateOptions): Promise { - return this._go(() => this._session.send('Page.goBack', { frameId: this.mainFrame()._id }), options); + return this._go(() => this._session.send('Page.goBack', { frameId: this._page.mainFrame()._id }), options); } goForward(options?: frames.NavigateOptions): Promise { - return this._go(() => this._session.send('Page.goForward', { frameId: this.mainFrame()._id }), options); + return this._go(() => this._session.send('Page.goForward', { frameId: this._page.mainFrame()._id }), options); } async evaluateOnNewDocument(source: string): Promise { @@ -416,7 +372,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { }); if (!frameId) return null; - return this.frame(frameId); + return this._page._frameManager.frame(frameId); } isElementHandle(remoteObject: any): boolean { diff --git a/src/firefox/NetworkManager.ts b/src/firefox/NetworkManager.ts index c2d2b58bdc..cee456ac6a 100644 --- a/src/firefox/NetworkManager.ts +++ b/src/firefox/NetworkManager.ts @@ -18,7 +18,7 @@ import { EventEmitter } from 'events'; import { assert, debugError, helper, RegisteredListener } from '../helper'; import { JugglerSession } from './Connection'; -import { FrameManager } from './FrameManager'; +import { Page } from '../page'; import * as network from '../network'; import * as frames from '../frames'; @@ -32,15 +32,15 @@ export const NetworkManagerEvents = { export class NetworkManager extends EventEmitter { private _session: JugglerSession; private _requests: Map; - private _frameManager: FrameManager; + private _page: Page; private _eventListeners: RegisteredListener[]; - constructor(session: JugglerSession, frameManager: FrameManager) { + constructor(session: JugglerSession, page: Page) { super(); this._session = session; this._requests = new Map(); - this._frameManager = frameManager; + this._page = page; this._eventListeners = [ helper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)), @@ -69,7 +69,7 @@ export class NetworkManager extends EventEmitter { _onRequestWillBeSent(event) { const redirected = event.redirectedFrom ? this._requests.get(event.redirectedFrom) : null; - const frame = redirected ? redirected.request.frame() : (this._frameManager && event.frameId ? this._frameManager.frame(event.frameId) : null); + const frame = redirected ? redirected.request.frame() : (event.frameId ? this._page._frameManager.frame(event.frameId) : null); if (!frame) return; let redirectChain: network.Request[] = []; diff --git a/src/frames.ts b/src/frames.ts index 1d0a89eecd..3de02c89cf 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -50,17 +50,144 @@ export type LifecycleEvent = 'load' | 'domcontentloaded'; export type WaitForOptions = types.TimeoutOptions & { waitFor?: boolean }; +export class FrameManager { + private _page: Page; + private _frames = new Map(); + private _mainFrame: Frame; + readonly _lifecycleWatchers = new Set(); + + constructor(page: Page) { + this._page = page; + } + + mainFrame(): Frame { + return this._mainFrame; + } + + frames() { + const frames: Frame[] = []; + collect(this._mainFrame); + return frames; + + function collect(frame: Frame) { + frames.push(frame); + for (const subframe of frame.childFrames()) + collect(subframe); + } + } + + frame(frameId: string): Frame | null { + return this._frames.get(frameId) || null; + } + + frameAttached(frameId: string, parentFrameId: string | null | undefined): Frame { + const parentFrame = parentFrameId ? this._frames.get(parentFrameId) : null; + if (!parentFrame) { + if (this._mainFrame) { + // Update frame id to retain frame identity on cross-process navigation. + this._frames.delete(this._mainFrame._id); + this._mainFrame._id = frameId; + } else { + assert(!this._frames.has(frameId)); + this._mainFrame = new Frame(this._page, frameId, parentFrame); + } + this._frames.set(frameId, this._mainFrame); + return this._mainFrame; + } else { + assert(!this._frames.has(frameId)); + const frame = new Frame(this._page, frameId, parentFrame); + this._frames.set(frameId, frame); + this._page.emit(Events.Page.FrameAttached, frame); + return frame; + } + } + + frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) { + const frame = this._frames.get(frameId); + for (const child of frame.childFrames()) + this._removeFramesRecursively(child); + frame._url = url; + frame._name = name; + frame._lastDocumentId = documentId; + frame._firedLifecycleEvents.clear(); + if (!initial) { + for (const watcher of this._lifecycleWatchers) + watcher._onCommittedNewDocumentNavigation(frame); + } + this._page.emit(Events.Page.FrameNavigated, frame); + } + + frameCommittedSameDocumentNavigation(frameId: string, url: string) { + const frame = this._frames.get(frameId); + if (!frame) + return; + frame._url = url; + for (const watcher of this._lifecycleWatchers) + watcher._onNavigatedWithinDocument(frame); + this._page.emit(Events.Page.FrameNavigated, frame); + } + + frameDetached(frameId: string) { + const frame = this._frames.get(frameId); + if (frame) + this._removeFramesRecursively(frame); + } + + frameStoppedLoading(frameId: string) { + const frame = this._frames.get(frameId); + if (!frame) + return; + const hasDOMContentLoaded = frame._firedLifecycleEvents.has('domcontentloaded'); + const hasLoad = frame._firedLifecycleEvents.has('load'); + frame._firedLifecycleEvents.add('domcontentloaded'); + frame._firedLifecycleEvents.add('load'); + for (const watcher of this._lifecycleWatchers) + watcher._onLifecycleEvent(frame); + if (frame === this.mainFrame() && !hasDOMContentLoaded) + this._page.emit(Events.Page.DOMContentLoaded); + if (frame === this.mainFrame() && !hasLoad) + this._page.emit(Events.Page.Load); + } + + frameLifecycleEvent(frameId: string, event: LifecycleEvent | 'clear') { + const frame = this._frames.get(frameId); + if (!frame) + return; + if (event === 'clear') { + frame._firedLifecycleEvents.clear(); + } else { + frame._firedLifecycleEvents.add(event); + for (const watcher of this._lifecycleWatchers) + watcher._onLifecycleEvent(frame); + } + if (frame === this._mainFrame && event === 'load') + this._page.emit(Events.Page.Load); + if (frame === this._mainFrame && event === 'domcontentloaded') + this._page.emit(Events.Page.DOMContentLoaded); + } + + private _removeFramesRecursively(frame: Frame) { + for (const child of frame.childFrames()) + this._removeFramesRecursively(child); + frame._onDetached(); + this._frames.delete(frame._id); + for (const watcher of this._lifecycleWatchers) + watcher._onFrameDetached(frame); + this._page.emit(Events.Page.FrameDetached, frame); + } +} + export class Frame { _id: string; readonly _firedLifecycleEvents: Set; _lastDocumentId: string; readonly _page: Page; private _parentFrame: Frame; - private _url = ''; + _url = ''; private _detached = false; private _contextData = new Map(); private _childFrames = new Set(); - private _name: string; + _name: string; constructor(page: Page, id: string, parentFrame: Frame | null) { this._id = id; @@ -387,7 +514,7 @@ export class Frame { handle = await this._waitForSelectorInUtilityContext(selector, options); } else { const context = await this._utilityContext(); - handle = await context._$(types.clearSelector(selector)); + handle = await context._$(types.clearSelector(selector)); } assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); return handle; @@ -431,39 +558,6 @@ export class Frame { return context.evaluate(() => document.title); } - _onNavigationRequest(request: network.Request) { - for (const watcher of this._page._lifecycleWatchers) - watcher._onNavigationRequest(this, request); - } - - _onAbortedNewDocumentNavigation(documentId: string, errorText: string) { - for (const watcher of this._page._lifecycleWatchers) - watcher._onAbortedNewDocumentNavigation(this, documentId, errorText); - } - - _onCommittedNewDocumentNavigation(url: string, name: string, documentId: string, initial: boolean) { - this._url = url; - this._name = name; - this._lastDocumentId = documentId; - this._firedLifecycleEvents.clear(); - if (!initial) { - for (const watcher of this._page._lifecycleWatchers) - watcher._onCommittedNewDocumentNavigation(this); - } - } - - _onCommittedSameDocumentNavigation(url: string) { - this._url = url; - for (const watcher of this._page._lifecycleWatchers) - watcher._onNavigatedWithinDocument(this); - } - - _lifecycleEvent(event: LifecycleEvent) { - this._firedLifecycleEvents.add(event); - for (const watcher of this._page._lifecycleWatchers) - watcher._onLifecycleEvent(this); - } - _onDetached() { this._detached = true; for (const data of this._contextData.values()) { @@ -473,8 +567,6 @@ export class Frame { if (this._parentFrame) this._parentFrame._childFrames.delete(this); this._parentFrame = null; - for (const watcher of this._page._lifecycleWatchers) - watcher._onFrameDetached(this); } private _scheduleRerunnableTask(task: dom.Task, contextType: ContextType, timeout?: number, title?: string): Promise { @@ -634,7 +726,7 @@ export class LifecycleWatcher { new Promise(f => this._navigationAbortedCallback = f), this._frame._page._disconnectedPromise.then(() => new Error('Navigation failed because browser has disconnected!')), ]); - frame._page._lifecycleWatchers.add(this); + frame._page._frameManager._lifecycleWatchers.add(this); this._checkLifecycleComplete(); } @@ -718,7 +810,7 @@ export class LifecycleWatcher { } dispose() { - this._frame._page._lifecycleWatchers.delete(this); + this._frame._page._frameManager._lifecycleWatchers.delete(this); clearTimeout(this._maximumTimer); } } diff --git a/src/network.ts b/src/network.ts index d3ea79b917..4f223c5534 100644 --- a/src/network.ts +++ b/src/network.ts @@ -101,19 +101,23 @@ export class Request { this._headers = headers; this._waitForResponsePromise = new Promise(f => this._waitForResponsePromiseCallback = f); this._waitForFinishedPromise = new Promise(f => this._waitForFinishedPromiseCallback = f); - if (documentId && frame) - frame._onNavigationRequest(this); + if (documentId && frame) { + for (const watcher of frame._page._frameManager._lifecycleWatchers) + watcher._onNavigationRequest(frame, this); + } } _setFailureText(failureText: string, canceled: boolean) { this._failureText = failureText; if (this._documentId && this._frame) { const isCurrentDocument = this._frame._lastDocumentId === this._documentId; - let errorText = failureText; - if (canceled) - errorText += '; maybe frame was detached?'; - if (!isCurrentDocument) - this._frame._onAbortedNewDocumentNavigation(this._documentId, errorText); + if (!isCurrentDocument) { + let errorText = failureText; + if (canceled) + errorText += '; maybe frame was detached?'; + for (const watcher of this._frame._page._frameManager._lifecycleWatchers) + watcher._onAbortedNewDocumentNavigation(this._frame, this._documentId, errorText); + } } this._waitForFinishedPromiseCallback(); } diff --git a/src/page.ts b/src/page.ts index 0bf1a5a16d..6fd4bf5609 100644 --- a/src/page.ts +++ b/src/page.ts @@ -43,8 +43,6 @@ export interface PageDelegate { // TODO: reverse didClose call sequence. didClose(): void; - mainFrame(): frames.Frame; - frames(): frames.Frame[]; navigateFrame(frame: frames.Frame, url: string, options?: frames.GotoOptions): Promise; waitForFrameNavigation(frame: frames.Frame, options?: frames.NavigateOptions): Promise; setFrameContent(frame: frames.Frame, html: string, options?: frames.NavigateOptions): Promise; @@ -104,7 +102,7 @@ export class Page extends EventEmitter { private _pageBindings = new Map(); readonly _screenshotter: Screenshotter; private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); - readonly _lifecycleWatchers = new Set(); + readonly _frameManager: frames.FrameManager; constructor(delegate: PageDelegate, browserContext: BrowserContext) { super(); @@ -126,6 +124,7 @@ export class Page extends EventEmitter { this.mouse = new input.Mouse(delegate.rawMouse, this.keyboard); this._timeoutSettings = new TimeoutSettings(); this._screenshotter = new Screenshotter(this); + this._frameManager = new frames.FrameManager(this); } _didClose() { @@ -178,11 +177,11 @@ export class Page extends EventEmitter { } mainFrame(): frames.Frame { - return this._delegate.mainFrame(); + return this._frameManager.mainFrame(); } frames(): frames.Frame[] { - return this._delegate.frames(); + return this._frameManager.frames(); } setDefaultNavigationTimeout(timeout: number) { diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 5307aab128..797f327b89 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -45,22 +45,19 @@ export class FrameManager extends EventEmitter implements PageDelegate { _session: TargetSession; readonly _page: Page; private readonly _networkManager: NetworkManager; - private readonly _frames: Map; private readonly _contextIdToContext: Map; private _isolatedWorlds: Set; private _sessionListeners: RegisteredListener[] = []; - private _mainFrame: frames.Frame; private readonly _bootstrapScripts: string[] = []; constructor(browserContext: BrowserContext) { super(); this.rawKeyboard = new RawKeyboardImpl(); this.rawMouse = new RawMouseImpl(); - this._networkManager = new NetworkManager(this); - this._frames = new Map(); this._contextIdToContext = new Map(); this._isolatedWorlds = new Set(); this._page = new Page(this, browserContext); + this._networkManager = new NetworkManager(this._page); this._networkManager.on(NetworkManagerEvents.Request, event => this._page.emit(Events.Page.Request, event)); this._networkManager.on(NetworkManagerEvents.Response, event => this._page.emit(Events.Page.Response, event)); this._networkManager.on(NetworkManagerEvents.RequestFailed, event => this._page.emit(Events.Page.RequestFailed, event)); @@ -142,30 +139,11 @@ export class FrameManager extends EventEmitter implements PageDelegate { } _onFrameStoppedLoading(frameId: string) { - const frame = this._frames.get(frameId); - if (!frame) - return; - const hasDOMContentLoaded = frame._firedLifecycleEvents.has('domcontentloaded'); - const hasLoad = frame._firedLifecycleEvents.has('load'); - frame._lifecycleEvent('domcontentloaded'); - frame._lifecycleEvent('load'); - if (frame === this.mainFrame() && !hasDOMContentLoaded) - this._page.emit(Events.Page.DOMContentLoaded); - if (frame === this.mainFrame() && !hasLoad) - this._page.emit(Events.Page.Load); + this._page._frameManager.frameStoppedLoading(frameId); } _onLifecycleEvent(frameId: string, event: frames.LifecycleEvent) { - const frame = this._frames.get(frameId); - if (!frame) - return; - frame._lifecycleEvent(event); - if (frame === this.mainFrame()) { - if (event === 'load') - this._page.emit(Events.Page.Load); - if (event === 'domcontentloaded') - this._page.emit(Events.Page.DOMContentLoaded); - } + this._page._frameManager.frameLifecycleEvent(frameId, event); } _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) { @@ -178,47 +156,12 @@ export class FrameManager extends EventEmitter implements PageDelegate { this._handleFrameTree(child); } - page(): Page { - return this._page; - } - - mainFrame(): frames.Frame { - return this._mainFrame; - } - - frames(): Array { - return Array.from(this._frames.values()); - } - - frame(frameId: string): frames.Frame | null { - return this._frames.get(frameId) || null; - } - _onFrameAttached(frameId: string, parentFrameId: string | null) { - assert(!this._frames.has(frameId)); - const parentFrame = parentFrameId ? this._frames.get(parentFrameId) : null; - const frame = new frames.Frame(this._page, frameId, parentFrame); - this._frames.set(frameId, frame); - if (!parentFrame) - this._mainFrame = frame; - this._page.emit(Events.Page.FrameAttached, frame); - return frame; + this._page._frameManager.frameAttached(frameId, parentFrameId); } _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) { - const isMainFrame = !framePayload.parentId; - const frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id); - - // Detach all child frames first. - for (const child of frame.childFrames()) - this._removeFramesRecursively(child); - if (isMainFrame) { - // Update frame id to retain frame identity on cross-process navigation. - this._frames.delete(frame._id); - frame._id = framePayload.id; - this._frames.set(framePayload.id, frame); - } - + const frame = this._page._frameManager.frame(framePayload.id); for (const context of this._contextIdToContext.values()) { if (context.frame() === frame) { const delegate = context._delegate as ExecutionContextDelegate; @@ -227,26 +170,17 @@ export class FrameManager extends EventEmitter implements PageDelegate { frame._contextDestroyed(context as dom.FrameExecutionContext); } } - // Append session id to avoid cross-process loaderId clash. const documentId = this._session._sessionId + '::' + framePayload.loaderId; - frame._onCommittedNewDocumentNavigation(framePayload.url, framePayload.name, documentId, initial); - - this._page.emit(Events.Page.FrameNavigated, frame); + this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', documentId, initial); } _onFrameNavigatedWithinDocument(frameId: string, url: string) { - const frame = this._frames.get(frameId); - if (!frame) - return; - frame._onCommittedSameDocumentNavigation(url); - this._page.emit(Events.Page.FrameNavigated, frame); + this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url); } _onFrameDetached(frameId: string) { - const frame = this._frames.get(frameId); - if (frame) - this._removeFramesRecursively(frame); + this._page._frameManager.frameDetached(frameId); } _onExecutionContextCreated(contextPayload : Protocol.Runtime.ExecutionContextDescription) { @@ -255,7 +189,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { const frameId = contextPayload.frameId; // If the frame was attached manually there is no navigation event. // FIXME: support frameAttached event in WebKit protocol. - const frame = this._frames.get(frameId) || null; + const frame = this._page._frameManager.frame(frameId); if (!frame) return; const delegate = new ExecutionContextDelegate(this._session, contextPayload); @@ -277,14 +211,6 @@ export class FrameManager extends EventEmitter implements PageDelegate { return context; } - _removeFramesRecursively(frame: frames.Frame) { - for (const child of frame.childFrames()) - this._removeFramesRecursively(child); - frame._onDetached(); - this._frames.delete(frame._id); - this._page.emit(Events.Page.FrameDetached, frame); - } - async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise { const { timeout = this._page._timeoutSettings.navigationTimeout(), @@ -355,7 +281,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { else if (type === 'timing') derivedType = 'timeEnd'; - const mainFrameContext = await this.mainFrame().executionContext(); + const mainFrameContext = await this._page.mainFrame().executionContext(); const handles = (parameters || []).map(p => { let context: js.ExecutionContext | null = null; if (p.objectId) { @@ -380,7 +306,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { } async _onFileChooserOpened(event: {frameId: Protocol.Network.FrameId, element: Protocol.Runtime.RemoteObject}) { - const context = await this.frame(event.frameId)._mainContext(); + const context = await this._page._frameManager.frame(event.frameId)._mainContext(); const handle = context._createHandle(event.element).asElement()!; this._page._onFileChooserOpened(handle); } @@ -486,7 +412,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { this._bootstrapScripts.unshift(script); const source = this._bootstrapScripts.join(';'); await this._session.send('Page.setBootstrapScript', { source }); - await Promise.all(this.frames().map(frame => frame.evaluate(script).catch(debugError))); + await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError))); } async evaluateOnNewDocument(script: string): Promise { diff --git a/src/webkit/NetworkManager.ts b/src/webkit/NetworkManager.ts index 2595dba7a7..24828b8d8c 100644 --- a/src/webkit/NetworkManager.ts +++ b/src/webkit/NetworkManager.ts @@ -17,7 +17,7 @@ import { EventEmitter } from 'events'; import { TargetSession } from './Connection'; -import { FrameManager } from './FrameManager'; +import { Page } from '../page'; import { assert, helper, RegisteredListener } from '../helper'; import { Protocol } from './protocol'; import * as network from '../network'; @@ -32,16 +32,16 @@ export const NetworkManagerEvents = { export class NetworkManager extends EventEmitter { private _session: TargetSession; - private _frameManager: FrameManager; + private _page: Page; private _requestIdToRequest = new Map(); private _extraHTTPHeaders: network.Headers = {}; private _attemptedAuthentications = new Set(); private _userCacheDisabled = false; private _sessionListeners: RegisteredListener[] = []; - constructor(frameManager: FrameManager) { + constructor(page: Page) { super(); - this._frameManager = frameManager; + this._page = page; } setSession(session: TargetSession) { @@ -97,7 +97,7 @@ export class NetworkManager extends EventEmitter { redirectChain = request.request._redirectChain; } } - const frame = this._frameManager.frame(event.frameId); + const frame = this._page._frameManager.frame(event.frameId); // TODO(einbinder) this will fail if we are an XHR document request const isNavigationRequest = event.type === 'Document'; const documentId = isNavigationRequest ? this._session._sessionId + '::' + event.loaderId : undefined;