chore: reuse LifecycleWatcher between browsers (#208)

This commit is contained in:
Dmitry Gozman 2019-12-11 07:17:32 -08:00 committed by Pavel Feldman
parent e42e361d20
commit 57acdfd860
10 changed files with 272 additions and 610 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)),