chore: reuse LifecycleWatcher between browsers (#208)
This commit is contained in:
parent
e42e361d20
commit
57acdfd860
|
|
@ -24,7 +24,6 @@ import * as network from '../network';
|
|||
import { CDPSession } from './Connection';
|
||||
import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext';
|
||||
import { DOMWorldDelegate } from './JSHandle';
|
||||
import { LifecycleWatcher } from './LifecycleWatcher';
|
||||
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
||||
import { Page } from '../page';
|
||||
import { Protocol } from './protocol';
|
||||
|
|
@ -59,7 +58,6 @@ export const FrameManagerEvents = {
|
|||
const frameDataSymbol = Symbol('frameData');
|
||||
type FrameData = {
|
||||
id: string,
|
||||
loaderId: string,
|
||||
};
|
||||
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
|
||||
|
|
@ -149,16 +147,16 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
|
||||
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
|
||||
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
let ensureNewDocumentNavigation = false;
|
||||
let error = await Promise.race([
|
||||
navigate(this._client, url, referer, this._frameData(frame).id),
|
||||
watcher.timeoutOrTerminationPromise(),
|
||||
watcher.timeoutOrTerminationPromise,
|
||||
]);
|
||||
if (!error) {
|
||||
error = await Promise.race([
|
||||
watcher.timeoutOrTerminationPromise(),
|
||||
ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise() : watcher.sameDocumentNavigationPromise(),
|
||||
watcher.timeoutOrTerminationPromise,
|
||||
ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise : watcher.sameDocumentNavigationPromise,
|
||||
]);
|
||||
}
|
||||
watcher.dispose();
|
||||
|
|
@ -183,11 +181,11 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
||||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
|
||||
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
const error = await Promise.race([
|
||||
watcher.timeoutOrTerminationPromise(),
|
||||
watcher.sameDocumentNavigationPromise(),
|
||||
watcher.newDocumentNavigationPromise()
|
||||
watcher.timeoutOrTerminationPromise,
|
||||
watcher.sameDocumentNavigationPromise,
|
||||
watcher.newDocumentNavigationPromise,
|
||||
]);
|
||||
watcher.dispose();
|
||||
if (error)
|
||||
|
|
@ -208,10 +206,10 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
document.write(html);
|
||||
document.close();
|
||||
}, html);
|
||||
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
|
||||
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
const error = await Promise.race([
|
||||
watcher.timeoutOrTerminationPromise(),
|
||||
watcher.lifecyclePromise(),
|
||||
watcher.timeoutOrTerminationPromise,
|
||||
watcher.lifecyclePromise,
|
||||
]);
|
||||
watcher.dispose();
|
||||
if (error)
|
||||
|
|
@ -222,15 +220,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
const frame = this._frames.get(event.frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
const data = this._frameData(frame);
|
||||
if (event.name === 'init') {
|
||||
data.loaderId = event.loaderId;
|
||||
frame._firedLifecycleEvents.clear();
|
||||
frame._onExpectedNewDocumentNavigation(event.loaderId);
|
||||
} else if (event.name === 'load') {
|
||||
frame._lifecycleEvent('load');
|
||||
} else if (event.name === 'DOMContentLoaded') {
|
||||
frame._lifecycleEvent('domcontentloaded');
|
||||
}
|
||||
if (event.name === 'load')
|
||||
frame._firedLifecycleEvents.add('load');
|
||||
else if (event.name === 'DOMContentLoaded')
|
||||
frame._firedLifecycleEvents.add('domcontentloaded');
|
||||
this.emit(FrameManagerEvents.LifecycleEvent, frame);
|
||||
}
|
||||
|
||||
|
|
@ -238,8 +235,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
const frame = this._frames.get(frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._firedLifecycleEvents.add('domcontentloaded');
|
||||
frame._firedLifecycleEvents.add('load');
|
||||
frame._lifecycleEvent('domcontentloaded');
|
||||
frame._lifecycleEvent('load');
|
||||
this.emit(FrameManagerEvents.LifecycleEvent, frame);
|
||||
}
|
||||
|
||||
|
|
@ -275,10 +272,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
return;
|
||||
assert(parentFrameId);
|
||||
const parentFrame = this._frames.get(parentFrameId);
|
||||
const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame);
|
||||
const frame = new frames.Frame(this, this._page, parentFrame);
|
||||
const data: FrameData = {
|
||||
id: frameId,
|
||||
loaderId: '',
|
||||
};
|
||||
(frame as any)[frameDataSymbol] = data;
|
||||
this._frames.set(frameId, frame);
|
||||
|
|
@ -306,10 +302,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
data.id = framePayload.id;
|
||||
} else {
|
||||
// Initial main frame navigation.
|
||||
frame = new frames.Frame(this, this._page._timeoutSettings, null);
|
||||
frame = new frames.Frame(this, this._page, null);
|
||||
const data: FrameData = {
|
||||
id: framePayload.id,
|
||||
loaderId: '',
|
||||
};
|
||||
(frame as any)[frameDataSymbol] = data;
|
||||
}
|
||||
|
|
@ -317,8 +312,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
this._mainFrame = frame;
|
||||
}
|
||||
|
||||
// Update frame payload.
|
||||
frame._navigated(framePayload.url, framePayload.name);
|
||||
frame._onCommittedNewDocumentNavigation(framePayload.url, framePayload.name, framePayload.loaderId);
|
||||
|
||||
this.emit(FrameManagerEvents.FrameNavigated, frame);
|
||||
this._page.emit(Events.Page.FrameNavigated, frame);
|
||||
|
|
@ -343,7 +337,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
const frame = this._frames.get(frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._navigated(url, frame.name());
|
||||
frame._onCommittedSameDocumentNavigation(url);
|
||||
this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame);
|
||||
this.emit(FrameManagerEvents.FrameNavigated, frame);
|
||||
this._page.emit(Events.Page.FrameNavigated, frame);
|
||||
|
|
@ -395,7 +389,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
_removeFramesRecursively(frame: frames.Frame) {
|
||||
for (const child of frame.childFrames())
|
||||
this._removeFramesRecursively(child);
|
||||
frame._detach();
|
||||
frame._onDetached();
|
||||
this._frames.delete(this._frameData(frame).id);
|
||||
this.emit(FrameManagerEvents.FrameDetached, frame);
|
||||
this._page.emit(Events.Page.FrameDetached, frame);
|
||||
|
|
|
|||
|
|
@ -1,165 +0,0 @@
|
|||
/**
|
||||
* Copyright 2019 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CDPSessionEvents } from './Connection';
|
||||
import { TimeoutError } from '../Errors';
|
||||
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
||||
import { helper, RegisteredListener } from '../helper';
|
||||
import { NetworkManagerEvents } from './NetworkManager';
|
||||
import * as frames from '../frames';
|
||||
import * as network from '../network';
|
||||
|
||||
export class LifecycleWatcher {
|
||||
private _expectedLifecycle: frames.LifecycleEvent[];
|
||||
private _frameManager: FrameManager;
|
||||
private _frame: frames.Frame;
|
||||
private _initialLoaderId: string;
|
||||
private _timeout: number;
|
||||
private _navigationRequest: network.Request | null = null;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
private _sameDocumentNavigationPromise: Promise<Error | null>;
|
||||
private _sameDocumentNavigationCompleteCallback: () => void;
|
||||
private _lifecyclePromise: Promise<void>;
|
||||
private _lifecycleCallback: () => void;
|
||||
private _newDocumentNavigationPromise: Promise<Error | null>;
|
||||
private _newDocumentNavigationCompleteCallback: () => void;
|
||||
private _timeoutPromise: Promise<Error>;
|
||||
private _terminationPromise: Promise<Error | null>;
|
||||
private _terminationCallback: (err: Error | null) => void;
|
||||
private _maximumTimer: NodeJS.Timer;
|
||||
private _hasSameDocumentNavigation: boolean;
|
||||
|
||||
constructor(frameManager: FrameManager, frame: frames.Frame, waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[], timeout: number) {
|
||||
if (Array.isArray(waitUntil))
|
||||
waitUntil = waitUntil.slice();
|
||||
else if (typeof waitUntil === 'string')
|
||||
waitUntil = [waitUntil];
|
||||
this._expectedLifecycle = waitUntil.slice();
|
||||
this._frameManager = frameManager;
|
||||
this._frame = frame;
|
||||
this._initialLoaderId = frameManager._frameData(frame).loaderId;
|
||||
this._timeout = timeout;
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(frameManager._client, CDPSessionEvents.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))),
|
||||
helper.addEventListener(this._frameManager, FrameManagerEvents.LifecycleEvent, this._checkLifecycleComplete.bind(this)),
|
||||
helper.addEventListener(this._frameManager, FrameManagerEvents.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)),
|
||||
helper.addEventListener(this._frameManager, FrameManagerEvents.FrameDetached, this._onFrameDetached.bind(this)),
|
||||
helper.addEventListener(this._frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)),
|
||||
];
|
||||
|
||||
this._sameDocumentNavigationPromise = new Promise(fulfill => {
|
||||
this._sameDocumentNavigationCompleteCallback = fulfill;
|
||||
});
|
||||
|
||||
this._lifecyclePromise = new Promise(fulfill => {
|
||||
this._lifecycleCallback = fulfill;
|
||||
});
|
||||
|
||||
this._newDocumentNavigationPromise = new Promise(fulfill => {
|
||||
this._newDocumentNavigationCompleteCallback = fulfill;
|
||||
});
|
||||
|
||||
this._timeoutPromise = this._createTimeoutPromise();
|
||||
this._terminationPromise = new Promise(fulfill => {
|
||||
this._terminationCallback = fulfill;
|
||||
});
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_onRequest(request: network.Request) {
|
||||
if (request.frame() !== this._frame || !request.isNavigationRequest())
|
||||
return;
|
||||
this._navigationRequest = request;
|
||||
}
|
||||
|
||||
_onFrameDetached(frame: frames.Frame) {
|
||||
if (this._frame === frame) {
|
||||
this._terminationCallback.call(null, new Error('Navigating frame was detached'));
|
||||
return;
|
||||
}
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
navigationResponse(): network.Response | null {
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
_terminate(error: Error) {
|
||||
this._terminationCallback.call(null, error);
|
||||
}
|
||||
|
||||
sameDocumentNavigationPromise(): Promise<Error | null> {
|
||||
return this._sameDocumentNavigationPromise;
|
||||
}
|
||||
|
||||
newDocumentNavigationPromise(): Promise<Error | null> {
|
||||
return this._newDocumentNavigationPromise;
|
||||
}
|
||||
|
||||
lifecyclePromise(): Promise<any> {
|
||||
return this._lifecyclePromise;
|
||||
}
|
||||
|
||||
timeoutOrTerminationPromise(): Promise<Error | null> {
|
||||
return Promise.race([this._timeoutPromise, this._terminationPromise]);
|
||||
}
|
||||
|
||||
_createTimeoutPromise(): Promise<Error | null> {
|
||||
if (!this._timeout)
|
||||
return new Promise(() => {});
|
||||
const errorMessage = 'Navigation timeout of ' + this._timeout + ' ms exceeded';
|
||||
return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout))
|
||||
.then(() => new TimeoutError(errorMessage));
|
||||
}
|
||||
|
||||
_navigatedWithinDocument(frame: frames.Frame) {
|
||||
if (frame !== this._frame)
|
||||
return;
|
||||
this._hasSameDocumentNavigation = true;
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_checkLifecycleComplete() {
|
||||
const checkLifecycle = (frame: frames.Frame, expectedLifecycle: frames.LifecycleEvent[]): boolean => {
|
||||
for (const event of expectedLifecycle) {
|
||||
if (!frame._firedLifecycleEvents.has(event))
|
||||
return false;
|
||||
}
|
||||
for (const child of frame.childFrames()) {
|
||||
if (!checkLifecycle(child, expectedLifecycle))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// We expect navigation to commit.
|
||||
if (!checkLifecycle(this._frame, this._expectedLifecycle))
|
||||
return;
|
||||
this._lifecycleCallback();
|
||||
if (this._frameManager._frameData(this._frame).loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation)
|
||||
return;
|
||||
if (this._hasSameDocumentNavigation)
|
||||
this._sameDocumentNavigationCompleteCallback();
|
||||
if (this._frameManager._frameData(this._frame).loaderId !== this._initialLoaderId)
|
||||
this._newDocumentNavigationCompleteCallback();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
clearTimeout(this._maximumTimer);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,14 +16,12 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { TimeoutError } from '../Errors';
|
||||
import * as frames from '../frames';
|
||||
import { assert, helper, RegisteredListener, debugError } from '../helper';
|
||||
import * as js from '../javascript';
|
||||
import * as dom from '../dom';
|
||||
import { JugglerSession } from './Connection';
|
||||
import { ExecutionContextDelegate } from './ExecutionContext';
|
||||
import { NavigationWatchdog, NextNavigationWatchdog } from './NavigationWatchdog';
|
||||
import { Page, PageDelegate } from '../page';
|
||||
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
||||
import { DOMWorldDelegate } from './JSHandle';
|
||||
|
|
@ -51,7 +49,6 @@ export const FrameManagerEvents = {
|
|||
const frameDataSymbol = Symbol('frameData');
|
||||
type FrameData = {
|
||||
frameId: string,
|
||||
lastCommittedNavigationId: string,
|
||||
};
|
||||
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
|
||||
|
|
@ -80,7 +77,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)),
|
||||
|
|
@ -157,29 +156,35 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
}
|
||||
}
|
||||
|
||||
_onNavigationStarted(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
frame._onExpectedNewDocumentNavigation(params.navigationId, params.url);
|
||||
}
|
||||
|
||||
_onNavigationAborted(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
frame._onAbortedNewDocumentNavigation(params.navigationId, params.errorText);
|
||||
}
|
||||
|
||||
_onNavigationCommitted(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
frame._navigated(params.url, params.name);
|
||||
const data = this._frameData(frame);
|
||||
data.lastCommittedNavigationId = params.navigationId;
|
||||
frame._firedLifecycleEvents.clear();
|
||||
frame._onCommittedNewDocumentNavigation(params.url, params.name, params.navigationId);
|
||||
this.emit(FrameManagerEvents.FrameNavigated, frame);
|
||||
this._page.emit(Events.Page.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
_onSameDocumentNavigation(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
frame._navigated(params.url, frame.name());
|
||||
frame._onCommittedSameDocumentNavigation(params.url);
|
||||
this.emit(FrameManagerEvents.FrameNavigated, frame);
|
||||
this._page.emit(Events.Page.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
_onFrameAttached(params) {
|
||||
const parentFrame = this._frames.get(params.parentFrameId) || null;
|
||||
const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame);
|
||||
const frame = new frames.Frame(this, this._page, parentFrame);
|
||||
const data: FrameData = {
|
||||
frameId: params.frameId,
|
||||
lastCommittedNavigationId: '',
|
||||
};
|
||||
frame[frameDataSymbol] = data;
|
||||
if (!parentFrame) {
|
||||
|
|
@ -194,7 +199,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
_onFrameDetached(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
this._frames.delete(params.frameId);
|
||||
frame._detach();
|
||||
frame._onDetached();
|
||||
this.emit(FrameManagerEvents.FrameDetached, frame);
|
||||
this._page.emit(Events.Page.FrameDetached, frame);
|
||||
}
|
||||
|
|
@ -202,14 +207,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
_onEventFired({frameId, name}) {
|
||||
const frame = this._frames.get(frameId);
|
||||
if (name === 'load') {
|
||||
frame._firedLifecycleEvents.add('load');
|
||||
frame._lifecycleEvent('load');
|
||||
if (frame === this._mainFrame) {
|
||||
this.emit(FrameManagerEvents.Load);
|
||||
this._page.emit(Events.Page.Load);
|
||||
}
|
||||
}
|
||||
if (name === 'DOMContentLoaded') {
|
||||
frame._firedLifecycleEvents.add('domcontentloaded');
|
||||
frame._lifecycleEvent('domcontentloaded');
|
||||
if (frame === this._mainFrame) {
|
||||
this.emit(FrameManagerEvents.DOMContentLoaded);
|
||||
this._page.emit(Events.Page.DOMContentLoaded);
|
||||
|
|
@ -265,44 +270,17 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
||||
} = options;
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const nextNavigationDog = new NextNavigationWatchdog(this, frame);
|
||||
const error1 = await Promise.race([
|
||||
nextNavigationDog.promise(),
|
||||
timeoutPromise,
|
||||
]);
|
||||
nextNavigationDog.dispose();
|
||||
|
||||
// If timeout happened first - throw.
|
||||
if (error1) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error1;
|
||||
}
|
||||
|
||||
const {navigationId, url} = nextNavigationDog.navigation();
|
||||
|
||||
if (!navigationId) {
|
||||
// Same document navigation happened.
|
||||
clearTimeout(timeoutId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, url, normalizedWaitUntil);
|
||||
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
watcher.timeoutOrTerminationPromise,
|
||||
watcher.newDocumentNavigationPromise,
|
||||
watcher.sameDocumentNavigationPromise,
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
watcher.dispose();
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
return watcher.navigationResponse();
|
||||
}
|
||||
|
||||
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}) {
|
||||
|
|
@ -311,30 +289,21 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
||||
referer,
|
||||
} = options;
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId} = await this._session.send('Page.navigate', {
|
||||
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
await this._session.send('Page.navigate', {
|
||||
frameId: this._frameData(frame).frameId,
|
||||
referer,
|
||||
url,
|
||||
});
|
||||
if (!navigationId)
|
||||
return;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, url, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
watcher.timeoutOrTerminationPromise,
|
||||
watcher.newDocumentNavigationPromise,
|
||||
watcher.sameDocumentNavigationPromise,
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
watcher.dispose();
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
return watcher.navigationResponse();
|
||||
}
|
||||
|
||||
async setFrameContent(frame: frames.Frame, html: string) {
|
||||
|
|
@ -387,32 +356,28 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled});
|
||||
}
|
||||
|
||||
private async _go(action: () => Promise<{ navigationId: string | null, navigationURL: string | null }>, options: frames.NavigateOptions = {}) {
|
||||
private async _go(action: () => Promise<{ navigationId: string | null, navigationURL: string | null }>, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
|
||||
const {
|
||||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
||||
} = options;
|
||||
const frame = this.mainFrame();
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const { navigationId, navigationURL } = await action();
|
||||
if (!navigationId)
|
||||
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
const { navigationId } = await action();
|
||||
if (navigationId === null) {
|
||||
// Cannot go back/forward.
|
||||
watcher.dispose();
|
||||
return null;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil);
|
||||
}
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
watcher.timeoutOrTerminationPromise,
|
||||
watcher.newDocumentNavigationPromise,
|
||||
watcher.sameDocumentNavigationPromise,
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
watcher.dispose();
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
return watcher.navigationResponse();
|
||||
}
|
||||
|
||||
reload(options?: frames.NavigateOptions): Promise<network.Response | null> {
|
||||
|
|
|
|||
|
|
@ -1,149 +0,0 @@
|
|||
/**
|
||||
* Copyright 2019 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { helper, RegisteredListener } from '../helper';
|
||||
import { JugglerSessionEvents } from './Connection';
|
||||
import { FrameManagerEvents, FrameManager } from './FrameManager';
|
||||
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
||||
import * as frames from '../frames';
|
||||
import * as network from '../network';
|
||||
|
||||
export class NextNavigationWatchdog {
|
||||
private _frameManager: FrameManager;
|
||||
private _navigatedFrame: frames.Frame;
|
||||
private _promise: Promise<unknown>;
|
||||
private _resolveCallback: (value?: unknown) => void;
|
||||
private _navigation: {navigationId: number|null, url?: string} = null;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
|
||||
constructor(frameManager: FrameManager, navigatedFrame: frames.Frame) {
|
||||
this._frameManager = frameManager;
|
||||
this._navigatedFrame = navigatedFrame;
|
||||
this._promise = new Promise(x => this._resolveCallback = x);
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(frameManager._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)),
|
||||
helper.addEventListener(frameManager._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
promise() {
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
navigation() {
|
||||
return this._navigation;
|
||||
}
|
||||
|
||||
_onNavigationStarted(params) {
|
||||
if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId) {
|
||||
this._navigation = {
|
||||
navigationId: params.navigationId,
|
||||
url: params.url,
|
||||
};
|
||||
this._resolveCallback();
|
||||
}
|
||||
}
|
||||
|
||||
_onSameDocumentNavigation(params) {
|
||||
if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId) {
|
||||
this._navigation = {
|
||||
navigationId: null,
|
||||
};
|
||||
this._resolveCallback();
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
export class NavigationWatchdog {
|
||||
private _frameManager: FrameManager;
|
||||
private _navigatedFrame: frames.Frame;
|
||||
private _targetNavigationId: any;
|
||||
private _firedEvents: frames.LifecycleEvent[];
|
||||
private _targetURL: any;
|
||||
private _promise: Promise<unknown>;
|
||||
private _resolveCallback: (value?: unknown) => void;
|
||||
private _navigationRequest: network.Request | null;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
|
||||
constructor(frameManager: FrameManager, navigatedFrame: frames.Frame, networkManager: NetworkManager, targetNavigationId, targetURL, firedEvents: frames.LifecycleEvent[]) {
|
||||
this._frameManager = frameManager;
|
||||
this._navigatedFrame = navigatedFrame;
|
||||
this._targetNavigationId = targetNavigationId;
|
||||
this._firedEvents = firedEvents;
|
||||
this._targetURL = targetURL;
|
||||
|
||||
this._promise = new Promise(x => this._resolveCallback = x);
|
||||
this._navigationRequest = null;
|
||||
|
||||
const check = this._checkNavigationComplete.bind(this);
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(frameManager._session, JugglerSessionEvents.Disconnected, () => this._resolveCallback(new Error('Navigation failed because browser has disconnected!'))),
|
||||
helper.addEventListener(frameManager._session, 'Page.eventFired', check),
|
||||
helper.addEventListener(frameManager._session, 'Page.frameAttached', check),
|
||||
helper.addEventListener(frameManager._session, 'Page.frameDetached', check),
|
||||
helper.addEventListener(frameManager._session, 'Page.navigationStarted', check),
|
||||
helper.addEventListener(frameManager._session, 'Page.navigationCommitted', check),
|
||||
helper.addEventListener(frameManager._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)),
|
||||
helper.addEventListener(networkManager, NetworkManagerEvents.Request, this._onRequest.bind(this)),
|
||||
helper.addEventListener(frameManager, FrameManagerEvents.FrameDetached, check),
|
||||
];
|
||||
check();
|
||||
}
|
||||
|
||||
_onRequest(request) {
|
||||
if (request.frame() !== this._navigatedFrame || !request.isNavigationRequest())
|
||||
return;
|
||||
this._navigationRequest = request;
|
||||
}
|
||||
|
||||
navigationResponse(): network.Response | null {
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
_checkNavigationComplete() {
|
||||
const checkFiredEvents = (frame: frames.Frame, firedEvents: frames.LifecycleEvent[]) => {
|
||||
for (const subframe of frame.childFrames()) {
|
||||
if (!checkFiredEvents(subframe, firedEvents))
|
||||
return false;
|
||||
}
|
||||
return firedEvents.every(event => frame._firedLifecycleEvents.has(event));
|
||||
};
|
||||
|
||||
if (this._navigatedFrame.isDetached())
|
||||
this._resolveCallback(new Error('Navigating frame was detached'));
|
||||
else if (this._frameManager._frameData(this._navigatedFrame).lastCommittedNavigationId === this._targetNavigationId
|
||||
&& checkFiredEvents(this._navigatedFrame, this._firedEvents))
|
||||
this._resolveCallback(null);
|
||||
}
|
||||
|
||||
_onNavigationAborted(params) {
|
||||
if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId && params.navigationId === this._targetNavigationId)
|
||||
this._resolveCallback(new Error('Navigation to ' + this._targetURL + ' failed: ' + params.errorText));
|
||||
}
|
||||
|
||||
promise() {
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
180
src/frames.ts
180
src/frames.ts
|
|
@ -20,10 +20,12 @@ import * as fs from 'fs';
|
|||
import * as js from './javascript';
|
||||
import * as dom from './dom';
|
||||
import * as network from './network';
|
||||
import { helper, assert } from './helper';
|
||||
import { helper, assert, RegisteredListener } from './helper';
|
||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
|
||||
import { TimeoutSettings } from './TimeoutSettings';
|
||||
import { TimeoutError } from './Errors';
|
||||
import { Events } from './events';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const readFileAsync = helper.promisify(fs.readFile);
|
||||
|
||||
|
|
@ -50,12 +52,19 @@ export interface FrameDelegate {
|
|||
setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise<void>;
|
||||
}
|
||||
|
||||
interface Page extends EventEmitter {
|
||||
_lifecycleWatchers: Set<LifecycleWatcher>;
|
||||
_timeoutSettings: TimeoutSettings;
|
||||
_disconnectedPromise: Promise<Error>;
|
||||
}
|
||||
|
||||
export type LifecycleEvent = 'load' | 'domcontentloaded';
|
||||
|
||||
export class Frame {
|
||||
readonly _delegate: FrameDelegate;
|
||||
readonly _firedLifecycleEvents: Set<LifecycleEvent>;
|
||||
private _timeoutSettings: TimeoutSettings;
|
||||
_lastDocumentId: string;
|
||||
readonly _page: Page;
|
||||
private _parentFrame: Frame;
|
||||
private _url = '';
|
||||
private _detached = false;
|
||||
|
|
@ -63,10 +72,11 @@ export class Frame {
|
|||
private _childFrames = new Set<Frame>();
|
||||
private _name: string;
|
||||
|
||||
constructor(delegate: FrameDelegate, timeoutSettings: TimeoutSettings, parentFrame: Frame | null) {
|
||||
constructor(delegate: FrameDelegate, page: Page, parentFrame: Frame | null) {
|
||||
this._delegate = delegate;
|
||||
this._firedLifecycleEvents = new Set();
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
this._lastDocumentId = '';
|
||||
this._page = page;
|
||||
this._parentFrame = parentFrame;
|
||||
|
||||
this._worlds.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
|
||||
|
|
@ -390,7 +400,7 @@ export class Frame {
|
|||
}
|
||||
|
||||
async waitForSelector(selector: string | types.Selector, options: types.TimeoutOptions = {}): Promise<dom.ElementHandle | null> {
|
||||
const { timeout = this._timeoutSettings.timeout() } = options;
|
||||
const { timeout = this._page._timeoutSettings.timeout() } = options;
|
||||
const task = dom.waitForSelectorTask(types.clearSelector(selector), timeout);
|
||||
const handle = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${types.selectorToString(selector)}"`);
|
||||
if (!handle.asElement()) {
|
||||
|
|
@ -410,7 +420,7 @@ export class Frame {
|
|||
}
|
||||
|
||||
waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions = {}, ...args: any[]): Promise<js.JSHandle> {
|
||||
options = { timeout: this._timeoutSettings.timeout(), ...options };
|
||||
options = { timeout: this._page._timeoutSettings.timeout(), ...options };
|
||||
const task = dom.waitForFunctionTask(pageFunction, options, ...args);
|
||||
return this._scheduleRerunnableTask(task, 'main', options.timeout);
|
||||
}
|
||||
|
|
@ -420,12 +430,36 @@ export class Frame {
|
|||
return context.evaluate(() => document.title);
|
||||
}
|
||||
|
||||
_navigated(url: string, name: string) {
|
||||
this._url = url;
|
||||
this._name = name;
|
||||
_onExpectedNewDocumentNavigation(documentId: string, url?: string) {
|
||||
for (const watcher of this._page._lifecycleWatchers)
|
||||
watcher._onExpectedNewDocumentNavigation(this, documentId, url);
|
||||
}
|
||||
|
||||
_detach() {
|
||||
_onAbortedNewDocumentNavigation(documentId: string, errorText: string) {
|
||||
for (const watcher of this._page._lifecycleWatchers)
|
||||
watcher._onAbortedNewDocumentNavigation(this, documentId, errorText);
|
||||
}
|
||||
|
||||
_onCommittedNewDocumentNavigation(url: string, name: string, documentId: string) {
|
||||
this._url = url;
|
||||
this._name = name;
|
||||
this._lastDocumentId = documentId;
|
||||
this._firedLifecycleEvents.clear();
|
||||
}
|
||||
|
||||
_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 world of this._worlds.values()) {
|
||||
for (const rerunnableTask of world.rerunnableTasks)
|
||||
|
|
@ -434,6 +468,8 @@ 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, worldType: WorldType, timeout?: number, title?: string): Promise<js.JSHandle> {
|
||||
|
|
@ -557,3 +593,127 @@ class RerunnableTask {
|
|||
this._world.rerunnableTasks.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class LifecycleWatcher {
|
||||
readonly sameDocumentNavigationPromise: Promise<Error | null>;
|
||||
readonly lifecyclePromise: Promise<void>;
|
||||
readonly newDocumentNavigationPromise: Promise<Error | null>;
|
||||
readonly timeoutOrTerminationPromise: Promise<Error | null>;
|
||||
private _expectedLifecycle: LifecycleEvent[];
|
||||
private _frame: Frame;
|
||||
private _navigationRequest: network.Request | null = null;
|
||||
private _sameDocumentNavigationCompleteCallback: () => void;
|
||||
private _lifecycleCallback: () => void;
|
||||
private _newDocumentNavigationCompleteCallback: () => void;
|
||||
private _frameDetachedCallback: (err: Error) => void;
|
||||
private _navigationAbortedCallback: (err: Error) => void;
|
||||
private _maximumTimer: NodeJS.Timer;
|
||||
private _hasSameDocumentNavigation: boolean;
|
||||
private _listeners: RegisteredListener[];
|
||||
private _targetUrl?: string;
|
||||
private _expectedDocumentId?: string;
|
||||
|
||||
constructor(frame: Frame, waitUntil: LifecycleEvent | LifecycleEvent[], timeout: number) {
|
||||
if (Array.isArray(waitUntil))
|
||||
waitUntil = waitUntil.slice();
|
||||
else if (typeof waitUntil === 'string')
|
||||
waitUntil = [waitUntil];
|
||||
if (waitUntil.some(e => e !== 'load' && e !== 'domcontentloaded'))
|
||||
throw new Error('Unsupported waitUntil option');
|
||||
this._expectedLifecycle = waitUntil.slice();
|
||||
this._frame = frame;
|
||||
this.sameDocumentNavigationPromise = new Promise(f => this._sameDocumentNavigationCompleteCallback = f);
|
||||
this.lifecyclePromise = new Promise(f => this._lifecycleCallback = f);
|
||||
this.newDocumentNavigationPromise = new Promise(f => this._newDocumentNavigationCompleteCallback = f);
|
||||
this.timeoutOrTerminationPromise = Promise.race([
|
||||
this._createTimeoutPromise(timeout),
|
||||
new Promise<Error>(f => this._frameDetachedCallback = f),
|
||||
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);
|
||||
this._listeners = [
|
||||
helper.addEventListener(this._frame._page, Events.Page.Request, (request: network.Request) => {
|
||||
if (request.frame() === this._frame && request.isNavigationRequest())
|
||||
this._navigationRequest = request;
|
||||
}),
|
||||
];
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_onFrameDetached(frame: Frame) {
|
||||
if (this._frame === frame) {
|
||||
this._frameDetachedCallback.call(null, new Error('Navigating frame was detached'));
|
||||
return;
|
||||
}
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_onNavigatedWithinDocument(frame: Frame) {
|
||||
if (frame !== this._frame)
|
||||
return;
|
||||
this._hasSameDocumentNavigation = true;
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_onExpectedNewDocumentNavigation(frame: Frame, documentId: string, url?: string) {
|
||||
if (frame === this._frame && this._expectedDocumentId === undefined) {
|
||||
this._expectedDocumentId = documentId;
|
||||
this._targetUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
_onAbortedNewDocumentNavigation(frame: Frame, documentId: string, errorText: string) {
|
||||
if (frame === this._frame && documentId === this._expectedDocumentId) {
|
||||
if (this._targetUrl)
|
||||
this._navigationAbortedCallback(new Error('Navigation to ' + this._targetUrl + ' failed: ' + errorText));
|
||||
else
|
||||
this._navigationAbortedCallback(new Error('Navigation failed: ' + errorText));
|
||||
}
|
||||
}
|
||||
|
||||
_onLifecycleEvent(frame: Frame) {
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
navigationResponse(): network.Response | null {
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
private _createTimeoutPromise(timeout: number): Promise<Error | null> {
|
||||
if (!timeout)
|
||||
return new Promise(() => {});
|
||||
const errorMessage = 'Navigation timeout of ' + timeout + ' ms exceeded';
|
||||
return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, timeout))
|
||||
.then(() => new TimeoutError(errorMessage));
|
||||
}
|
||||
|
||||
private _checkLifecycleRecursively(frame: Frame, expectedLifecycle: LifecycleEvent[]): boolean {
|
||||
for (const event of expectedLifecycle) {
|
||||
if (!frame._firedLifecycleEvents.has(event))
|
||||
return false;
|
||||
}
|
||||
for (const child of frame.childFrames()) {
|
||||
if (!this._checkLifecycleRecursively(child, expectedLifecycle))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _checkLifecycleComplete() {
|
||||
// We expect navigation to commit.
|
||||
if (!this._checkLifecycleRecursively(this._frame, this._expectedLifecycle))
|
||||
return;
|
||||
this._lifecycleCallback();
|
||||
if (this._hasSameDocumentNavigation)
|
||||
this._sameDocumentNavigationCompleteCallback();
|
||||
if (this._frame._lastDocumentId === this._expectedDocumentId)
|
||||
this._newDocumentNavigationCompleteCallback();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._frame._page._lifecycleWatchers.delete(this);
|
||||
helper.removeEventListeners(this._listeners);
|
||||
clearTimeout(this._maximumTimer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export class Page<Browser> extends EventEmitter {
|
|||
private _pageBindings = new Map<string, Function>();
|
||||
readonly _screenshotter: Screenshotter;
|
||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||
readonly _lifecycleWatchers = new Set<frames.LifecycleWatcher>();
|
||||
|
||||
constructor(delegate: PageDelegate, browserContext: BrowserContext<Browser>) {
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
|
||||
import * as EventEmitter from 'events';
|
||||
import { TimeoutError } from '../Errors';
|
||||
import * as frames from '../frames';
|
||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||
import * as js from '../javascript';
|
||||
|
|
@ -51,9 +50,10 @@ export const FrameManagerEvents = {
|
|||
const frameDataSymbol = Symbol('frameData');
|
||||
type FrameData = {
|
||||
id: string,
|
||||
loaderId: string,
|
||||
};
|
||||
|
||||
let lastDocumentId = 0;
|
||||
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
|
||||
readonly rawMouse: RawMouseImpl;
|
||||
readonly rawKeyboard: RawKeyboardImpl;
|
||||
|
|
@ -132,7 +132,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
|
||||
_addSessionListeners() {
|
||||
this._sessionListeners = [
|
||||
helper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame)),
|
||||
helper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)),
|
||||
helper.addEventListener(this._session, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)),
|
||||
helper.addEventListener(this._session, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)),
|
||||
helper.addEventListener(this._session, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)),
|
||||
|
|
@ -163,8 +163,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
return;
|
||||
const hasDOMContentLoaded = frame._firedLifecycleEvents.has('domcontentloaded');
|
||||
const hasLoad = frame._firedLifecycleEvents.has('load');
|
||||
frame._firedLifecycleEvents.add('domcontentloaded');
|
||||
frame._firedLifecycleEvents.add('load');
|
||||
frame._lifecycleEvent('domcontentloaded');
|
||||
frame._lifecycleEvent('load');
|
||||
this.emit(FrameManagerEvents.LifecycleEvent, frame);
|
||||
if (frame === this.mainFrame() && !hasDOMContentLoaded)
|
||||
this._page.emit(Events.Page.DOMContentLoaded);
|
||||
|
|
@ -176,7 +176,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
const frame = this._frames.get(frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._firedLifecycleEvents.add(event);
|
||||
frame._lifecycleEvent(event);
|
||||
this.emit(FrameManagerEvents.LifecycleEvent, frame);
|
||||
if (frame === this.mainFrame()) {
|
||||
if (event === 'load')
|
||||
|
|
@ -189,7 +189,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
_handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) {
|
||||
if (frameTree.frame.parentId)
|
||||
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId);
|
||||
this._onFrameNavigated(frameTree.frame);
|
||||
this._onFrameNavigated(frameTree.frame, true);
|
||||
if (!frameTree.childFrames)
|
||||
return;
|
||||
|
||||
|
|
@ -222,10 +222,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
return;
|
||||
assert(parentFrameId);
|
||||
const parentFrame = this._frames.get(parentFrameId);
|
||||
const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame);
|
||||
const frame = new frames.Frame(this, this._page, parentFrame);
|
||||
const data: FrameData = {
|
||||
id: frameId,
|
||||
loaderId: '',
|
||||
};
|
||||
frame[frameDataSymbol] = data;
|
||||
this._frames.set(frameId, frame);
|
||||
|
|
@ -234,7 +233,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
return frame;
|
||||
}
|
||||
|
||||
_onFrameNavigated(framePayload: Protocol.Page.Frame) {
|
||||
_onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
|
||||
const isMainFrame = !framePayload.parentId;
|
||||
let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id);
|
||||
|
||||
|
|
@ -251,10 +250,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
}
|
||||
} else if (isMainFrame) {
|
||||
// Initial frame navigation.
|
||||
frame = new frames.Frame(this, this._page._timeoutSettings, null);
|
||||
frame = new frames.Frame(this, this._page, null);
|
||||
const data: FrameData = {
|
||||
id: framePayload.id,
|
||||
loaderId: framePayload.loaderId,
|
||||
};
|
||||
frame[frameDataSymbol] = data;
|
||||
this._frames.set(framePayload.id, frame);
|
||||
|
|
@ -266,12 +264,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
if (isMainFrame)
|
||||
this._mainFrame = frame;
|
||||
|
||||
// Update frame payload.
|
||||
frame._navigated(framePayload.url, framePayload.name);
|
||||
frame._firedLifecycleEvents.clear();
|
||||
const data = this._frameData(frame);
|
||||
data.loaderId = framePayload.loaderId;
|
||||
|
||||
for (const context of this._contextIdToContext.values()) {
|
||||
if (context.frame() === frame) {
|
||||
const delegate = context._delegate as ExecutionContextDelegate;
|
||||
|
|
@ -281,6 +273,12 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
}
|
||||
}
|
||||
|
||||
// Auto-increment to avoid cross-process loaderId clash.
|
||||
const documentId = framePayload.loaderId + '::' + (++lastDocumentId);
|
||||
if (!initial)
|
||||
frame._onExpectedNewDocumentNavigation(documentId);
|
||||
frame._onCommittedNewDocumentNavigation(framePayload.url, framePayload.name, documentId);
|
||||
|
||||
this.emit(FrameManagerEvents.FrameNavigated, frame);
|
||||
this._page.emit(Events.Page.FrameNavigated, frame);
|
||||
}
|
||||
|
|
@ -289,7 +287,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
const frame = this._frames.get(frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._navigated(url, frame.name());
|
||||
frame._onCommittedSameDocumentNavigation(url);
|
||||
this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame);
|
||||
this.emit(FrameManagerEvents.FrameNavigated, frame);
|
||||
this._page.emit(Events.Page.FrameNavigated, frame);
|
||||
|
|
@ -330,7 +328,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
_removeFramesRecursively(frame: frames.Frame) {
|
||||
for (const child of frame.childFrames())
|
||||
this._removeFramesRecursively(child);
|
||||
frame._detach();
|
||||
frame._onDetached();
|
||||
this._frames.delete(this._frameData(frame).id);
|
||||
this.emit(FrameManagerEvents.FrameDetached, frame);
|
||||
this._page.emit(Events.Page.FrameDetached, frame);
|
||||
|
|
@ -341,12 +339,12 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = (['load'] as frames.LifecycleEvent[])
|
||||
} = options;
|
||||
const watchDog = new NextNavigationWatchdog(this, frame, waitUntil, timeout);
|
||||
const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
await this._session.send('Page.navigate', {url, frameId: this._frameData(frame).id});
|
||||
const error = await Promise.race([
|
||||
watchDog.timeoutOrTerminationPromise(),
|
||||
watchDog.newDocumentNavigationPromise(),
|
||||
watchDog.sameDocumentNavigationPromise(),
|
||||
watchDog.timeoutOrTerminationPromise,
|
||||
watchDog.newDocumentNavigationPromise,
|
||||
watchDog.sameDocumentNavigationPromise,
|
||||
]);
|
||||
watchDog.dispose();
|
||||
if (error)
|
||||
|
|
@ -359,11 +357,11 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = (['load'] as frames.LifecycleEvent[])
|
||||
} = options;
|
||||
const watchDog = new NextNavigationWatchdog(this, frame, waitUntil, timeout);
|
||||
const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
const error = await Promise.race([
|
||||
watchDog.timeoutOrTerminationPromise(),
|
||||
watchDog.newDocumentNavigationPromise(),
|
||||
watchDog.sameDocumentNavigationPromise(),
|
||||
watchDog.timeoutOrTerminationPromise,
|
||||
watchDog.newDocumentNavigationPromise,
|
||||
watchDog.sameDocumentNavigationPromise,
|
||||
]);
|
||||
watchDog.dispose();
|
||||
if (error)
|
||||
|
|
@ -377,15 +375,15 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = (['load'] as frames.LifecycleEvent[])
|
||||
} = options;
|
||||
const watchDog = new NextNavigationWatchdog(this, frame, waitUntil, timeout);
|
||||
const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout);
|
||||
await frame.evaluate(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
}, html);
|
||||
const error = await Promise.race([
|
||||
watchDog.timeoutOrTerminationPromise(),
|
||||
watchDog.lifecyclePromise(),
|
||||
watchDog.timeoutOrTerminationPromise,
|
||||
watchDog.lifecyclePromise,
|
||||
]);
|
||||
watchDog.dispose();
|
||||
if (error)
|
||||
|
|
@ -529,145 +527,3 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||
this._page.browser()._closePage(this._page);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class NextNavigationWatchdog {
|
||||
private readonly _frameManager: FrameManager;
|
||||
private readonly _frame: frames.Frame;
|
||||
private readonly _newDocumentNavigationPromise: Promise<Error | null>;
|
||||
private _newDocumentNavigationCallback: (value?: unknown) => void;
|
||||
private readonly _sameDocumentNavigationPromise: Promise<Error | null>;
|
||||
private _sameDocumentNavigationCallback: (value?: unknown) => void;
|
||||
private readonly _lifecyclePromise: Promise<void>;
|
||||
private _lifecycleCallback: () => void;
|
||||
private readonly _frameDetachPromise: Promise<Error | null>;
|
||||
private _frameDetachCallback: (err: Error | null) => void;
|
||||
private readonly _initialSession: TargetSession;
|
||||
private _navigationRequest?: network.Request = null;
|
||||
private readonly _eventListeners: RegisteredListener[];
|
||||
private readonly _timeoutPromise: Promise<Error | null>;
|
||||
private readonly _timeoutId: NodeJS.Timer;
|
||||
private _hasSameDocumentNavigation = false;
|
||||
private readonly _expectedLifecycle: frames.LifecycleEvent[];
|
||||
private readonly _initialLoaderId: string;
|
||||
|
||||
constructor(frameManager: FrameManager, frame: frames.Frame, waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[], timeout) {
|
||||
if (Array.isArray(waitUntil))
|
||||
waitUntil = waitUntil.slice();
|
||||
else if (typeof waitUntil === 'string')
|
||||
waitUntil = [waitUntil];
|
||||
this._expectedLifecycle = waitUntil.slice();
|
||||
this._frameManager = frameManager;
|
||||
this._frame = frame;
|
||||
this._initialLoaderId = frameManager._frameData(frame).loaderId;
|
||||
this._initialSession = frameManager._session;
|
||||
this._newDocumentNavigationPromise = new Promise(fulfill => {
|
||||
this._newDocumentNavigationCallback = fulfill;
|
||||
});
|
||||
this._sameDocumentNavigationPromise = new Promise(fulfill => {
|
||||
this._sameDocumentNavigationCallback = fulfill;
|
||||
});
|
||||
this._lifecyclePromise = new Promise(fulfill => {
|
||||
this._lifecycleCallback = fulfill;
|
||||
});
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(frameManager, FrameManagerEvents.LifecycleEvent, frame => this._onLifecycleEvent(frame)),
|
||||
helper.addEventListener(frameManager, FrameManagerEvents.FrameNavigated, frame => this._onLifecycleEvent(frame)),
|
||||
helper.addEventListener(frameManager, FrameManagerEvents.FrameNavigatedWithinDocument, frame => this._onSameDocumentNavigation(frame)),
|
||||
helper.addEventListener(frameManager, FrameManagerEvents.FrameDetached, frame => this._onFrameDetached(frame)),
|
||||
helper.addEventListener(frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)),
|
||||
];
|
||||
const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms');
|
||||
let timeoutCallback;
|
||||
this._timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
this._timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
this._frameDetachPromise = new Promise(fulfill => {
|
||||
this._frameDetachCallback = fulfill;
|
||||
});
|
||||
}
|
||||
|
||||
sameDocumentNavigationPromise(): Promise<Error | null> {
|
||||
return this._sameDocumentNavigationPromise;
|
||||
}
|
||||
|
||||
newDocumentNavigationPromise(): Promise<Error | null> {
|
||||
return this._newDocumentNavigationPromise;
|
||||
}
|
||||
|
||||
lifecyclePromise(): Promise<any> {
|
||||
return this._lifecyclePromise;
|
||||
}
|
||||
|
||||
timeoutOrTerminationPromise(): Promise<Error | null> {
|
||||
return Promise.race([
|
||||
this._timeoutPromise,
|
||||
this._frameDetachPromise,
|
||||
this._frameManager._page._disconnectedPromise
|
||||
]);
|
||||
}
|
||||
|
||||
_onLifecycleEvent(frame: frames.Frame) {
|
||||
this._checkLifecycle();
|
||||
}
|
||||
|
||||
_onSameDocumentNavigation(frame) {
|
||||
if (this._frame === frame)
|
||||
this._hasSameDocumentNavigation = true;
|
||||
this._checkLifecycle();
|
||||
}
|
||||
|
||||
_checkLifecycle() {
|
||||
const checkLifecycle = (frame: frames.Frame, expectedLifecycle: frames.LifecycleEvent[]): boolean => {
|
||||
for (const event of expectedLifecycle) {
|
||||
if (!frame._firedLifecycleEvents.has(event))
|
||||
return false;
|
||||
}
|
||||
for (const child of frame.childFrames()) {
|
||||
if (!checkLifecycle(child, expectedLifecycle))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (this._frame.isDetached()) {
|
||||
this._newDocumentNavigationCallback(new Error('Navigating frame was detached'));
|
||||
this._sameDocumentNavigationCallback(new Error('Navigating frame was detached'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkLifecycle(this._frame, this._expectedLifecycle))
|
||||
return;
|
||||
this._lifecycleCallback();
|
||||
if (this._hasSameDocumentNavigation)
|
||||
this._sameDocumentNavigationCallback();
|
||||
if (this._frameManager._frameData(this._frame).loaderId !== this._initialLoaderId ||
|
||||
this._initialSession !== this._frameManager._session)
|
||||
this._newDocumentNavigationCallback();
|
||||
}
|
||||
|
||||
_onFrameDetached(frame: frames.Frame) {
|
||||
if (this._frame === frame) {
|
||||
this._frameDetachCallback.call(null, new Error('Navigating frame was detached'));
|
||||
return;
|
||||
}
|
||||
this._checkLifecycle();
|
||||
}
|
||||
|
||||
_onRequest(request: network.Request) {
|
||||
if (request.frame() !== this._frame || !request.isNavigationRequest())
|
||||
return;
|
||||
this._navigationRequest = request;
|
||||
}
|
||||
|
||||
navigationResponse(): network.Response | null {
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// TODO: handle exceptions
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
clearTimeout(this._timeoutId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function dimensions() {
|
|||
};
|
||||
}
|
||||
|
||||
module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||
module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT, MAC}) {
|
||||
const {describe, xdescribe, fdescribe} = testRunner;
|
||||
const {it, fit, xit} = testRunner;
|
||||
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
|||
server.setRoute('/empty.html', (req, res) => { });
|
||||
let error = null;
|
||||
await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(e => error = e);
|
||||
const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded';
|
||||
const message = 'Navigation timeout of 1 ms exceeded';
|
||||
expect(error.message).toContain(message);
|
||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||
});
|
||||
|
|
@ -151,7 +151,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
|||
let error = null;
|
||||
page.setDefaultNavigationTimeout(1);
|
||||
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
||||
const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded';
|
||||
const message = 'Navigation timeout of 1 ms exceeded';
|
||||
expect(error.message).toContain(message);
|
||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||
});
|
||||
|
|
@ -161,7 +161,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
|||
let error = null;
|
||||
page.setDefaultTimeout(1);
|
||||
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
||||
const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded';
|
||||
const message = 'Navigation timeout of 1 ms exceeded';
|
||||
expect(error.message).toContain(message);
|
||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||
});
|
||||
|
|
@ -172,7 +172,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
|||
page.setDefaultTimeout(0);
|
||||
page.setDefaultNavigationTimeout(1);
|
||||
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
||||
const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded';
|
||||
const message = 'Navigation timeout of 1 ms exceeded';
|
||||
expect(error.message).toContain(message);
|
||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
|
|||
});
|
||||
});
|
||||
|
||||
describe('Page.Events.Popup', function() {
|
||||
describe.skip(WEBKIT)('Page.Events.Popup', function() {
|
||||
it('should work', async({page}) => {
|
||||
const [popup] = await Promise.all([
|
||||
new Promise(x => page.once('popup', x)),
|
||||
|
|
|
|||
Loading…
Reference in a new issue