chore: unify frame lifecycle events between browsers (#172)

This commit is contained in:
Dmitry Gozman 2019-12-09 16:34:42 -08:00 committed by GitHub
parent e5a85e4e67
commit b4c89ca0f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 243 additions and 107 deletions

View File

@ -10,7 +10,7 @@
"playwright": {
"chromium_revision": "719491",
"firefox_revision": "1004",
"webkit_revision": "1016"
"webkit_revision": "1022"
},
"scripts": {
"unit": "node test/test.js",

View File

@ -60,7 +60,6 @@ const frameDataSymbol = Symbol('frameData');
type FrameData = {
id: string,
loaderId: string,
lifecycleEvents: Set<string>,
};
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
@ -142,14 +141,11 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
return (frame as any)[frameDataSymbol];
}
async navigateFrame(
frame: frames.Frame,
url: string,
options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise<network.Response | null> {
assertNoLegacyNavigationOptions(options);
const {
referer = this._networkManager.extraHTTPHeaders()['referer'],
waitUntil = ['load'],
waitUntil = (['load'] as frames.LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
@ -181,13 +177,10 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
}
}
async waitForFrameNavigation(
frame: frames.Frame,
options: { timeout?: number; waitUntil?: string | string[]; } = {}
): Promise<network.Response | null> {
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
assertNoLegacyNavigationOptions(options);
const {
waitUntil = ['load'],
waitUntil = (['load'] as frames.LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
@ -204,7 +197,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) {
const {
waitUntil = ['load'],
waitUntil = (['load'] as frames.LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const context = await frame._utilityContext();
@ -232,9 +225,12 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
const data = this._frameData(frame);
if (event.name === 'init') {
data.loaderId = event.loaderId;
data.lifecycleEvents.clear();
frame._firedLifecycleEvents.clear();
}
data.lifecycleEvents.add(event.name);
if (event.name === 'load')
frame._firedLifecycleEvents.add('load');
else if (event.name === 'DOMContentLoaded')
frame._firedLifecycleEvents.add('domcontentloaded');
this.emit(FrameManagerEvents.LifecycleEvent, frame);
}
@ -242,9 +238,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
const frame = this._frames.get(frameId);
if (!frame)
return;
const data = this._frameData(frame);
data.lifecycleEvents.add('DOMContentLoaded');
data.lifecycleEvents.add('load');
frame._firedLifecycleEvents.add('domcontentloaded');
frame._firedLifecycleEvents.add('load');
this.emit(FrameManagerEvents.LifecycleEvent, frame);
}
@ -284,7 +279,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
const data: FrameData = {
id: frameId,
loaderId: '',
lifecycleEvents: new Set(),
};
frame[frameDataSymbol] = data;
this._frames.set(frameId, frame);
@ -316,7 +310,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
const data: FrameData = {
id: framePayload.id,
loaderId: '',
lifecycleEvents: new Set(),
};
frame[frameDataSymbol] = data;
}

View File

@ -24,7 +24,7 @@ import * as frames from '../frames';
import * as network from '../network';
export class LifecycleWatcher {
private _expectedLifecycle: string[];
private _expectedLifecycle: frames.LifecycleEvent[];
private _frameManager: FrameManager;
private _frame: frames.Frame;
private _initialLoaderId: string;
@ -43,17 +43,12 @@ export class LifecycleWatcher {
private _maximumTimer: NodeJS.Timer;
private _hasSameDocumentNavigation: boolean;
constructor(frameManager: FrameManager, frame: frames.Frame, waitUntil: string | string[], timeout: number) {
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.map(value => {
const protocolEvent = playwrightToProtocolLifecycle.get(value);
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
return protocolEvent;
});
this._expectedLifecycle = waitUntil.slice();
this._frameManager = frameManager;
this._frame = frame;
this._initialLoaderId = frameManager._frameData(frame).loaderId;
@ -139,9 +134,9 @@ export class LifecycleWatcher {
}
_checkLifecycleComplete() {
const checkLifecycle = (frame: frames.Frame, expectedLifecycle: string[]): boolean => {
const checkLifecycle = (frame: frames.Frame, expectedLifecycle: frames.LifecycleEvent[]): boolean => {
for (const event of expectedLifecycle) {
if (!this._frameManager._frameData(frame).lifecycleEvents.has(event))
if (!frame._firedLifecycleEvents.has(event))
return false;
}
for (const child of frame.childFrames()) {
@ -168,10 +163,3 @@ export class LifecycleWatcher {
clearTimeout(this._maximumTimer);
}
}
const playwrightToProtocolLifecycle = new Map([
['load', 'load'],
['domcontentloaded', 'DOMContentLoaded'],
['networkidle0', 'networkIdle'],
['networkidle2', 'networkAlmostIdle'],
]);

View File

@ -52,7 +52,6 @@ const frameDataSymbol = Symbol('frameData');
type FrameData = {
frameId: string,
lastCommittedNavigationId: string,
firedEvents: Set<string>,
};
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
@ -163,7 +162,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
frame._navigated(params.url, params.name);
const data = this._frameData(frame);
data.lastCommittedNavigationId = params.navigationId;
data.firedEvents.clear();
frame._firedLifecycleEvents.clear();
this.emit(FrameManagerEvents.FrameNavigated, frame);
this._page.emit(CommonEvents.Page.FrameNavigated, frame);
}
@ -181,7 +180,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
const data: FrameData = {
frameId: params.frameId,
lastCommittedNavigationId: '',
firedEvents: new Set(),
};
frame[frameDataSymbol] = data;
if (!parentFrame) {
@ -203,12 +201,16 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
_onEventFired({frameId, name}) {
const frame = this._frames.get(frameId);
this._frameData(frame).firedEvents.add(name.toLowerCase());
if (frame === this._mainFrame) {
if (name === 'load') {
if (name === 'load') {
frame._firedLifecycleEvents.add('load');
if (frame === this._mainFrame) {
this.emit(FrameManagerEvents.Load);
this._page.emit(CommonEvents.Page.Load);
} else if (name === 'DOMContentLoaded') {
}
}
if (name === 'DOMContentLoaded') {
frame._firedLifecycleEvents.add('domcontentloaded');
if (frame === this._mainFrame) {
this.emit(FrameManagerEvents.DOMContentLoaded);
this._page.emit(CommonEvents.Page.DOMContentLoaded);
}
@ -261,7 +263,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}) {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = ['load'],
waitUntil = (['load'] as frames.LifecycleEvent[]),
} = options;
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
@ -306,7 +308,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}) {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = ['load'],
waitUntil = (['load'] as frames.LifecycleEvent[]),
referer,
} = options;
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
@ -388,7 +390,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
private async _go(action: () => Promise<{ navigationId: string | null, navigationURL: string | null }>, options: frames.NavigateOptions = {}) {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = ['load'],
waitUntil = (['load'] as frames.LifecycleEvent[]),
} = options;
const frame = this.mainFrame();
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
@ -434,7 +436,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
}
}
export function normalizeWaitUntil(waitUntil) {
export function normalizeWaitUntil(waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[]): frames.LifecycleEvent[] {
if (!Array.isArray(waitUntil))
waitUntil = [waitUntil];
for (const condition of waitUntil) {

View File

@ -20,6 +20,7 @@ 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;
@ -75,14 +76,14 @@ export class NavigationWatchdog {
private _frameManager: FrameManager;
private _navigatedFrame: frames.Frame;
private _targetNavigationId: any;
private _firedEvents: any;
private _firedEvents: frames.LifecycleEvent[];
private _targetURL: any;
private _promise: Promise<unknown>;
private _resolveCallback: (value?: unknown) => void;
private _navigationRequest: any;
private _navigationRequest: network.Request | null;
private _eventListeners: RegisteredListener[];
constructor(frameManager: FrameManager, navigatedFrame: frames.Frame, networkManager: NetworkManager, targetNavigationId, targetURL, firedEvents) {
constructor(frameManager: FrameManager, navigatedFrame: frames.Frame, networkManager: NetworkManager, targetNavigationId, targetURL, firedEvents: frames.LifecycleEvent[]) {
this._frameManager = frameManager;
this._navigatedFrame = navigatedFrame;
this._targetNavigationId = targetNavigationId;
@ -113,17 +114,17 @@ export class NavigationWatchdog {
this._navigationRequest = request;
}
navigationResponse() {
navigationResponse(): network.Response | null {
return this._navigationRequest ? this._navigationRequest.response() : null;
}
_checkNavigationComplete() {
const checkFiredEvents = (frame: frames.Frame, firedEvents) => {
const checkFiredEvents = (frame: frames.Frame, firedEvents: frames.LifecycleEvent[]) => {
for (const subframe of frame.childFrames()) {
if (!checkFiredEvents(subframe, firedEvents))
return false;
}
return firedEvents.every(event => this._frameManager._frameData(frame).firedEvents.has(event));
return firedEvents.every(event => frame._firedLifecycleEvents.has(event));
};
if (this._navigatedFrame.isDetached())

View File

@ -37,7 +37,7 @@ type World = {
export type NavigateOptions = {
timeout?: number,
waitUntil?: string | string[],
waitUntil?: LifecycleEvent | LifecycleEvent[],
};
export type GotoOptions = NavigateOptions & {
@ -50,8 +50,11 @@ export interface FrameDelegate {
setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise<void>;
}
export type LifecycleEvent = 'load' | 'domcontentloaded';
export class Frame {
_delegate: FrameDelegate;
readonly _delegate: FrameDelegate;
readonly _firedLifecycleEvents: Set<LifecycleEvent>;
private _timeoutSettings: TimeoutSettings;
private _parentFrame: Frame;
private _url = '';
@ -62,6 +65,7 @@ export class Frame {
constructor(delegate: FrameDelegate, timeoutSettings: TimeoutSettings, parentFrame: Frame | null) {
this._delegate = delegate;
this._firedLifecycleEvents = new Set();
this._timeoutSettings = timeoutSettings;
this._parentFrame = parentFrame;

View File

@ -22,7 +22,7 @@ import { assert, debugError, helper, RegisteredListener } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import * as network from '../network';
import { TargetSession } from './Connection';
import { TargetSession, TargetSessionEvents } from './Connection';
import { Events } from './events';
import { Events as CommonEvents } from '../events';
import { ExecutionContextDelegate } from './ExecutionContext';
@ -43,11 +43,13 @@ export const FrameManagerEvents = {
FrameAttached: Symbol('FrameAttached'),
FrameDetached: Symbol('FrameDetached'),
FrameNavigated: Symbol('FrameNavigated'),
LifecycleEvent: Symbol('LifecycleEvent'),
};
const frameDataSymbol = Symbol('frameData');
type FrameData = {
id: string,
loaderId: string,
};
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
@ -121,6 +123,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
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)),
helper.addEventListener(this._session, 'Page.loadEventFired', event => this._onLifecycleEvent(event.frameId, 'load')),
helper.addEventListener(this._session, 'Page.domContentEventFired', event => this._onLifecycleEvent(event.frameId, 'domcontentloaded')),
helper.addEventListener(this._session, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)),
helper.addEventListener(this._session, 'Page.loadEventFired', event => this._page.emit(Events.Page.Load)),
helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)),
@ -146,6 +150,29 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
const frame = this._frames.get(frameId);
if (!frame)
return;
const hasDOMContentLoaded = frame._firedLifecycleEvents.has('domcontentloaded');
const hasLoad = frame._firedLifecycleEvents.has('load');
frame._firedLifecycleEvents.add('domcontentloaded');
frame._firedLifecycleEvents.add('load');
this.emit(FrameManagerEvents.LifecycleEvent, frame);
if (frame === this.mainFrame() && !hasDOMContentLoaded)
this._page.emit(CommonEvents.Page.DOMContentLoaded);
if (frame === this.mainFrame() && !hasLoad)
this._page.emit(CommonEvents.Page.Load);
}
_onLifecycleEvent(frameId: string, event: frames.LifecycleEvent) {
const frame = this._frames.get(frameId);
if (!frame)
return;
frame._firedLifecycleEvents.add(event);
this.emit(FrameManagerEvents.LifecycleEvent, frame);
if (frame === this.mainFrame()) {
if (event === 'load')
this._page.emit(CommonEvents.Page.Load);
if (event === 'domcontentloaded')
this._page.emit(CommonEvents.Page.DOMContentLoaded);
}
}
_handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) {
@ -187,6 +214,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame);
const data: FrameData = {
id: frameId,
loaderId: '',
};
frame[frameDataSymbol] = data;
this._frames.set(frameId, frame);
@ -215,6 +243,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
frame = new frames.Frame(this, this._page._timeoutSettings, null);
const data: FrameData = {
id: framePayload.id,
loaderId: framePayload.loaderId,
};
frame[frameDataSymbol] = data;
this._frames.set(framePayload.id, frame);
@ -228,6 +257,10 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
// 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;
@ -292,30 +325,60 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
this._page.emit(CommonEvents.Page.FrameDetached, frame);
}
async navigateFrame(frame: frames.Frame, url: string, options: { referer?: string; timeout?: number; waitUntil?: string | Array<string>; } | undefined = {}): Promise<network.Response | null> {
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise<network.Response | null> {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[])
} = options;
const watchDog = new NextNavigationWatchdog(this, frame, timeout);
await this._session.send('Page.navigate', {url});
return watchDog.waitForNavigation();
const watchDog = new NextNavigationWatchdog(this, 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.dispose();
if (error)
throw error;
return watchDog.navigationResponse();
}
async waitForFrameNavigation(frame: frames.Frame, options?: frames.NavigateOptions): Promise<network.Response | null> {
// FIXME: this method only works for main frames.
const watchDog = new NextNavigationWatchdog(this, frame, 10000);
return watchDog.waitForNavigation();
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[])
} = options;
const watchDog = new NextNavigationWatchdog(this, frame, waitUntil, timeout);
const error = await Promise.race([
watchDog.timeoutOrTerminationPromise(),
watchDog.newDocumentNavigationPromise(),
watchDog.sameDocumentNavigationPromise(),
]);
watchDog.dispose();
if (error)
throw error;
return watchDog.navigationResponse();
}
async setFrameContent(frame: frames.Frame, html: string, options: { timeout?: number; waitUntil?: string | Array<string>; } | undefined = {}) {
async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) {
// We rely upon the fact that document.open() will trigger Page.loadEventFired.
const watchDog = new NextNavigationWatchdog(this, frame, 1000);
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[])
} = options;
const watchDog = new NextNavigationWatchdog(this, frame, waitUntil, timeout);
await frame.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
await watchDog.waitForNavigation();
const error = await Promise.race([
watchDog.timeoutOrTerminationPromise(),
watchDog.lifecyclePromise(),
]);
watchDog.dispose();
if (error)
throw error;
}
async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
@ -441,52 +504,90 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
class NextNavigationWatchdog {
_frameManager: FrameManager;
_frame: frames.Frame;
_newDocumentNavigationPromise: Promise<unknown>;
_newDocumentNavigationPromise: Promise<Error | null>;
_newDocumentNavigationCallback: (value?: unknown) => void;
_sameDocumentNavigationPromise: Promise<unknown>;
_sameDocumentNavigationPromise: Promise<Error | null>;
_sameDocumentNavigationCallback: (value?: unknown) => void;
private _lifecyclePromise: Promise<void>;
private _lifecycleCallback: () => void;
private _terminationPromise: Promise<Error | null>;
private _terminationCallback: (err: Error | null) => void;
_navigationRequest: any;
_eventListeners: RegisteredListener[];
_timeoutPromise: Promise<unknown>;
_timeoutPromise: Promise<Error | null>;
_timeoutId: NodeJS.Timer;
_hasSameDocumentNavigation = false;
_expectedLifecycle: frames.LifecycleEvent[];
_initialLoaderId: string;
_disconnectedListener: RegisteredListener;
constructor(frameManager: FrameManager, frame: frames.Frame, timeout) {
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._newDocumentNavigationPromise = new Promise(fulfill => {
this._newDocumentNavigationCallback = fulfill;
});
this._sameDocumentNavigationPromise = new Promise(fulfill => {
this._sameDocumentNavigationCallback = fulfill;
});
this._lifecyclePromise = new Promise(fulfill => {
this._lifecycleCallback = fulfill;
});
/** @type {?Request} */
this._navigationRequest = null;
this._eventListeners = [
helper.addEventListener(frameManager._page, Events.Page.Load, event => this._newDocumentNavigationCallback()),
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.TargetSwappedOnNavigation, event => this._onTargetReconnected()),
helper.addEventListener(frameManager, FrameManagerEvents.FrameDetached, frame => this._onFrameDetached(frame)),
helper.addEventListener(frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)),
];
this._registerDisconnectedListener();
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._terminationPromise = new Promise(fulfill => {
this._terminationCallback = fulfill;
});
}
async waitForNavigation() {
const error = await Promise.race([
this._timeoutPromise,
this._newDocumentNavigationPromise,
this._sameDocumentNavigationPromise
]);
// TODO: handle exceptions
this.dispose();
if (error)
throw error;
return this.navigationResponse();
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]);
}
_registerDisconnectedListener() {
if (this._disconnectedListener)
helper.removeEventListeners([this._disconnectedListener]);
const session = this._frameManager._session;
this._disconnectedListener = helper.addEventListener(this._frameManager._session, TargetSessionEvents.Disconnected, () => {
// Session may change on swap out, check that it's current.
if (session === this._frameManager._session)
this._terminationCallback(new Error('Navigation failed because browser has disconnected!'));
});
}
async _onTargetReconnected() {
this._registerDisconnectedListener();
// In case web process change we migh have missed load event. Check current ready
// state to mitigate that.
try {
@ -504,9 +605,50 @@ class NextNavigationWatchdog {
}
}
_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._newDocumentNavigationCallback();
}
_onFrameDetached(frame: frames.Frame) {
if (this._frame === frame) {
this._terminationCallback.call(null, new Error('Navigating frame was detached'));
return;
}
this._checkLifecycle();
}
_onRequest(request: network.Request) {
@ -520,6 +662,7 @@ class NextNavigationWatchdog {
}
dispose() {
// TODO: handle exceptions
helper.removeEventListeners(this._eventListeners);
clearTimeout(this._timeoutId);
}

View File

@ -20,7 +20,7 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('Browser.version', function() {
it('should return whether we are in headless', async({browser}) => {
it.skip(WEBKIT)('should return whether we are in headless', async({browser}) => {
const version = await browser.version();
expect(version.length).toBeGreaterThan(0);
if (CHROME)

View File

@ -197,7 +197,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(error.message).toBe('No node found for selector: button.does-not-exist');
});
// @see https://github.com/GoogleChrome/puppeteer/issues/161
it('should not hang with touch-enabled viewports', async({page, server}) => {
it.skip(WEBKIT)('should not hang with touch-enabled viewports', async({page, server}) => {
await page.setViewport(playwright.devices['iPhone 6'].viewport);
await page.mouse.down();
await page.mouse.move(100, 10);
@ -328,7 +328,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(await page.evaluate(() => offsetY)).toBe(1910);
});
it('should update modifiers correctly', async({page, server}) => {
it.skip(WEBKIT)('should update modifiers correctly', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.click('button', { modifiers: ['Shift'] });
expect(await page.evaluate(() => shiftKey)).toBe(true);

View File

@ -70,7 +70,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
expect(newDimensions.width).toBe(Math.round(width + 104));
expect(newDimensions.height).toBe(Math.round(height + 104));
});
it('should select the text with mouse', async({page, server}) => {
it.skip(WEBKIT)('should select the text with mouse', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
const text = 'This is the text that we are going to try to select. Let\'s see how it goes.';
@ -103,7 +103,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
await page.hover('#button-6');
expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6');
});
it('should set modifier keys on click', async({page, server}) => {
it.skip(WEBKIT)('should set modifier keys on click', async({page, server}) => {
await page.goto(server.PREFIX + '/input/scrollable.html');
await page.evaluate(() => document.querySelector('#button-3').addEventListener('mousedown', e => window.lastEvent = e, true));
const modifiers = {'Shift': 'shiftKey', 'Control': 'ctrlKey', 'Alt': 'altKey', 'Meta': 'metaKey'};

View File

@ -26,7 +26,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
await page.goto(server.EMPTY_PAGE);
expect(page.url()).toBe(server.EMPTY_PAGE);
});
it.skip(WEBKIT)('should work with anchor navigation', async({page, server}) => {
it('should work with anchor navigation', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
expect(page.url()).toBe(server.EMPTY_PAGE);
await page.goto(server.EMPTY_PAGE + '#foo');
@ -72,7 +72,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'});
expect(response.status()).toBe(200);
});
it.skip(WEBKIT)('should work when page calls history API in beforeunload', async({page, server}) => {
it('should work when page calls history API in beforeunload', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
window.addEventListener('beforeunload', () => history.replaceState(null, 'initial', window.location.href), false);
@ -80,23 +80,23 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
const response = await page.goto(server.PREFIX + '/grid.html');
expect(response.status()).toBe(200);
});
it.skip(FFOX || WEBKIT)('should navigate to empty page with networkidle0', async({page, server}) => {
xit('should navigate to empty page with networkidle0', async({page, server}) => {
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle0'});
expect(response.status()).toBe(200);
});
it.skip(FFOX || WEBKIT)('should navigate to empty page with networkidle2', async({page, server}) => {
xit('should navigate to empty page with networkidle2', async({page, server}) => {
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle2'});
expect(response.status()).toBe(200);
});
it.skip(WEBKIT)('should fail when navigating to bad url', async({page, server}) => {
let error = null;
await page.goto('asdfasdf').catch(e => error = e);
// FIXME: shows dialog in WebKit.
if (CHROME || WEBKIT)
expect(error.message).toContain('Cannot navigate to invalid URL');
else
expect(error.message).toContain('Invalid url');
});
// FIXME: shows dialog in WebKit.
it.skip(WEBKIT)('should fail when navigating to bad SSL', async({page, httpsServer}) => {
// Make sure that network events do not emit 'undefined'.
// @see https://crbug.com/750469
@ -105,23 +105,24 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
page.on('requestfailed', request => expect(request).toBeTruthy());
let error = null;
await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e);
// FIXME: shows dialog in WebKit.
if (CHROME || WEBKIT)
expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID');
else
expect(error.message).toContain('SSL_ERROR_UNKNOWN');
});
// FIXME: shows dialog in WebKit.
it.skip(WEBKIT)('should fail when navigating to bad SSL after redirects', async({page, server, httpsServer}) => {
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html');
let error = null;
await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(e => error = e);
// FIXME: shows dialog in WebKit.
if (CHROME || WEBKIT)
expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID');
else
expect(error.message).toContain('SSL_ERROR_UNKNOWN');
});
it.skip(FFOX || WEBKIT)('should throw if networkidle is passed as an option', async({page, server}) => {
xit('should throw if networkidle is passed as an option', async({page, server}) => {
let error = null;
await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle'}).catch(err => error = err);
expect(error.message).toContain('"networkidle" option is no longer supported');
@ -203,7 +204,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(response.ok()).toBe(true);
expect(response.url()).toBe(server.EMPTY_PAGE);
});
it.skip(FFOX || WEBKIT)('should wait for network idle to succeed navigation', async({page, server}) => {
xit('should wait for network idle to succeed navigation', async({page, server}) => {
let responses = [];
// Hold on to a bunch of requests without answering.
server.setRoute('/fetch-request-a.js', (req, res) => responses.push(res));
@ -270,6 +271,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(warning).toBe(null);
});
it.skip(WEBKIT)('should not leak listeners during bad navigation', async({page, server}) => {
// FIXME: shows dialog in webkit.
let warning = null;
const warningHandler = w => warning = w;
process.on('warning', warningHandler);
@ -347,7 +349,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(response.ok()).toBe(true);
expect(response.url()).toContain('grid.html');
});
it.skip(WEBKIT)('should work with both domcontentloaded and load', async({page, server}) => {
it('should work with both domcontentloaded and load', async({page, server}) => {
let response = null;
server.setRoute('/one-style.css', (req, res) => response = res);
const navigationPromise = page.goto(server.PREFIX + '/one-style.html');
@ -436,11 +438,14 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
it.skip(WEBKIT)('should work when subframe issues window.stop()', async({page, server}) => {
server.setRoute('/frames/style.css', (req, res) => {});
const navigationPromise = page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = await utils.waitEvent(page, 'frameattached');
let frame;
await new Promise(fulfill => {
page.on('framenavigated', f => {
if (f === frame)
fulfill();
page.once('frameattached', attached => {
frame = attached;
page.on('framenavigated', f => {
if (f === frame)
fulfill();
});
});
});
await Promise.all([
@ -484,7 +489,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
});
describe('Frame.goto', function() {
it.skip(WEBKIT)('should navigate subframes', async({page, server}) => {
it('should navigate subframes', async({page, server}) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
expect(page.frames()[0].url()).toContain('/frames/one-frame.html');
expect(page.frames()[1].url()).toContain('/frames/frame.html');
@ -493,7 +498,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(response.ok()).toBe(true);
expect(response.frame()).toBe(page.frames()[1]);
});
it.skip(WEBKIT)('should reject when frame detaches', async({page, server}) => {
it('should reject when frame detaches', async({page, server}) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
server.setRoute('/empty.html', () => {});
@ -534,7 +539,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
});
describe('Frame.waitForNavigation', function() {
it.skip(WEBKIT)('should work', async({page, server}) => {
it('should work', async({page, server}) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = page.frames()[1];
const [response] = await Promise.all([
@ -546,7 +551,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(response.frame()).toBe(frame);
expect(page.url()).toContain('/frames/one-frame.html');
});
it.skip(WEBKIT)('should fail when frame detaches', async({page, server}) => {
it('should fail when frame detaches', async({page, server}) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = page.frames()[1];

View File

@ -80,7 +80,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
await page.evaluate(() => window.__FOO = 1);
await watchdog;
});
it.skip(WEBKIT)('should work when resolved right before execution context disposal', async({page, server}) => {
it('should work when resolved right before execution context disposal', async({page, server}) => {
// FIXME: implement Page.addScriptToEvaluateOnNewDocument in WebKit.
await page.evaluateOnNewDocument(() => window.__RELOADED = true);
await page.waitForFunction(() => {