feat(webkit): pause and configure provisional pages on creation (#200)

This commit is contained in:
Yury Semikhatsky 2019-12-10 15:34:36 -07:00 committed by GitHub
parent fe6addc71a
commit e8ec7e5118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 189 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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