mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(webkit): pause and configure provisional pages on creation (#200)
This commit is contained in:
parent
fe6addc71a
commit
e8ec7e5118
@ -78,7 +78,7 @@ export class Page<Browser, BrowserContext extends BrowserContextInterface<Browse
|
|||||||
private _closedPromise: Promise<void>;
|
private _closedPromise: Promise<void>;
|
||||||
private _disconnected = false;
|
private _disconnected = false;
|
||||||
private _disconnectedCallback: (e: Error) => void;
|
private _disconnectedCallback: (e: Error) => void;
|
||||||
private _disconnectedPromise: Promise<Error>;
|
readonly _disconnectedPromise: Promise<Error>;
|
||||||
private _browserContext: BrowserContext;
|
private _browserContext: BrowserContext;
|
||||||
readonly keyboard: input.Keyboard;
|
readonly keyboard: input.Keyboard;
|
||||||
readonly mouse: input.Mouse;
|
readonly mouse: input.Mouse;
|
||||||
|
@ -59,6 +59,12 @@ export class Browser extends EventEmitter {
|
|||||||
helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
|
helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
|
||||||
helper.addEventListener(this._connection, 'Target.didCommitProvisionalTarget', this._onProvisionalTargetCommitted.bind(this)),
|
helper.addEventListener(this._connection, 'Target.didCommitProvisionalTarget', this._onProvisionalTargetCommitted.bind(this)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Intercept provisional targets during cross-process navigation.
|
||||||
|
this._connection.send('Target.setPauseOnStart', { pauseOnStart: true }).catch(e => {
|
||||||
|
debugError(e);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async userAgent(): Promise<string> {
|
async userAgent(): Promise<string> {
|
||||||
@ -157,6 +163,11 @@ export class Browser extends EventEmitter {
|
|||||||
context = this._defaultContext;
|
context = this._defaultContext;
|
||||||
const target = new Target(session, targetInfo, context);
|
const target = new Target(session, targetInfo, context);
|
||||||
this._targets.set(targetInfo.targetId, target);
|
this._targets.set(targetInfo.targetId, target);
|
||||||
|
if (targetInfo.isProvisional) {
|
||||||
|
const oldTarget = this._targets.get(targetInfo.oldTargetId);
|
||||||
|
if (oldTarget)
|
||||||
|
oldTarget._initializeSession(session);
|
||||||
|
}
|
||||||
this._privateEvents.emit(BrowserEvents.TargetCreated, target);
|
this._privateEvents.emit(BrowserEvents.TargetCreated, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +196,7 @@ export class Browser extends EventEmitter {
|
|||||||
async _onProvisionalTargetCommitted({oldTargetId, newTargetId}) {
|
async _onProvisionalTargetCommitted({oldTargetId, newTargetId}) {
|
||||||
const oldTarget = this._targets.get(oldTargetId);
|
const oldTarget = this._targets.get(oldTargetId);
|
||||||
const newTarget = this._targets.get(newTargetId);
|
const newTarget = this._targets.get(newTargetId);
|
||||||
newTarget._swappedIn(oldTarget);
|
newTarget._swapWith(oldTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
@ -15,26 +15,29 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {assert} from '../helper';
|
import {assert, debugError} from '../helper';
|
||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import { ConnectionTransport } from '../types';
|
import { ConnectionTransport } from '../types';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import { throws } from 'assert';
|
||||||
|
|
||||||
const debugProtocol = debug('playwright:protocol');
|
const debugProtocol = debug('playwright:protocol');
|
||||||
const debugWrappedMessage = require('debug')('wrapped');
|
const debugWrappedMessage = require('debug')('wrapped');
|
||||||
|
|
||||||
export const ConnectionEvents = {
|
export const ConnectionEvents = {
|
||||||
Disconnected: Symbol('ConnectionEvents.Disconnected'),
|
|
||||||
TargetCreated: Symbol('ConnectionEvents.TargetCreated')
|
TargetCreated: Symbol('ConnectionEvents.TargetCreated')
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Connection extends EventEmitter {
|
export class Connection extends EventEmitter {
|
||||||
_lastId = 0;
|
_lastId = 0;
|
||||||
private _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
private readonly _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
||||||
private _delay: number;
|
private readonly _delay: number;
|
||||||
private _transport: ConnectionTransport;
|
private readonly _transport: ConnectionTransport;
|
||||||
private _sessions = new Map<string, TargetSession>();
|
private readonly _sessions = new Map<string, TargetSession>();
|
||||||
|
private _incomingMessageQueue: string[] = [];
|
||||||
|
private _dispatchTimerId?: NodeJS.Timer;
|
||||||
|
|
||||||
_closed = false;
|
_closed = false;
|
||||||
|
|
||||||
constructor(transport: ConnectionTransport, delay: number | undefined = 0) {
|
constructor(transport: ConnectionTransport, delay: number | undefined = 0) {
|
||||||
@ -68,12 +71,48 @@ export class Connection extends EventEmitter {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onMessage(message: string) {
|
private _onMessage(message: string) {
|
||||||
if (this._delay)
|
if (this._incomingMessageQueue.length || this._delay)
|
||||||
await new Promise(f => setTimeout(f, this._delay));
|
this._enqueueMessage(message);
|
||||||
|
else
|
||||||
|
this._dispatchMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _enqueueMessage(message: string) {
|
||||||
|
this._incomingMessageQueue.push(message);
|
||||||
|
this._scheduleQueueDispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _enqueueMessages(messages: string[]) {
|
||||||
|
this._incomingMessageQueue = this._incomingMessageQueue.concat(messages);
|
||||||
|
this._scheduleQueueDispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _scheduleQueueDispatch() {
|
||||||
|
if (this._dispatchTimerId)
|
||||||
|
return;
|
||||||
|
if (!this._incomingMessageQueue.length)
|
||||||
|
return;
|
||||||
|
const delay = this._delay || 0;
|
||||||
|
this._dispatchTimerId = setTimeout(() => {
|
||||||
|
this._dispatchTimerId = undefined;
|
||||||
|
this._dispatchOneMessageFromQueue()
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dispatchOneMessageFromQueue() {
|
||||||
|
const message = this._incomingMessageQueue.shift();
|
||||||
|
try {
|
||||||
|
this._dispatchMessage(message);
|
||||||
|
} finally {
|
||||||
|
this._scheduleQueueDispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dispatchMessage(message: string) {
|
||||||
debugProtocol('◀ RECV ' + message);
|
debugProtocol('◀ RECV ' + message);
|
||||||
const object = JSON.parse(message);
|
const object = JSON.parse(message);
|
||||||
this._dispatchTargetMessageToSession(object);
|
this._dispatchTargetMessageToSession(object, message);
|
||||||
if (object.id) {
|
if (object.id) {
|
||||||
const callback = this._callbacks.get(object.id);
|
const callback = this._callbacks.get(object.id);
|
||||||
// Callbacks could be all rejected if someone has called `.dispose()`.
|
// Callbacks could be all rejected if someone has called `.dispose()`.
|
||||||
@ -91,12 +130,14 @@ export class Connection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_dispatchTargetMessageToSession(object: {method: string, params: any}) {
|
_dispatchTargetMessageToSession(object: {method: string, params: any}, wrappedMessage: string) {
|
||||||
if (object.method === 'Target.targetCreated') {
|
if (object.method === 'Target.targetCreated') {
|
||||||
const {targetId, type} = object.params.targetInfo;
|
const targetInfo = object.params.targetInfo as Protocol.Target.TargetInfo;
|
||||||
const session = new TargetSession(this, type, targetId);
|
const session = new TargetSession(this, targetInfo);
|
||||||
this._sessions.set(targetId, session);
|
this._sessions.set(session._sessionId, session);
|
||||||
this.emit(ConnectionEvents.TargetCreated, session, object.params.targetInfo);
|
this.emit(ConnectionEvents.TargetCreated, session, object.params.targetInfo);
|
||||||
|
if (targetInfo.isPaused)
|
||||||
|
this.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
|
||||||
} else if (object.method === 'Target.targetDestroyed') {
|
} else if (object.method === 'Target.targetDestroyed') {
|
||||||
const session = this._sessions.get(object.params.targetId);
|
const session = this._sessions.get(object.params.targetId);
|
||||||
if (session) {
|
if (session) {
|
||||||
@ -104,12 +145,16 @@ export class Connection extends EventEmitter {
|
|||||||
this._sessions.delete(object.params.targetId);
|
this._sessions.delete(object.params.targetId);
|
||||||
}
|
}
|
||||||
} else if (object.method === 'Target.dispatchMessageFromTarget') {
|
} else if (object.method === 'Target.dispatchMessageFromTarget') {
|
||||||
const session = this._sessions.get(object.params.targetId);
|
const {targetId, message} = object.params as Protocol.Target.dispatchMessageFromTargetPayload;
|
||||||
|
const session = this._sessions.get(targetId);
|
||||||
if (!session)
|
if (!session)
|
||||||
throw new Error('Unknown target: ' + object.params.targetId);
|
throw new Error('Unknown target: ' + targetId);
|
||||||
session._dispatchMessageFromTarget(object.params.message);
|
if (session.isProvisional())
|
||||||
|
session._addProvisionalMessage(wrappedMessage);
|
||||||
|
else
|
||||||
|
session._dispatchMessageFromTarget(message);
|
||||||
} else if (object.method === 'Target.didCommitProvisionalTarget') {
|
} else if (object.method === 'Target.didCommitProvisionalTarget') {
|
||||||
const {oldTargetId, newTargetId} = object.params;
|
const {oldTargetId, newTargetId} = object.params as Protocol.Target.didCommitProvisionalTargetPayload;
|
||||||
const newSession = this._sessions.get(newTargetId);
|
const newSession = this._sessions.get(newTargetId);
|
||||||
if (!newSession)
|
if (!newSession)
|
||||||
throw new Error('Unknown new target: ' + newTargetId);
|
throw new Error('Unknown new target: ' + newTargetId);
|
||||||
@ -117,6 +162,7 @@ export class Connection extends EventEmitter {
|
|||||||
if (!oldSession)
|
if (!oldSession)
|
||||||
throw new Error('Unknown old target: ' + oldTargetId);
|
throw new Error('Unknown old target: ' + oldTargetId);
|
||||||
oldSession._swappedOut = true;
|
oldSession._swappedOut = true;
|
||||||
|
this._enqueueMessages(newSession._takeProvisionalMessagesAndCommit());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +178,6 @@ export class Connection extends EventEmitter {
|
|||||||
for (const session of this._sessions.values())
|
for (const session of this._sessions.values())
|
||||||
session._onClosed();
|
session._onClosed();
|
||||||
this._sessions.clear();
|
this._sessions.clear();
|
||||||
this.emit(ConnectionEvents.Disconnected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
@ -149,19 +194,27 @@ export class TargetSession extends EventEmitter {
|
|||||||
_connection: Connection;
|
_connection: Connection;
|
||||||
private _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
private _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
||||||
private _targetType: string;
|
private _targetType: string;
|
||||||
private _sessionId: string;
|
_sessionId: string;
|
||||||
_swappedOut = false;
|
_swappedOut = false;
|
||||||
|
private _provisionalMessages?: string[];
|
||||||
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||||
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||||
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||||
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||||
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||||
|
|
||||||
constructor(connection: Connection, targetType: string, sessionId: string) {
|
constructor(connection: Connection, targetInfo: Protocol.Target.TargetInfo) {
|
||||||
super();
|
super();
|
||||||
|
const {targetId, type, isProvisional} = targetInfo;
|
||||||
this._connection = connection;
|
this._connection = connection;
|
||||||
this._targetType = targetType;
|
this._targetType = type;
|
||||||
this._sessionId = sessionId;
|
this._sessionId = targetId;
|
||||||
|
if (isProvisional)
|
||||||
|
this._provisionalMessages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
isProvisional() : boolean {
|
||||||
|
return !!this._provisionalMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
send<T extends keyof Protocol.CommandParameters>(
|
send<T extends keyof Protocol.CommandParameters>(
|
||||||
@ -194,7 +247,18 @@ export class TargetSession extends EventEmitter {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_addProvisionalMessage(message: string) {
|
||||||
|
this._provisionalMessages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_takeProvisionalMessagesAndCommit() : string[] {
|
||||||
|
const messages = this._provisionalMessages;
|
||||||
|
this._provisionalMessages = undefined;
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
_dispatchMessageFromTarget(message: string) {
|
_dispatchMessageFromTarget(message: string) {
|
||||||
|
console.assert(!this.isProvisional());
|
||||||
const object = JSON.parse(message);
|
const object = JSON.parse(message);
|
||||||
debugWrappedMessage('◀ RECV ' + JSON.stringify(object, null, 2));
|
debugWrappedMessage('◀ RECV ' + JSON.stringify(object, null, 2));
|
||||||
if (object.id && this._callbacks.has(object.id)) {
|
if (object.id && this._callbacks.has(object.id)) {
|
||||||
|
@ -40,7 +40,6 @@ const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
|||||||
|
|
||||||
export const FrameManagerEvents = {
|
export const FrameManagerEvents = {
|
||||||
FrameNavigatedWithinDocument: Symbol('FrameNavigatedWithinDocument'),
|
FrameNavigatedWithinDocument: Symbol('FrameNavigatedWithinDocument'),
|
||||||
TargetSwappedOnNavigation: Symbol('TargetSwappedOnNavigation'),
|
|
||||||
FrameAttached: Symbol('FrameAttached'),
|
FrameAttached: Symbol('FrameAttached'),
|
||||||
FrameDetached: Symbol('FrameDetached'),
|
FrameDetached: Symbol('FrameDetached'),
|
||||||
FrameNavigated: Symbol('FrameNavigated'),
|
FrameNavigated: Symbol('FrameNavigated'),
|
||||||
@ -58,14 +57,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||||||
readonly rawKeyboard: RawKeyboardImpl;
|
readonly rawKeyboard: RawKeyboardImpl;
|
||||||
readonly screenshotterDelegate: WKScreenshotDelegate;
|
readonly screenshotterDelegate: WKScreenshotDelegate;
|
||||||
_session: TargetSession;
|
_session: TargetSession;
|
||||||
_page: Page<Browser, BrowserContext>;
|
readonly _page: Page<Browser, BrowserContext>;
|
||||||
_networkManager: NetworkManager;
|
private readonly _networkManager: NetworkManager;
|
||||||
_frames: Map<string, frames.Frame>;
|
private readonly _frames: Map<string, frames.Frame>;
|
||||||
_contextIdToContext: Map<number, js.ExecutionContext>;
|
private readonly _contextIdToContext: Map<number, js.ExecutionContext>;
|
||||||
_isolatedWorlds: Set<string>;
|
private _isolatedWorlds: Set<string>;
|
||||||
_sessionListeners: RegisteredListener[] = [];
|
private _sessionListeners: RegisteredListener[] = [];
|
||||||
_mainFrame: frames.Frame;
|
private _mainFrame: frames.Frame;
|
||||||
private _bootstrapScripts: string[] = [];
|
private readonly _bootstrapScripts: string[] = [];
|
||||||
|
|
||||||
constructor(browserContext: BrowserContext) {
|
constructor(browserContext: BrowserContext) {
|
||||||
super();
|
super();
|
||||||
@ -83,34 +82,43 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||||||
this._networkManager.on(NetworkManagerEvents.RequestFinished, event => this._page.emit(Events.Page.RequestFinished, event));
|
this._networkManager.on(NetworkManagerEvents.RequestFinished, event => this._page.emit(Events.Page.RequestFinished, event));
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(session: TargetSession) {
|
setSession(session: TargetSession) {
|
||||||
helper.removeEventListeners(this._sessionListeners);
|
helper.removeEventListeners(this._sessionListeners);
|
||||||
this.disconnectFromTarget();
|
this.disconnectFromTarget();
|
||||||
this._session = session;
|
this._session = session;
|
||||||
this.rawKeyboard.setSession(session);
|
this.rawKeyboard.setSession(session);
|
||||||
this.rawMouse.setSession(session);
|
this.rawMouse.setSession(session);
|
||||||
this.screenshotterDelegate.initialize(session);
|
this.screenshotterDelegate.setSession(session);
|
||||||
this._addSessionListeners();
|
this._addSessionListeners();
|
||||||
this.emit(FrameManagerEvents.TargetSwappedOnNavigation);
|
this._networkManager.setSession(session);
|
||||||
const [,{frameTree}] = await Promise.all([
|
this._isolatedWorlds = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is called for provisional targets as well. The session passed as the parameter
|
||||||
|
// may be different from the current session and may be destroyed without becoming current.
|
||||||
|
async _initializeSession(session: TargetSession) {
|
||||||
|
const promises : Promise<any>[] = [
|
||||||
// Page agent must be enabled before Runtime.
|
// Page agent must be enabled before Runtime.
|
||||||
this._session.send('Page.enable'),
|
session.send('Page.enable'),
|
||||||
this._session.send('Page.getResourceTree'),
|
session.send('Page.getResourceTree').then(({frameTree}) => this._handleFrameTree(frameTree)),
|
||||||
]);
|
// Resource tree should be received before first execution context.
|
||||||
this._handleFrameTree(frameTree);
|
session.send('Runtime.enable').then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
|
||||||
await Promise.all([
|
session.send('Console.enable'),
|
||||||
this._session.send('Runtime.enable').then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
|
session.send('Page.setInterceptFileChooserDialog', { enabled: true }),
|
||||||
this._session.send('Console.enable'),
|
this._networkManager.initializeSession(session),
|
||||||
this._session.send('Dialog.enable'),
|
];
|
||||||
this._session.send('Page.setInterceptFileChooserDialog', { enabled: true }),
|
if (!session.isProvisional()) {
|
||||||
this._networkManager.initialize(session),
|
// FIXME: move dialog agent to web process.
|
||||||
]);
|
// Dialog agent resides in the UI process and should not be re-enabled on navigation.
|
||||||
|
promises.push(session.send('Dialog.enable'));
|
||||||
|
}
|
||||||
if (this._page._state.userAgent !== null)
|
if (this._page._state.userAgent !== null)
|
||||||
await this._session.send('Page.overrideUserAgent', { value: this._page._state.userAgent });
|
promises.push(session.send('Page.overrideUserAgent', { value: this._page._state.userAgent }));
|
||||||
if (this._page._state.mediaType !== null)
|
if (this._page._state.mediaType !== null)
|
||||||
await this._session.send('Page.setEmulatedMedia', { media: this._page._state.mediaType || '' });
|
promises.push(session.send('Page.setEmulatedMedia', { media: this._page._state.mediaType || '' }));
|
||||||
if (this._page._state.javascriptEnabled !== null)
|
if (this._page._state.javascriptEnabled !== null)
|
||||||
await this._session.send('Emulation.setJavaScriptEnabled', { enabled: this._page._state.javascriptEnabled });
|
promises.push(session.send('Emulation.setJavaScriptEnabled', { enabled: this._page._state.javascriptEnabled }));
|
||||||
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
didClose() {
|
didClose() {
|
||||||
@ -511,24 +519,24 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
class NextNavigationWatchdog {
|
class NextNavigationWatchdog {
|
||||||
_frameManager: FrameManager;
|
private readonly _frameManager: FrameManager;
|
||||||
_frame: frames.Frame;
|
private readonly _frame: frames.Frame;
|
||||||
_newDocumentNavigationPromise: Promise<Error | null>;
|
private readonly _newDocumentNavigationPromise: Promise<Error | null>;
|
||||||
_newDocumentNavigationCallback: (value?: unknown) => void;
|
private _newDocumentNavigationCallback: (value?: unknown) => void;
|
||||||
_sameDocumentNavigationPromise: Promise<Error | null>;
|
private readonly _sameDocumentNavigationPromise: Promise<Error | null>;
|
||||||
_sameDocumentNavigationCallback: (value?: unknown) => void;
|
private _sameDocumentNavigationCallback: (value?: unknown) => void;
|
||||||
private _lifecyclePromise: Promise<void>;
|
private readonly _lifecyclePromise: Promise<void>;
|
||||||
private _lifecycleCallback: () => void;
|
private _lifecycleCallback: () => void;
|
||||||
private _terminationPromise: Promise<Error | null>;
|
private readonly _frameDetachPromise: Promise<Error | null>;
|
||||||
private _terminationCallback: (err: Error | null) => void;
|
private _frameDetachCallback: (err: Error | null) => void;
|
||||||
_navigationRequest: any;
|
private readonly _initialSession: TargetSession;
|
||||||
_eventListeners: RegisteredListener[];
|
private _navigationRequest?: network.Request = null;
|
||||||
_timeoutPromise: Promise<Error | null>;
|
private readonly _eventListeners: RegisteredListener[];
|
||||||
_timeoutId: NodeJS.Timer;
|
private readonly _timeoutPromise: Promise<Error | null>;
|
||||||
_hasSameDocumentNavigation = false;
|
private readonly _timeoutId: NodeJS.Timer;
|
||||||
_expectedLifecycle: frames.LifecycleEvent[];
|
private _hasSameDocumentNavigation = false;
|
||||||
_initialLoaderId: string;
|
private readonly _expectedLifecycle: frames.LifecycleEvent[];
|
||||||
_disconnectedListener: RegisteredListener;
|
private readonly _initialLoaderId: string;
|
||||||
|
|
||||||
constructor(frameManager: FrameManager, frame: frames.Frame, waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[], timeout) {
|
constructor(frameManager: FrameManager, frame: frames.Frame, waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[], timeout) {
|
||||||
if (Array.isArray(waitUntil))
|
if (Array.isArray(waitUntil))
|
||||||
@ -539,6 +547,7 @@ class NextNavigationWatchdog {
|
|||||||
this._frameManager = frameManager;
|
this._frameManager = frameManager;
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
this._initialLoaderId = frameManager._frameData(frame).loaderId;
|
this._initialLoaderId = frameManager._frameData(frame).loaderId;
|
||||||
|
this._initialSession = frameManager._session;
|
||||||
this._newDocumentNavigationPromise = new Promise(fulfill => {
|
this._newDocumentNavigationPromise = new Promise(fulfill => {
|
||||||
this._newDocumentNavigationCallback = fulfill;
|
this._newDocumentNavigationCallback = fulfill;
|
||||||
});
|
});
|
||||||
@ -548,23 +557,19 @@ class NextNavigationWatchdog {
|
|||||||
this._lifecyclePromise = new Promise(fulfill => {
|
this._lifecyclePromise = new Promise(fulfill => {
|
||||||
this._lifecycleCallback = fulfill;
|
this._lifecycleCallback = fulfill;
|
||||||
});
|
});
|
||||||
/** @type {?Request} */
|
|
||||||
this._navigationRequest = null;
|
|
||||||
this._eventListeners = [
|
this._eventListeners = [
|
||||||
helper.addEventListener(frameManager, FrameManagerEvents.LifecycleEvent, frame => this._onLifecycleEvent(frame)),
|
helper.addEventListener(frameManager, FrameManagerEvents.LifecycleEvent, frame => this._onLifecycleEvent(frame)),
|
||||||
helper.addEventListener(frameManager, FrameManagerEvents.FrameNavigated, 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.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, FrameManagerEvents.FrameDetached, frame => this._onFrameDetached(frame)),
|
||||||
helper.addEventListener(frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)),
|
helper.addEventListener(frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)),
|
||||||
];
|
];
|
||||||
this._registerDisconnectedListener();
|
|
||||||
const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms');
|
const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms');
|
||||||
let timeoutCallback;
|
let timeoutCallback;
|
||||||
this._timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
this._timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||||
this._timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
this._timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||||
this._terminationPromise = new Promise(fulfill => {
|
this._frameDetachPromise = new Promise(fulfill => {
|
||||||
this._terminationCallback = fulfill;
|
this._frameDetachCallback = fulfill;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -581,37 +586,11 @@ class NextNavigationWatchdog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
timeoutOrTerminationPromise(): Promise<Error | null> {
|
timeoutOrTerminationPromise(): Promise<Error | null> {
|
||||||
return Promise.race([this._timeoutPromise, this._terminationPromise]);
|
return Promise.race([
|
||||||
}
|
this._timeoutPromise,
|
||||||
|
this._frameDetachPromise,
|
||||||
_registerDisconnectedListener() {
|
this._frameManager._page._disconnectedPromise
|
||||||
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 {
|
|
||||||
const context = await this._frame.executionContext();
|
|
||||||
const readyState = await context.evaluate(() => document.readyState);
|
|
||||||
switch (readyState) {
|
|
||||||
case 'loading':
|
|
||||||
case 'interactive':
|
|
||||||
case 'complete':
|
|
||||||
this._newDocumentNavigationCallback();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugError('_onTargetReconnected ' + e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLifecycleEvent(frame: frames.Frame) {
|
_onLifecycleEvent(frame: frames.Frame) {
|
||||||
@ -648,13 +627,14 @@ class NextNavigationWatchdog {
|
|||||||
this._lifecycleCallback();
|
this._lifecycleCallback();
|
||||||
if (this._hasSameDocumentNavigation)
|
if (this._hasSameDocumentNavigation)
|
||||||
this._sameDocumentNavigationCallback();
|
this._sameDocumentNavigationCallback();
|
||||||
if (this._frameManager._frameData(this._frame).loaderId !== this._initialLoaderId)
|
if (this._frameManager._frameData(this._frame).loaderId !== this._initialLoaderId ||
|
||||||
|
this._initialSession !== this._frameManager._session)
|
||||||
this._newDocumentNavigationCallback();
|
this._newDocumentNavigationCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFrameDetached(frame: frames.Frame) {
|
_onFrameDetached(frame: frames.Frame) {
|
||||||
if (this._frame === frame) {
|
if (this._frame === frame) {
|
||||||
this._terminationCallback.call(null, new Error('Navigating frame was detached'));
|
this._frameDetachCallback.call(null, new Error('Navigating frame was detached'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._checkLifecycle();
|
this._checkLifecycle();
|
||||||
|
@ -44,7 +44,7 @@ export class NetworkManager extends EventEmitter {
|
|||||||
this._frameManager = frameManager;
|
this._frameManager = frameManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(session: TargetSession) {
|
setSession(session: TargetSession) {
|
||||||
helper.removeEventListeners(this._sessionListeners);
|
helper.removeEventListeners(this._sessionListeners);
|
||||||
this._session = session;
|
this._session = session;
|
||||||
this._sessionListeners = [
|
this._sessionListeners = [
|
||||||
@ -53,8 +53,13 @@ export class NetworkManager extends EventEmitter {
|
|||||||
helper.addEventListener(this._session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
|
helper.addEventListener(this._session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
|
||||||
helper.addEventListener(this._session, 'Network.loadingFailed', this._onLoadingFailed.bind(this)),
|
helper.addEventListener(this._session, 'Network.loadingFailed', this._onLoadingFailed.bind(this)),
|
||||||
];
|
];
|
||||||
await this._session.send('Network.enable');
|
}
|
||||||
await this._session.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders });
|
|
||||||
|
async initializeSession(session: TargetSession) {
|
||||||
|
await Promise.all([
|
||||||
|
session.send('Network.enable'),
|
||||||
|
session.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders }),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setExtraHTTPHeaders(extraHTTPHeaders: { [s: string]: string; }) {
|
async setExtraHTTPHeaders(extraHTTPHeaders: { [s: string]: string; }) {
|
||||||
|
@ -11,7 +11,7 @@ import { TargetSession } from './Connection';
|
|||||||
export class WKScreenshotDelegate implements ScreenshotterDelegate {
|
export class WKScreenshotDelegate implements ScreenshotterDelegate {
|
||||||
private _session: TargetSession;
|
private _session: TargetSession;
|
||||||
|
|
||||||
initialize(session: TargetSession) {
|
setSession(session: TargetSession) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,18 @@ export class Target {
|
|||||||
this._page._didClose();
|
this._page._didClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _swappedIn(oldTarget: Target) {
|
async _initializeSession(session: TargetSession) {
|
||||||
|
if (!this._page)
|
||||||
|
return;
|
||||||
|
await (this._page._delegate as FrameManager)._initializeSession(session).catch(e => {
|
||||||
|
// Swallow initialization errors due to newer target swap in,
|
||||||
|
// since we will reinitialize again.
|
||||||
|
if (!isSwappedOutError(e))
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _swapWith(oldTarget: Target) {
|
||||||
if (!oldTarget._pagePromise)
|
if (!oldTarget._pagePromise)
|
||||||
return;
|
return;
|
||||||
this._pagePromise = oldTarget._pagePromise;
|
this._pagePromise = oldTarget._pagePromise;
|
||||||
@ -59,22 +70,17 @@ export class Target {
|
|||||||
// old target does not close the page on connection reset.
|
// old target does not close the page on connection reset.
|
||||||
oldTarget._pagePromise = null;
|
oldTarget._pagePromise = null;
|
||||||
oldTarget._page = null;
|
oldTarget._page = null;
|
||||||
await this._adoptPage();
|
this._adoptPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _adoptPage() {
|
private _adoptPage() {
|
||||||
(this._page as any)[targetSymbol] = this;
|
(this._page as any)[targetSymbol] = this;
|
||||||
this._session.once(TargetSessionEvents.Disconnected, () => {
|
this._session.once(TargetSessionEvents.Disconnected, () => {
|
||||||
// Once swapped out, we reset _page and won't call _didDisconnect for old session.
|
// Once swapped out, we reset _page and won't call _didDisconnect for old session.
|
||||||
if (this._page)
|
if (this._page)
|
||||||
this._page._didDisconnect();
|
this._page._didDisconnect();
|
||||||
});
|
});
|
||||||
await (this._page._delegate as FrameManager).initialize(this._session).catch(e => {
|
(this._page._delegate as FrameManager).setSession(this._session);
|
||||||
// Swallow initialization errors due to newer target swap in,
|
|
||||||
// since we will reinitialize again.
|
|
||||||
if (!isSwappedOutError(e))
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async page(): Promise<Page<Browser, BrowserContext>> {
|
async page(): Promise<Page<Browser, BrowserContext>> {
|
||||||
@ -86,7 +92,8 @@ export class Target {
|
|||||||
const page = frameManager._page;
|
const page = frameManager._page;
|
||||||
this._page = page;
|
this._page = page;
|
||||||
this._pagePromise = new Promise(async f => {
|
this._pagePromise = new Promise(async f => {
|
||||||
await this._adoptPage();
|
this._adoptPage();
|
||||||
|
await this._initializeSession(this._session);
|
||||||
if (browser._defaultViewport)
|
if (browser._defaultViewport)
|
||||||
await page.setViewport(browser._defaultViewport);
|
await page.setViewport(browser._defaultViewport);
|
||||||
f(page);
|
f(page);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user