mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: unify frame lifecycle events between browsers (#172)
This commit is contained in:
parent
e5a85e4e67
commit
b4c89ca0f9
@ -10,7 +10,7 @@
|
||||
"playwright": {
|
||||
"chromium_revision": "719491",
|
||||
"firefox_revision": "1004",
|
||||
"webkit_revision": "1016"
|
||||
"webkit_revision": "1022"
|
||||
},
|
||||
"scripts": {
|
||||
"unit": "node test/test.js",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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'],
|
||||
]);
|
||||
|
||||
@ -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') {
|
||||
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) {
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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'};
|
||||
|
||||
@ -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,13 +438,16 @@ 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.once('frameattached', attached => {
|
||||
frame = attached;
|
||||
page.on('framenavigated', f => {
|
||||
if (f === frame)
|
||||
fulfill();
|
||||
});
|
||||
});
|
||||
});
|
||||
await Promise.all([
|
||||
frame.evaluate(() => window.stop()),
|
||||
navigationPromise
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user