chore: introduce FrameManager to be reused between browsers (#261)

This commit is contained in:
Dmitry Gozman 2019-12-16 15:56:11 -08:00 committed by GitHub
parent 6b9f475217
commit 4b7a017456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 307 deletions

View File

@ -9,7 +9,7 @@
"main": "index.js",
"playwright": {
"chromium_revision": "724623",
"firefox_revision": "1005",
"firefox_revision": "1007",
"webkit_revision": "1038"
},
"scripts": {

View File

@ -49,10 +49,8 @@ export class FrameManager extends EventEmitter implements PageDelegate {
_client: CDPSession;
private _page: Page;
private _networkManager: NetworkManager;
private _frames = new Map<string, frames.Frame>();
private _contextIdToContext = new Map<number, js.ExecutionContext>();
private _isolatedWorlds = new Set<string>();
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 {

View File

@ -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<string, InterceptableRequest>();
private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
private _extraHTTPHeaders: network.Headers = {};
@ -45,11 +45,11 @@ export class NetworkManager extends EventEmitter {
private _userCacheDisabled = false;
private _requestIdToInterceptionId = new Map<string, string>();
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);

View File

@ -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<string, frames.Frame>;
private readonly _contextIdToContext: Map<string, js.ExecutionContext>;
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<frames.Frame> = [];
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<void> {
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<network.Response | null> {
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<network.Response | null> {
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<network.Response | null> {
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<void> {
@ -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 {

View File

@ -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<string, InterceptableRequest>;
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[] = [];

View File

@ -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<string, Frame>();
private _mainFrame: Frame;
readonly _lifecycleWatchers = new Set<LifecycleWatcher>();
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<LifecycleEvent>;
_lastDocumentId: string;
readonly _page: Page;
private _parentFrame: Frame;
private _url = '';
_url = '';
private _detached = false;
private _contextData = new Map<ContextType, ContextData>();
private _childFrames = new Set<Frame>();
private _name: string;
_name: string;
constructor(page: Page, id: string, parentFrame: Frame | null) {
this._id = id;
@ -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<js.JSHandle> {
@ -634,7 +726,7 @@ export class LifecycleWatcher {
new Promise<Error>(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);
}
}

View File

@ -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();
}

View File

@ -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<network.Response | null>;
waitForFrameNavigation(frame: frames.Frame, options?: frames.NavigateOptions): Promise<network.Response | null>;
setFrameContent(frame: frames.Frame, html: string, options?: frames.NavigateOptions): Promise<void>;
@ -104,7 +102,7 @@ export class Page extends EventEmitter {
private _pageBindings = new Map<string, Function>();
readonly _screenshotter: Screenshotter;
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
readonly _lifecycleWatchers = new Set<frames.LifecycleWatcher>();
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) {

View File

@ -45,22 +45,19 @@ export class FrameManager extends EventEmitter implements PageDelegate {
_session: TargetSession;
readonly _page: Page;
private readonly _networkManager: NetworkManager;
private readonly _frames: Map<string, frames.Frame>;
private readonly _contextIdToContext: Map<number, js.ExecutionContext>;
private _isolatedWorlds: Set<string>;
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<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) {
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<network.Response | null> {
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<void> {

View File

@ -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<string, InterceptableRequest>();
private _extraHTTPHeaders: network.Headers = {};
private _attemptedAuthentications = new Set<string>();
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;