feat(webkit): introduce WKPageProxy and use it instead of WKTarget (#394)

This commit is contained in:
Yury Semikhatsky 2020-01-07 10:39:01 -08:00 committed by GitHub
parent f14409cea9
commit 52c175f001
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 282 additions and 211 deletions

View file

@ -10,7 +10,7 @@
"playwright": { "playwright": {
"chromium_revision": "724623", "chromium_revision": "724623",
"firefox_revision": "1009", "firefox_revision": "1009",
"webkit_revision": "1063" "webkit_revision": "1066"
}, },
"scripts": { "scripts": {
"unit": "node test/test.js", "unit": "node test/test.js",

View file

@ -15,46 +15,39 @@
* limitations under the License. * limitations under the License.
*/ */
import { helper, RegisteredListener, debugError, assert } from '../helper';
import * as browser from '../browser'; import * as browser from '../browser';
import * as network from '../network';
import * as types from '../types';
import { WKConnection, WKConnectionEvents, WKTargetSession } from './wkConnection';
import { Page } from '../page';
import { WKTarget } from './wkTarget';
import { Protocol } from './protocol';
import { Events } from '../events';
import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { BrowserContext, BrowserContextOptions } from '../browserContext';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import * as network from '../network';
import { Page } from '../page';
import { ConnectionTransport } from '../transport'; import { ConnectionTransport } from '../transport';
import * as types from '../types';
import { Protocol } from './protocol';
import { WKConnection, WKConnectionEvents, WKPageProxySession } from './wkConnection';
import { WKPageProxy } from './wkPageProxy';
export class WKBrowser extends browser.Browser { export class WKBrowser extends browser.Browser {
readonly _connection: WKConnection; readonly _connection: WKConnection;
private readonly _defaultContext: BrowserContext; private readonly _defaultContext: BrowserContext;
private readonly _contexts = new Map<string, BrowserContext>(); private readonly _contexts = new Map<string, BrowserContext>();
private readonly _targets = new Map<string, WKTarget>(); private readonly _pageProxies = new Map<string, WKPageProxy>();
private readonly _eventListeners: RegisteredListener[]; private readonly _eventListeners: RegisteredListener[];
private _firstTargetCallback?: () => void; private _firstPageProxyCallback?: () => void;
private readonly _firstTargetPromise: Promise<void>; private readonly _firstPageProxyPromise: Promise<void>;
constructor(transport: ConnectionTransport) { constructor(transport: ConnectionTransport) {
super(); super();
this._connection = new WKConnection(transport); this._connection = new WKConnection(transport);
/** @type {!Map<string, !WKTarget>} */
this._targets = new Map();
this._defaultContext = this._createBrowserContext(undefined, {}); this._defaultContext = this._createBrowserContext(undefined, {});
/** @type {!Map<string, !BrowserContext>} */
this._contexts = new Map();
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._connection, WKConnectionEvents.TargetCreated, this._onTargetCreated.bind(this)), helper.addEventListener(this._connection, WKConnectionEvents.PageProxyCreated, this._onPageProxyCreated.bind(this)),
helper.addEventListener(this._connection, WKConnectionEvents.TargetDestroyed, this._onTargetDestroyed.bind(this)), helper.addEventListener(this._connection, WKConnectionEvents.PageProxyDestroyed, this._onPageProxyDestroyed.bind(this))
helper.addEventListener(this._connection, WKConnectionEvents.DidCommitProvisionalTarget, this._onProvisionalTargetCommitted.bind(this)),
]; ];
this._firstTargetPromise = new Promise<void>(resolve => this._firstTargetCallback = resolve); this._firstPageProxyPromise = new Promise<void>(resolve => this._firstPageProxyCallback = resolve);
// Intercept provisional targets during cross-process navigation. // Intercept provisional targets during cross-process navigation.
this._connection.send('Target.setPauseOnStart', { pauseOnStart: true }).catch(e => { this._connection.send('Target.setPauseOnStart', { pauseOnStart: true }).catch(e => {
@ -81,65 +74,40 @@ export class WKBrowser extends browser.Browser {
} }
async _waitForFirstPageTarget(timeout: number): Promise<void> { async _waitForFirstPageTarget(timeout: number): Promise<void> {
assert(!this._targets.size); assert(!this._pageProxies.size);
await helper.waitWithTimeout(this._firstTargetPromise, 'target', timeout); await helper.waitWithTimeout(this._firstPageProxyPromise, 'firstPageProxy', timeout);
} }
_onTargetCreated(session: WKTargetSession, targetInfo: Protocol.Target.TargetInfo) { _onPageProxyCreated(session: WKPageProxySession, pageProxyInfo: Protocol.Browser.PageProxyInfo) {
assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type);
let context = null; let context = null;
if (targetInfo.browserContextId) { if (pageProxyInfo.browserContextId) {
// FIXME: we don't know about the default context id, so assume that all targets from // FIXME: we don't know about the default context id, so assume that all targets from
// unknown contexts are created in the 'default' context which can in practice be represented // unknown contexts are created in the 'default' context which can in practice be represented
// by multiple actual contexts in WebKit. Solving this properly will require adding context // by multiple actual contexts in WebKit. Solving this properly will require adding context
// lifecycle events. // lifecycle events.
context = this._contexts.get(targetInfo.browserContextId); context = this._contexts.get(pageProxyInfo.browserContextId);
// if (!context)
// throw new Error(`Target ${targetId} created in unknown browser context ${browserContextId}.`);
} }
if (!context) if (!context)
context = this._defaultContext; context = this._defaultContext;
const target = new WKTarget(this, session, targetInfo, context); const pageProxy = new WKPageProxy(this, session, context);
this._targets.set(targetInfo.targetId, target); this._pageProxies.set(pageProxyInfo.pageProxyId, pageProxy);
if (targetInfo.isProvisional) {
const oldTarget = this._targets.get(targetInfo.oldTargetId); if (pageProxyInfo.openerId) {
if (oldTarget) const opener = this._pageProxies.get(pageProxyInfo.openerId);
oldTarget._initializeSession(session); if (opener)
opener.onPopupCreated(pageProxy);
} }
if (this._firstTargetCallback) {
this._firstTargetCallback(); if (this._firstPageProxyCallback) {
this._firstTargetCallback = null; this._firstPageProxyCallback();
this._firstPageProxyCallback = null;
} }
if (!targetInfo.oldTargetId && targetInfo.openerId) {
const opener = this._targets.get(targetInfo.openerId);
if (!opener)
return;
const openerPage = opener._wkPage ? opener._wkPage._page : null;
if (!openerPage || !openerPage.listenerCount(Events.Page.Popup))
return;
target.page().then(page => openerPage.emit(Events.Page.Popup, page));
}
if (targetInfo.isPaused)
this._connection.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
} }
_onTargetDestroyed({targetId, crashed}) { _onPageProxyDestroyed(pageProxyId: Protocol.Browser.PageProxyID) {
const target = this._targets.get(targetId); const pageProxy = this._pageProxies.get(pageProxyId);
this._targets.delete(targetId); pageProxy.dispose();
target._didClose(crashed); this._pageProxies.delete(pageProxyId);
}
_closePage(targetId: string, runBeforeUnload: boolean) {
this._connection.send('Target.close', {
targetId,
runBeforeUnload
}).catch(debugError);
}
async _onProvisionalTargetCommitted({oldTargetId, newTargetId}) {
const oldTarget = this._targets.get(oldTargetId);
const newTarget = this._targets.get(newTargetId);
newTarget._swapWith(oldTarget);
} }
disconnect() { disconnect() {
@ -158,15 +126,14 @@ export class WKBrowser extends browser.Browser {
_createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext { _createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext {
const context = new BrowserContext({ const context = new BrowserContext({
pages: async (): Promise<Page[]> => { pages: async (): Promise<Page[]> => {
const targets = Array.from(this._targets.values()).filter(target => target._browserContext === context && !target._session.isProvisional()); const pageProxies = Array.from(this._pageProxies.values()).filter(proxy => proxy._browserContext === context);
const pages = await Promise.all(targets.map(target => target.page())); return await Promise.all(pageProxies.map(proxy => proxy.page()));
return pages.filter(page => !!page);
}, },
newPage: async (): Promise<Page> => { newPage: async (): Promise<Page> => {
const { targetId } = await this._connection.send('Browser.createPage', { browserContextId }); const { pageProxyId } = await this._connection.send('Browser.createPage', { browserContextId });
const target = this._targets.get(targetId); const pageProxy = this._pageProxies.get(pageProxyId);
return await target.page(); return await pageProxy.page();
}, },
close: async (): Promise<void> => { close: async (): Promise<void> => {

View file

@ -25,18 +25,23 @@ const debugProtocol = debug('playwright:protocol');
const debugWrappedMessage = require('debug')('wrapped'); const debugWrappedMessage = require('debug')('wrapped');
export const WKConnectionEvents = { export const WKConnectionEvents = {
TargetCreated: Symbol('ConnectionEvents.TargetCreated'), PageProxyCreated: Symbol('ConnectionEvents.PageProxyCreated'),
TargetDestroyed: Symbol('Connection.TargetDestroyed'), PageProxyDestroyed: Symbol('Connection.PageProxyDestroyed')
DidCommitProvisionalTarget: Symbol('Connection.DidCommitProvisionalTarget') };
export const WKPageProxySessionEvents = {
TargetCreated: Symbol('PageProxyEvents.TargetCreated'),
TargetDestroyed: Symbol('PageProxyEvents.TargetDestroyed'),
DidCommitProvisionalTarget: Symbol('PageProxyEvents.DidCommitProvisionalTarget'),
}; };
export class WKConnection extends EventEmitter { export class WKConnection extends EventEmitter {
_lastId = 0; private _lastId = 0;
private readonly _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 readonly _transport: ConnectionTransport; private readonly _transport: ConnectionTransport;
private readonly _sessions = new Map<string, WKTargetSession>(); private readonly _pageProxySessions = new Map<string, WKPageProxySession>();
_closed = false; private _closed = false;
constructor(transport: ConnectionTransport) { constructor(transport: ConnectionTransport) {
super(); super();
@ -45,18 +50,23 @@ export class WKConnection extends EventEmitter {
this._transport.onclose = this._onClose.bind(this); this._transport.onclose = this._onClose.bind(this);
} }
nextMessageId(): number {
return ++this._lastId;
}
send<T extends keyof Protocol.CommandParameters>( send<T extends keyof Protocol.CommandParameters>(
method: T, method: T,
params?: Protocol.CommandParameters[T] params?: Protocol.CommandParameters[T],
pageProxyId?: string
): Promise<Protocol.CommandReturnValues[T]> { ): Promise<Protocol.CommandReturnValues[T]> {
const id = this._rawSend({method, params}); const id = this._rawSend({pageProxyId, method, params});
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method}); this._callbacks.set(id, {resolve, reject, error: new Error(), method});
}); });
} }
_rawSend(message: any): number { _rawSend(message: any): number {
const id = ++this._lastId; const id = this.nextMessageId();
message = JSON.stringify(Object.assign({}, message, {id})); message = JSON.stringify(Object.assign({}, message, {id}));
debugProtocol('SEND ► ' + message); debugProtocol('SEND ► ' + message);
this._transport.send(message); this._transport.send(message);
@ -66,7 +76,7 @@ export class WKConnection extends EventEmitter {
private _dispatchMessage(message: string) { private _dispatchMessage(message: string) {
debugProtocol('◀ RECV ' + message); debugProtocol('◀ RECV ' + message);
const object = JSON.parse(message); const object = JSON.parse(message);
this._dispatchTargetMessageToSession(object, message); this._dispatchPageProxyMessage(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()`.
@ -84,40 +94,21 @@ export class WKConnection extends EventEmitter {
} }
} }
_dispatchTargetMessageToSession(object: {method: string, params: any}, wrappedMessage: string) { _dispatchPageProxyMessage(object: {method: string, params: any, id?: string, pageProxyId?: string}, message: string) {
if (object.method === 'Target.targetCreated') { if (object.method === 'Browser.pageProxyCreated') {
const targetInfo = object.params.targetInfo as Protocol.Target.TargetInfo; const pageProxyId = object.params.pageProxyInfo.pageProxyId;
const session = new WKTargetSession(this, targetInfo); const pageProxySession = new WKPageProxySession(this, pageProxyId);
this._sessions.set(session._sessionId, session); this._pageProxySessions.set(pageProxyId, pageProxySession);
Promise.resolve().then(() => this.emit(WKConnectionEvents.TargetCreated, session, object.params.targetInfo)); Promise.resolve().then(() => this.emit(WKConnectionEvents.PageProxyCreated, pageProxySession, object.params.pageProxyInfo));
} else if (object.method === 'Target.targetDestroyed') { } else if (object.method === 'Browser.pageProxyDestroyed') {
const session = this._sessions.get(object.params.targetId); const pageProxyId = object.params.pageProxyId as string;
if (session) { const pageProxySession = this._pageProxySessions.get(pageProxyId);
session._onClosed(); this._pageProxySessions.delete(pageProxyId);
this._sessions.delete(object.params.targetId); pageProxySession.dispose();
} Promise.resolve().then(() => this.emit(WKConnectionEvents.PageProxyDestroyed, pageProxyId));
Promise.resolve().then(() => this.emit(WKConnectionEvents.TargetDestroyed, { targetId: object.params.targetId, crashed: object.params.crashed })); } else if (!object.id && object.pageProxyId) {
} else if (object.method === 'Target.dispatchMessageFromTarget') { const pageProxySession = this._pageProxySessions.get(object.pageProxyId);
const {targetId, message} = object.params as Protocol.Target.dispatchMessageFromTargetPayload; pageProxySession._dispatchEvent(object, message);
const session = this._sessions.get(targetId);
if (!session)
throw new Error('Unknown target: ' + targetId);
if (session.isProvisional())
session._addProvisionalMessage(message);
else
session._dispatchMessageFromTarget(message);
} else if (object.method === 'Target.didCommitProvisionalTarget') {
const {oldTargetId, newTargetId} = object.params as Protocol.Target.didCommitProvisionalTargetPayload;
Promise.resolve().then(() => this.emit(WKConnectionEvents.DidCommitProvisionalTarget, { oldTargetId, newTargetId }));
const newSession = this._sessions.get(newTargetId);
if (!newSession)
throw new Error('Unknown new target: ' + newTargetId);
const oldSession = this._sessions.get(oldTargetId);
if (!oldSession)
throw new Error('Unknown old target: ' + oldTargetId);
oldSession._swappedOut = true;
for (const message of newSession._takeProvisionalMessagesAndCommit())
newSession._dispatchMessageFromTarget(message);
} }
} }
@ -130,9 +121,10 @@ export class WKConnection extends EventEmitter {
for (const callback of this._callbacks.values()) for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear(); this._callbacks.clear();
for (const session of this._sessions.values())
session._onClosed(); for (const pageProxySession of this._pageProxySessions.values())
this._sessions.clear(); pageProxySession.dispose();
this._pageProxySessions.clear();
} }
dispose() { dispose() {
@ -145,11 +137,89 @@ export const WKTargetSessionEvents = {
Disconnected: Symbol('TargetSessionEvents.Disconnected') Disconnected: Symbol('TargetSessionEvents.Disconnected')
}; };
export class WKPageProxySession extends EventEmitter {
_connection: WKConnection;
private readonly _sessions = new Map<string, WKTargetSession>();
private readonly _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
private readonly _pageProxyId: 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;
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;
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;
constructor(connection: WKConnection, pageProxyId: string) {
super();
this._connection = connection;
this._pageProxyId = pageProxyId;
}
send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (!this._connection)
return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the pageProxy has been closed.`));
return this._connection.send(method, params, this._pageProxyId).catch(e => {
// There is a possible race of the connection closure. We may have received
// targetDestroyed notification before response for the command, in that
// case it's safe to swallow the exception.
}) as Promise<Protocol.CommandReturnValues[T]>;
}
_dispatchEvent(object: {method: string, params: any, pageProxyId?: string}, wrappedMessage: string) {
if (object.method === 'Target.targetCreated') {
const targetInfo = object.params.targetInfo as Protocol.Target.TargetInfo;
const session = new WKTargetSession(this, targetInfo);
this._sessions.set(session._sessionId, session);
Promise.resolve().then(() => this.emit(WKPageProxySessionEvents.TargetCreated, session, object.params.targetInfo));
} else if (object.method === 'Target.targetDestroyed') {
const session = this._sessions.get(object.params.targetId);
if (session) {
session._onClosed();
this._sessions.delete(object.params.targetId);
}
Promise.resolve().then(() => this.emit(WKPageProxySessionEvents.TargetDestroyed, { targetId: object.params.targetId, crashed: object.params.crashed }));
} else if (object.method === 'Target.dispatchMessageFromTarget') {
const {targetId, message} = object.params as Protocol.Target.dispatchMessageFromTargetPayload;
const session = this._sessions.get(targetId);
if (!session)
throw new Error('Unknown target: ' + targetId);
if (session.isProvisional())
session._addProvisionalMessage(message);
else
session._dispatchMessageFromTarget(message);
} else if (object.method === 'Target.didCommitProvisionalTarget') {
const {oldTargetId, newTargetId} = object.params as Protocol.Target.didCommitProvisionalTargetPayload;
Promise.resolve().then(() => this.emit(WKPageProxySessionEvents.DidCommitProvisionalTarget, { oldTargetId, newTargetId }));
const newSession = this._sessions.get(newTargetId);
if (!newSession)
throw new Error('Unknown new target: ' + newTargetId);
const oldSession = this._sessions.get(oldTargetId);
if (!oldSession)
throw new Error('Unknown old target: ' + oldTargetId);
oldSession._swappedOut = true;
for (const message of newSession._takeProvisionalMessagesAndCommit())
newSession._dispatchMessageFromTarget(message);
} else {
Promise.resolve().then(() => this.emit(object.method, object.params));
}
}
dispose() {
for (const session of this._sessions.values())
session._onClosed();
this._sessions.clear();
this._connection = null;
}
}
export class WKTargetSession extends EventEmitter { export class WKTargetSession extends EventEmitter {
private _connection: WKConnection; _pageProxySession: WKPageProxySession;
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 _targetType: string; private readonly _targetType: string;
_sessionId: string; readonly _sessionId: string;
_swappedOut = false; _swappedOut = false;
private _provisionalMessages?: string[]; 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;
@ -158,10 +228,10 @@ export class WKTargetSession extends EventEmitter {
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: WKConnection, targetInfo: Protocol.Target.TargetInfo) { constructor(pageProxySession: WKPageProxySession, targetInfo: Protocol.Target.TargetInfo) {
super(); super();
const {targetId, type, isProvisional} = targetInfo; const {targetId, type, isProvisional} = targetInfo;
this._connection = connection; this._pageProxySession = pageProxySession;
this._targetType = type; this._targetType = type;
this._sessionId = targetId; this._sessionId = targetId;
if (isProvisional) if (isProvisional)
@ -172,13 +242,17 @@ export class WKTargetSession extends EventEmitter {
return !!this._provisionalMessages; return !!this._provisionalMessages;
} }
isClosed(): boolean {
return !this._pageProxySession;
}
send<T extends keyof Protocol.CommandParameters>( send<T extends keyof Protocol.CommandParameters>(
method: T, method: T,
params?: Protocol.CommandParameters[T] params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> { ): Promise<Protocol.CommandReturnValues[T]> {
if (!this._connection) if (!this._pageProxySession)
return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`)); return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
const innerId = ++this._connection._lastId; const innerId = this._pageProxySession._connection.nextMessageId();
const messageObj = { const messageObj = {
id: innerId, id: innerId,
method, method,
@ -190,7 +264,7 @@ export class WKTargetSession extends EventEmitter {
const result = new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => { const result = new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
this._callbacks.set(innerId, {resolve, reject, error: new Error(), method}); this._callbacks.set(innerId, {resolve, reject, error: new Error(), method});
}); });
this._connection.send('Target.sendMessageToTarget', { this._pageProxySession.send('Target.sendMessageToTarget', {
message: message, targetId: this._sessionId message: message, targetId: this._sessionId
}).catch(e => { }).catch(e => {
// There is a possible race of the connection closure. We may have received // There is a possible race of the connection closure. We may have received
@ -238,7 +312,7 @@ export class WKTargetSession extends EventEmitter {
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
} }
this._callbacks.clear(); this._callbacks.clear();
this._connection = null; this._pageProxySession = null;
Promise.resolve().then(() => this.emit(WKTargetSessionEvents.Disconnected)); Promise.resolve().then(() => this.emit(WKTargetSessionEvents.Disconnected));
} }
} }

View file

@ -367,7 +367,10 @@ export class WKPage implements PageDelegate {
} }
async closePage(runBeforeUnload: boolean): Promise<void> { async closePage(runBeforeUnload: boolean): Promise<void> {
this._browser._closePage(this._session._sessionId, runBeforeUnload); this._session._pageProxySession.send('Target.close', {
targetId: this._session._sessionId,
runBeforeUnload
}).catch(debugError);
} }
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> { getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {

108
src/webkit/wkPageProxy.ts Normal file
View file

@ -0,0 +1,108 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { BrowserContext } from '../browserContext';
import { Page } from '../page';
import { Protocol } from './protocol';
import { WKPageProxySession, WKPageProxySessionEvents, WKTargetSession } from './wkConnection';
import { WKPage } from './wkPage';
import { WKBrowser } from './wkBrowser';
import { RegisteredListener, helper, assert, debugError } from '../helper';
import { Events } from '../events';
export class WKPageProxy {
private readonly _browser: WKBrowser;
private readonly _pageProxySession: WKPageProxySession;
readonly _browserContext: BrowserContext;
private _pagePromise: Promise<Page> | null = null;
private _wkPage: WKPage | null = null;
private readonly _firstTargetPromise: Promise<void>;
private _firstTargetCallback: () => void;
private readonly _targetSessions = new Map<string, WKTargetSession>();
private readonly _eventListeners: RegisteredListener[];
constructor(browser: WKBrowser, session: WKPageProxySession, browserContext: BrowserContext) {
this._browser = browser;
this._pageProxySession = session;
this._browserContext = browserContext;
this._firstTargetPromise = new Promise(r => this._firstTargetCallback = r);
this._eventListeners = [
helper.addEventListener(this._pageProxySession, WKPageProxySessionEvents.TargetCreated, this._onTargetCreated.bind(this)),
helper.addEventListener(this._pageProxySession, WKPageProxySessionEvents.TargetDestroyed, this._onTargetDestroyed.bind(this)),
helper.addEventListener(this._pageProxySession, WKPageProxySessionEvents.DidCommitProvisionalTarget, this._onProvisionalTargetCommitted.bind(this))
];
}
dispose() {
helper.removeEventListeners(this._eventListeners);
}
async page(): Promise<Page> {
if (!this._pagePromise)
this._pagePromise = this._initializeWKPage();
return this._pagePromise;
}
onPopupCreated(popupPageProxy: WKPageProxy) {
if (!this._wkPage)
return;
if (!this._wkPage._page.listenerCount(Events.Page.Popup))
return;
popupPageProxy.page().then(page => this._wkPage._page.emit(Events.Page.Popup, page));
}
private async _initializeWKPage(): Promise<Page> {
await this._firstTargetPromise;
let session: WKTargetSession;
for (const targetSession of this._targetSessions.values()) {
if (!targetSession.isProvisional()) {
session = targetSession;
break;
}
}
assert(session, 'One non-provisional target session must exist');
this._wkPage = new WKPage(this._browser, this._browserContext);
this._wkPage.setSession(session);
await this._initializeSession(session);
return this._wkPage._page;
}
private _initializeSession(session: WKTargetSession) : Promise<void> {
return this._wkPage._initializeSession(session).catch(e => {
if (session.isClosed())
return;
// Swallow initialization errors due to newer target swap in,
// since we will reinitialize again.
if (this._wkPage._session === session)
throw e;
});
}
private _onTargetCreated(session: WKTargetSession, targetInfo: Protocol.Target.TargetInfo) {
assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type);
this._targetSessions.set(targetInfo.targetId, session);
if (this._firstTargetCallback) {
this._firstTargetCallback();
this._firstTargetCallback = null;
}
if (targetInfo.isProvisional && this._wkPage)
this._initializeSession(session);
if (targetInfo.isPaused)
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
}
private _onTargetDestroyed({targetId, crashed}) {
const targetSession = this._targetSessions.get(targetId);
this._targetSessions.delete(targetId);
if (!this._wkPage)
return;
if (this._wkPage._session === targetSession)
this._wkPage.didClose(crashed);
}
private _onProvisionalTargetCommitted({oldTargetId, newTargetId}) {
const newTargetSession = this._targetSessions.get(newTargetId);
this._wkPage.setSession(newTargetSession);
}
}

View file

@ -1,81 +0,0 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BrowserContext } from '../browserContext';
import { Page } from '../page';
import { Protocol } from './protocol';
import { WKTargetSession } from './wkConnection';
import { WKPage } from './wkPage';
import { WKBrowser } from './wkBrowser';
export class WKTarget {
readonly _browserContext: BrowserContext;
readonly _targetId: string;
readonly _session: WKTargetSession;
private _pagePromise: Promise<Page> | null = null;
private _browser: WKBrowser;
_wkPage: WKPage | null = null;
constructor(browser: WKBrowser, session: WKTargetSession, targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext) {
this._browser = browser;
this._session = session;
this._browserContext = browserContext;
this._targetId = targetInfo.targetId;
/** @type {?Promise<!Page>} */
this._pagePromise = null;
}
_didClose(crashed: boolean) {
if (this._wkPage)
this._wkPage.didClose(crashed);
}
async _initializeSession(session: WKTargetSession) {
if (!this._wkPage)
return;
await this._wkPage._initializeSession(session).catch(e => {
// Swallow initialization errors due to newer target swap in,
// since we will reinitialize again.
if (this._wkPage)
throw e;
});
}
async _swapWith(oldTarget: WKTarget) {
if (!oldTarget._pagePromise)
return;
this._pagePromise = oldTarget._pagePromise;
this._wkPage = oldTarget._wkPage;
// Swapped out target should not be accessed by anyone. Reset page promise so that
// old target does not close the page on connection reset.
oldTarget._pagePromise = null;
oldTarget._wkPage = null;
this._wkPage.setSession(this._session);
}
async page(): Promise<Page> {
if (!this._pagePromise) {
this._wkPage = new WKPage(this._browser, this._browserContext);
this._wkPage.setSession(this._session);
// Reference local page variable as |this._frameManager| may be
// cleared on swap.
const page = this._wkPage._page;
this._pagePromise = this._initializeSession(this._session).then(() => page);
}
return this._pagePromise;
}
}