mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-20 08:47:26 +03:00
feat(webkit): introduce WKPageProxy and use it instead of WKTarget (#394)
This commit is contained in:
parent
f14409cea9
commit
52c175f001
@ -10,7 +10,7 @@
|
||||
"playwright": {
|
||||
"chromium_revision": "724623",
|
||||
"firefox_revision": "1009",
|
||||
"webkit_revision": "1063"
|
||||
"webkit_revision": "1066"
|
||||
},
|
||||
"scripts": {
|
||||
"unit": "node test/test.js",
|
||||
|
@ -15,46 +15,39 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { helper, RegisteredListener, debugError, assert } from '../helper';
|
||||
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 { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||
import * as network from '../network';
|
||||
import { Page } from '../page';
|
||||
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 {
|
||||
readonly _connection: WKConnection;
|
||||
private readonly _defaultContext: 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 _firstTargetCallback?: () => void;
|
||||
private readonly _firstTargetPromise: Promise<void>;
|
||||
private _firstPageProxyCallback?: () => void;
|
||||
private readonly _firstPageProxyPromise: Promise<void>;
|
||||
|
||||
constructor(transport: ConnectionTransport) {
|
||||
super();
|
||||
this._connection = new WKConnection(transport);
|
||||
|
||||
/** @type {!Map<string, !WKTarget>} */
|
||||
this._targets = new Map();
|
||||
|
||||
this._defaultContext = this._createBrowserContext(undefined, {});
|
||||
/** @type {!Map<string, !BrowserContext>} */
|
||||
this._contexts = new Map();
|
||||
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._connection, WKConnectionEvents.TargetCreated, this._onTargetCreated.bind(this)),
|
||||
helper.addEventListener(this._connection, WKConnectionEvents.TargetDestroyed, this._onTargetDestroyed.bind(this)),
|
||||
helper.addEventListener(this._connection, WKConnectionEvents.DidCommitProvisionalTarget, this._onProvisionalTargetCommitted.bind(this)),
|
||||
helper.addEventListener(this._connection, WKConnectionEvents.PageProxyCreated, this._onPageProxyCreated.bind(this)),
|
||||
helper.addEventListener(this._connection, WKConnectionEvents.PageProxyDestroyed, this._onPageProxyDestroyed.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.
|
||||
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> {
|
||||
assert(!this._targets.size);
|
||||
await helper.waitWithTimeout(this._firstTargetPromise, 'target', timeout);
|
||||
assert(!this._pageProxies.size);
|
||||
await helper.waitWithTimeout(this._firstPageProxyPromise, 'firstPageProxy', timeout);
|
||||
}
|
||||
|
||||
_onTargetCreated(session: WKTargetSession, targetInfo: Protocol.Target.TargetInfo) {
|
||||
assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type);
|
||||
_onPageProxyCreated(session: WKPageProxySession, pageProxyInfo: Protocol.Browser.PageProxyInfo) {
|
||||
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
|
||||
// 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
|
||||
// lifecycle events.
|
||||
context = this._contexts.get(targetInfo.browserContextId);
|
||||
// if (!context)
|
||||
// throw new Error(`Target ${targetId} created in unknown browser context ${browserContextId}.`);
|
||||
context = this._contexts.get(pageProxyInfo.browserContextId);
|
||||
}
|
||||
if (!context)
|
||||
context = this._defaultContext;
|
||||
const target = new WKTarget(this, session, targetInfo, context);
|
||||
this._targets.set(targetInfo.targetId, target);
|
||||
if (targetInfo.isProvisional) {
|
||||
const oldTarget = this._targets.get(targetInfo.oldTargetId);
|
||||
if (oldTarget)
|
||||
oldTarget._initializeSession(session);
|
||||
const pageProxy = new WKPageProxy(this, session, context);
|
||||
this._pageProxies.set(pageProxyInfo.pageProxyId, pageProxy);
|
||||
|
||||
if (pageProxyInfo.openerId) {
|
||||
const opener = this._pageProxies.get(pageProxyInfo.openerId);
|
||||
if (opener)
|
||||
opener.onPopupCreated(pageProxy);
|
||||
}
|
||||
if (this._firstTargetCallback) {
|
||||
this._firstTargetCallback();
|
||||
this._firstTargetCallback = null;
|
||||
|
||||
if (this._firstPageProxyCallback) {
|
||||
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}) {
|
||||
const target = this._targets.get(targetId);
|
||||
this._targets.delete(targetId);
|
||||
target._didClose(crashed);
|
||||
}
|
||||
|
||||
_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);
|
||||
_onPageProxyDestroyed(pageProxyId: Protocol.Browser.PageProxyID) {
|
||||
const pageProxy = this._pageProxies.get(pageProxyId);
|
||||
pageProxy.dispose();
|
||||
this._pageProxies.delete(pageProxyId);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
@ -158,15 +126,14 @@ export class WKBrowser extends browser.Browser {
|
||||
_createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext {
|
||||
const context = new BrowserContext({
|
||||
pages: async (): Promise<Page[]> => {
|
||||
const targets = Array.from(this._targets.values()).filter(target => target._browserContext === context && !target._session.isProvisional());
|
||||
const pages = await Promise.all(targets.map(target => target.page()));
|
||||
return pages.filter(page => !!page);
|
||||
const pageProxies = Array.from(this._pageProxies.values()).filter(proxy => proxy._browserContext === context);
|
||||
return await Promise.all(pageProxies.map(proxy => proxy.page()));
|
||||
},
|
||||
|
||||
newPage: async (): Promise<Page> => {
|
||||
const { targetId } = await this._connection.send('Browser.createPage', { browserContextId });
|
||||
const target = this._targets.get(targetId);
|
||||
return await target.page();
|
||||
const { pageProxyId } = await this._connection.send('Browser.createPage', { browserContextId });
|
||||
const pageProxy = this._pageProxies.get(pageProxyId);
|
||||
return await pageProxy.page();
|
||||
},
|
||||
|
||||
close: async (): Promise<void> => {
|
||||
|
@ -25,18 +25,23 @@ const debugProtocol = debug('playwright:protocol');
|
||||
const debugWrappedMessage = require('debug')('wrapped');
|
||||
|
||||
export const WKConnectionEvents = {
|
||||
TargetCreated: Symbol('ConnectionEvents.TargetCreated'),
|
||||
TargetDestroyed: Symbol('Connection.TargetDestroyed'),
|
||||
DidCommitProvisionalTarget: Symbol('Connection.DidCommitProvisionalTarget')
|
||||
PageProxyCreated: Symbol('ConnectionEvents.PageProxyCreated'),
|
||||
PageProxyDestroyed: Symbol('Connection.PageProxyDestroyed')
|
||||
};
|
||||
|
||||
export const WKPageProxySessionEvents = {
|
||||
TargetCreated: Symbol('PageProxyEvents.TargetCreated'),
|
||||
TargetDestroyed: Symbol('PageProxyEvents.TargetDestroyed'),
|
||||
DidCommitProvisionalTarget: Symbol('PageProxyEvents.DidCommitProvisionalTarget'),
|
||||
};
|
||||
|
||||
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 _transport: ConnectionTransport;
|
||||
private readonly _sessions = new Map<string, WKTargetSession>();
|
||||
private readonly _pageProxySessions = new Map<string, WKPageProxySession>();
|
||||
|
||||
_closed = false;
|
||||
private _closed = false;
|
||||
|
||||
constructor(transport: ConnectionTransport) {
|
||||
super();
|
||||
@ -45,18 +50,23 @@ export class WKConnection extends EventEmitter {
|
||||
this._transport.onclose = this._onClose.bind(this);
|
||||
}
|
||||
|
||||
nextMessageId(): number {
|
||||
return ++this._lastId;
|
||||
}
|
||||
|
||||
send<T extends keyof Protocol.CommandParameters>(
|
||||
method: T,
|
||||
params?: Protocol.CommandParameters[T]
|
||||
params?: Protocol.CommandParameters[T],
|
||||
pageProxyId?: string
|
||||
): Promise<Protocol.CommandReturnValues[T]> {
|
||||
const id = this._rawSend({method, params});
|
||||
const id = this._rawSend({pageProxyId, method, params});
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||
});
|
||||
}
|
||||
|
||||
_rawSend(message: any): number {
|
||||
const id = ++this._lastId;
|
||||
const id = this.nextMessageId();
|
||||
message = JSON.stringify(Object.assign({}, message, {id}));
|
||||
debugProtocol('SEND ► ' + message);
|
||||
this._transport.send(message);
|
||||
@ -66,7 +76,7 @@ export class WKConnection extends EventEmitter {
|
||||
private _dispatchMessage(message: string) {
|
||||
debugProtocol('◀ RECV ' + message);
|
||||
const object = JSON.parse(message);
|
||||
this._dispatchTargetMessageToSession(object, message);
|
||||
this._dispatchPageProxyMessage(object, message);
|
||||
if (object.id) {
|
||||
const callback = this._callbacks.get(object.id);
|
||||
// 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) {
|
||||
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(WKConnectionEvents.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(WKConnectionEvents.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(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);
|
||||
_dispatchPageProxyMessage(object: {method: string, params: any, id?: string, pageProxyId?: string}, message: string) {
|
||||
if (object.method === 'Browser.pageProxyCreated') {
|
||||
const pageProxyId = object.params.pageProxyInfo.pageProxyId;
|
||||
const pageProxySession = new WKPageProxySession(this, pageProxyId);
|
||||
this._pageProxySessions.set(pageProxyId, pageProxySession);
|
||||
Promise.resolve().then(() => this.emit(WKConnectionEvents.PageProxyCreated, pageProxySession, object.params.pageProxyInfo));
|
||||
} else if (object.method === 'Browser.pageProxyDestroyed') {
|
||||
const pageProxyId = object.params.pageProxyId as string;
|
||||
const pageProxySession = this._pageProxySessions.get(pageProxyId);
|
||||
this._pageProxySessions.delete(pageProxyId);
|
||||
pageProxySession.dispose();
|
||||
Promise.resolve().then(() => this.emit(WKConnectionEvents.PageProxyDestroyed, pageProxyId));
|
||||
} else if (!object.id && object.pageProxyId) {
|
||||
const pageProxySession = this._pageProxySessions.get(object.pageProxyId);
|
||||
pageProxySession._dispatchEvent(object, message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,9 +121,10 @@ export class WKConnection extends EventEmitter {
|
||||
for (const callback of this._callbacks.values())
|
||||
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
|
||||
this._callbacks.clear();
|
||||
for (const session of this._sessions.values())
|
||||
session._onClosed();
|
||||
this._sessions.clear();
|
||||
|
||||
for (const pageProxySession of this._pageProxySessions.values())
|
||||
pageProxySession.dispose();
|
||||
this._pageProxySessions.clear();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@ -145,11 +137,89 @@ export const WKTargetSessionEvents = {
|
||||
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 {
|
||||
private _connection: WKConnection;
|
||||
private _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
||||
private _targetType: string;
|
||||
_sessionId: string;
|
||||
_pageProxySession: WKPageProxySession;
|
||||
private readonly _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
||||
private readonly _targetType: string;
|
||||
readonly _sessionId: string;
|
||||
_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;
|
||||
@ -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;
|
||||
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();
|
||||
const {targetId, type, isProvisional} = targetInfo;
|
||||
this._connection = connection;
|
||||
this._pageProxySession = pageProxySession;
|
||||
this._targetType = type;
|
||||
this._sessionId = targetId;
|
||||
if (isProvisional)
|
||||
@ -172,13 +242,17 @@ export class WKTargetSession extends EventEmitter {
|
||||
return !!this._provisionalMessages;
|
||||
}
|
||||
|
||||
isClosed(): boolean {
|
||||
return !this._pageProxySession;
|
||||
}
|
||||
|
||||
send<T extends keyof Protocol.CommandParameters>(
|
||||
method: T,
|
||||
params?: Protocol.CommandParameters[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.`));
|
||||
const innerId = ++this._connection._lastId;
|
||||
const innerId = this._pageProxySession._connection.nextMessageId();
|
||||
const messageObj = {
|
||||
id: innerId,
|
||||
method,
|
||||
@ -190,7 +264,7 @@ export class WKTargetSession extends EventEmitter {
|
||||
const result = new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
|
||||
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
|
||||
}).catch(e => {
|
||||
// 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.`));
|
||||
}
|
||||
this._callbacks.clear();
|
||||
this._connection = null;
|
||||
this._pageProxySession = null;
|
||||
Promise.resolve().then(() => this.emit(WKTargetSessionEvents.Disconnected));
|
||||
}
|
||||
}
|
||||
|
@ -367,7 +367,10 @@ export class WKPage implements PageDelegate {
|
||||
}
|
||||
|
||||
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> {
|
||||
|
108
src/webkit/wkPageProxy.ts
Normal file
108
src/webkit/wkPageProxy.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user