chore(webkit): always attach to all pages, simplify initialization (#1139)

This commit is contained in:
Dmitry Gozman 2020-02-27 08:49:09 -08:00 committed by GitHub
parent 6b6a671754
commit c6fde22b1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 82 additions and 99 deletions

View File

@ -79,7 +79,7 @@ export class WebKit implements BrowserType {
async launchPersistent(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext> { async launchPersistent(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext> {
const { timeout = 30000 } = options || {}; const { timeout = 30000 } = options || {};
const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir); const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await WKBrowser.connect(transport!); const browser = await WKBrowser.connect(transport!, undefined, true);
await helper.waitWithTimeout(browser._waitForFirstPageTarget(), 'first page', timeout); await helper.waitWithTimeout(browser._waitForFirstPageTarget(), 'first page', timeout);
// Hack: for typical launch scenario, ensure that close waits for actual process termination. // Hack: for typical launch scenario, ensure that close waits for actual process termination.
const browserContext = browser._defaultContext; const browserContext = browser._defaultContext;

View File

@ -34,6 +34,7 @@ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) Appl
export class WKBrowser extends platform.EventEmitter implements Browser { export class WKBrowser extends platform.EventEmitter implements Browser {
private readonly _connection: WKConnection; private readonly _connection: WKConnection;
private readonly _attachToDefaultContext: boolean;
readonly _browserSession: WKSession; readonly _browserSession: WKSession;
readonly _defaultContext: BrowserContext; readonly _defaultContext: BrowserContext;
readonly _contexts = new Map<string, WKBrowserContext>(); readonly _contexts = new Map<string, WKBrowserContext>();
@ -43,14 +44,15 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
private _firstPageProxyCallback?: () => void; private _firstPageProxyCallback?: () => void;
private readonly _firstPageProxyPromise: Promise<void>; private readonly _firstPageProxyPromise: Promise<void>;
static async connect(transport: ConnectionTransport, slowMo: number = 0): Promise<WKBrowser> { static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> {
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo)); const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext);
return browser; return browser;
} }
constructor(transport: ConnectionTransport) { constructor(transport: ConnectionTransport, attachToDefaultContext: boolean) {
super(); super();
this._connection = new WKConnection(transport, this._onDisconnect.bind(this)); this._connection = new WKConnection(transport, this._onDisconnect.bind(this));
this._attachToDefaultContext = attachToDefaultContext;
this._browserSession = this._connection.browserSession; this._browserSession = this._connection.browserSession;
this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({})); this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({}));
@ -108,27 +110,17 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
// lifecycle events. // lifecycle events.
context = this._contexts.get(pageProxyInfo.browserContextId); context = this._contexts.get(pageProxyInfo.browserContextId);
} }
if (!context && !this._attachToDefaultContext)
return;
if (!context) if (!context)
context = this._defaultContext; context = this._defaultContext;
const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => { const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => {
this._connection.rawSend({ ...message, pageProxyId }); this._connection.rawSend({ ...message, pageProxyId });
}); });
const pageProxy = new WKPageProxy(pageProxySession, context, () => { const opener = pageProxyInfo.openerId ? this._pageProxies.get(pageProxyInfo.openerId) : undefined;
if (!pageProxyInfo.openerId) const pageProxy = new WKPageProxy(pageProxySession, context, opener || null);
return null;
const opener = this._pageProxies.get(pageProxyInfo.openerId);
if (!opener)
return null;
return opener;
});
this._pageProxies.set(pageProxyId, pageProxy); this._pageProxies.set(pageProxyId, pageProxy);
if (pageProxyInfo.openerId) {
const opener = this._pageProxies.get(pageProxyInfo.openerId);
if (opener)
opener.onPopupCreated(pageProxy);
}
if (this._firstPageProxyCallback) { if (this._firstPageProxyCallback) {
this._firstPageProxyCallback(); this._firstPageProxyCallback();
this._firstPageProxyCallback = undefined; this._firstPageProxyCallback = undefined;
@ -137,19 +129,25 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
_onPageProxyDestroyed(event: Protocol.Browser.pageProxyDestroyedPayload) { _onPageProxyDestroyed(event: Protocol.Browser.pageProxyDestroyedPayload) {
const pageProxyId = event.pageProxyId; const pageProxyId = event.pageProxyId;
const pageProxy = this._pageProxies.get(pageProxyId)!; const pageProxy = this._pageProxies.get(pageProxyId);
if (!pageProxy)
return;
pageProxy.didClose(); pageProxy.didClose();
pageProxy.dispose(); pageProxy.dispose();
this._pageProxies.delete(pageProxyId); this._pageProxies.delete(pageProxyId);
} }
_onPageProxyMessageReceived(event: PageProxyMessageReceivedPayload) { _onPageProxyMessageReceived(event: PageProxyMessageReceivedPayload) {
const pageProxy = this._pageProxies.get(event.pageProxyId)!; const pageProxy = this._pageProxies.get(event.pageProxyId);
if (!pageProxy)
return;
pageProxy.dispatchMessageToSession(event.message); pageProxy.dispatchMessageToSession(event.message);
} }
_onProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) { _onProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) {
const pageProxy = this._pageProxies.get(event.pageProxyId)!; const pageProxy = this._pageProxies.get(event.pageProxyId);
if (!pageProxy)
return;
pageProxy.handleProvisionalLoadFailed(event); pageProxy.handleProvisionalLoadFailed(event);
} }
@ -218,14 +216,16 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
async pages(): Promise<Page[]> { async pages(): Promise<Page[]> {
const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this); const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this);
return await Promise.all(pageProxies.map(proxy => proxy.page())); const pages = await Promise.all(pageProxies.map(proxy => proxy.page()));
return pages.filter(page => !!page) as Page[];
} }
async newPage(): Promise<Page> { async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this); assertBrowserContextIsNotOwned(this);
const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId }); const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId });
const pageProxy = this._browser._pageProxies.get(pageProxyId)!; const pageProxy = this._browser._pageProxies.get(pageProxyId)!;
return await pageProxy.page(); const page = await pageProxy.page();
return page!;
} }
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> { async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {

View File

@ -34,6 +34,7 @@ import * as accessibility from '../accessibility';
import * as platform from '../platform'; import * as platform from '../platform';
import { getAccessibilityTree } from './wkAccessibility'; import { getAccessibilityTree } from './wkAccessibility';
import { WKProvisionalPage } from './wkProvisionalPage'; import { WKProvisionalPage } from './wkProvisionalPage';
import { WKPageProxy } from './wkPageProxy';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -45,7 +46,7 @@ export class WKPage implements PageDelegate {
private _provisionalPage: WKProvisionalPage | null = null; private _provisionalPage: WKProvisionalPage | null = null;
readonly _page: Page; readonly _page: Page;
private readonly _pageProxySession: WKSession; private readonly _pageProxySession: WKSession;
private readonly _openerResolver: () => Promise<Page | null>; private readonly _opener: WKPageProxy | null;
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>(); private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
private readonly _workers: WKWorkers; private readonly _workers: WKWorkers;
private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>; private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>;
@ -53,9 +54,9 @@ export class WKPage implements PageDelegate {
private _sessionListeners: RegisteredListener[] = []; private _sessionListeners: RegisteredListener[] = [];
private readonly _bootstrapScripts: string[] = []; private readonly _bootstrapScripts: string[] = [];
constructor(browserContext: BrowserContext, pageProxySession: WKSession, openerResolver: () => Promise<Page | null>) { constructor(browserContext: BrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) {
this._pageProxySession = pageProxySession; this._pageProxySession = pageProxySession;
this._openerResolver = openerResolver; this._opener = opener;
this.rawKeyboard = new RawKeyboardImpl(pageProxySession); this.rawKeyboard = new RawKeyboardImpl(pageProxySession);
this.rawMouse = new RawMouseImpl(pageProxySession); this.rawMouse = new RawMouseImpl(pageProxySession);
this._contextIdToContext = new Map(); this._contextIdToContext = new Map();
@ -436,8 +437,9 @@ export class WKPage implements PageDelegate {
await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed. await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed.
} }
async opener() { async opener(): Promise<Page | null> {
return await this._openerResolver(); const openerPage = this._opener ? await this._opener.page() : null;
return openerPage && !openerPage.isClosed() ? openerPage : null;
} }
async reload(): Promise<void> { async reload(): Promise<void> {

View File

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { Page } from '../page'; import { Page } from '../page';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
@ -28,30 +27,34 @@ const isPovisionalSymbol = Symbol('isPovisional');
export class WKPageProxy { export class WKPageProxy {
private readonly _pageProxySession: WKSession; private readonly _pageProxySession: WKSession;
readonly _browserContext: BrowserContext; readonly _browserContext: BrowserContext;
private readonly _openerResolver: () => WKPageProxy | null; private readonly _opener: WKPageProxy | null;
private _pagePromise: Promise<Page> | null = null; private readonly _pagePromise: Promise<Page | null>;
private _wkPage: WKPage | null = null; private _pagePromiseFulfill: (page: Page | null) => void = () => {};
private readonly _firstTargetPromise: Promise<void>; private _pagePromiseReject: (error: Error) => void = () => {};
private _firstTargetCallback?: () => void; private readonly _wkPage: WKPage;
private _pagePausedOnStart: boolean = false; private _initialized = false;
private readonly _sessions = new Map<string, WKSession>(); private readonly _sessions = new Map<string, WKSession>();
private readonly _eventListeners: RegisteredListener[]; private readonly _eventListeners: RegisteredListener[];
constructor(pageProxySession: WKSession, browserContext: BrowserContext, openerResolver: () => (WKPageProxy | null)) { constructor(pageProxySession: WKSession, browserContext: BrowserContext, opener: WKPageProxy | null) {
this._pageProxySession = pageProxySession; this._pageProxySession = pageProxySession;
this._browserContext = browserContext; this._browserContext = browserContext;
this._openerResolver = openerResolver; this._opener = opener;
this._firstTargetPromise = new Promise(r => this._firstTargetCallback = r);
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)), helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)),
helper.addEventListener(this._pageProxySession, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), helper.addEventListener(this._pageProxySession, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
helper.addEventListener(this._pageProxySession, 'Target.dispatchMessageFromTarget', this._onDispatchMessageFromTarget.bind(this)), helper.addEventListener(this._pageProxySession, 'Target.dispatchMessageFromTarget', this._onDispatchMessageFromTarget.bind(this)),
helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)), helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)),
]; ];
this._pagePromise = new Promise((f, r) => {
this._pagePromiseFulfill = f;
this._pagePromiseReject = r;
});
this._wkPage = new WKPage(this._browserContext, this._pageProxySession, this._opener);
} }
didClose() { didClose() {
if (this._wkPage) if (this._initialized)
this._wkPage.didClose(false); this._wkPage.didClose(false);
} }
@ -61,8 +64,7 @@ export class WKPageProxy {
for (const session of this._sessions.values()) for (const session of this._sessions.values())
session.dispose(); session.dispose();
this._sessions.clear(); this._sessions.clear();
if (this._wkPage) this._wkPage.dispose();
this._wkPage.dispose();
} }
dispatchMessageToSession(message: any) { dispatchMessageToSession(message: any) {
@ -78,7 +80,7 @@ export class WKPageProxy {
} }
handleProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) { handleProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) {
if (!this._wkPage) if (!this._initialized)
return; return;
if (!this._isProvisionalCrossProcessLoadInProgress()) if (!this._isProvisionalCrossProcessLoadInProgress())
return; return;
@ -88,46 +90,15 @@ export class WKPageProxy {
this._wkPage._page._frameManager.provisionalLoadFailed(this._wkPage._page.mainFrame(), event.loaderId, errorText); this._wkPage._page._frameManager.provisionalLoadFailed(this._wkPage._page.mainFrame(), event.loaderId, errorText);
} }
async page(): Promise<Page> { async page(): Promise<Page | null> {
if (!this._pagePromise)
this._pagePromise = this._initializeWKPage();
return this._pagePromise; return this._pagePromise;
} }
existingPage(): Page | undefined { existingPage(): Page | undefined {
return this._wkPage ? this._wkPage._page : undefined; return this._initialized ? this._wkPage._page : undefined;
} }
onPopupCreated(popupPageProxy: WKPageProxy) { private async _onTargetCreated(event: Protocol.Target.targetCreatedPayload) {
if (this._wkPage)
popupPageProxy.page().then(page => this._wkPage!._page.emit(Events.Page.Popup, page));
}
private async _initializeWKPage(): Promise<Page> {
await this._firstTargetPromise;
let session: WKSession | undefined;
for (const anySession of this._sessions.values()) {
if (!(anySession as any)[isPovisionalSymbol]) {
session = anySession;
break;
}
}
assert(session, 'One non-provisional target session must exist');
this._wkPage = new WKPage(this._browserContext, this._pageProxySession, async () => {
const pageProxy = this._openerResolver();
if (!pageProxy)
return null;
return await pageProxy.page();
});
await this._wkPage.initialize(session);
if (this._pagePausedOnStart) {
this._resumeTarget(session.sessionId);
this._pagePausedOnStart = false;
}
return this._wkPage._page;
}
private _onTargetCreated(event: Protocol.Target.targetCreatedPayload) {
const { targetInfo } = event; const { targetInfo } = event;
const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, `The ${targetInfo.type} has been closed.`, (message: any) => { const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, `The ${targetInfo.type} has been closed.`, (message: any) => {
this._pageProxySession.send('Target.sendMessageToTarget', { this._pageProxySession.send('Target.sendMessageToTarget', {
@ -138,26 +109,38 @@ export class WKPageProxy {
}); });
assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type); assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type);
this._sessions.set(targetInfo.targetId, session); this._sessions.set(targetInfo.targetId, session);
if (this._firstTargetCallback) {
this._firstTargetCallback(); if (!this._initialized) {
this._firstTargetCallback = undefined; assert(!targetInfo.isProvisional);
} this._initialized = true;
if (targetInfo.isProvisional) { let page: Page | null = null;
(session as any)[isPovisionalSymbol] = true; let error: Error | undefined;
if (this._wkPage) { try {
const provisionalPageInitialized = this._wkPage.initializeProvisionalPage(session); await this._wkPage.initialize(session);
if (targetInfo.isPaused) page = this._wkPage._page;
provisionalPageInitialized.then(() => this._resumeTarget(targetInfo.targetId)); } catch (e) {
} else if (targetInfo.isPaused) { if (!this._pageProxySession.isDisposed())
this._resumeTarget(targetInfo.targetId); error = e;
} }
} else if (this._pagePromise) { if (error)
assert(!this._pagePausedOnStart); this._pagePromiseReject(error);
// This is the first time page target is created, will resume else
// after finishing intialization. this._pagePromiseFulfill(page);
this._pagePausedOnStart = !!targetInfo.isPaused; if (targetInfo.isPaused)
} else if (targetInfo.isPaused) { this._resumeTarget(targetInfo.targetId);
this._resumeTarget(targetInfo.targetId); if (page && this._opener) {
this._opener.page().then(openerPage => {
if (!openerPage || page!.isClosed())
return;
openerPage.emit(Events.Page.Popup, page);
});
}
} else {
assert(targetInfo.isProvisional);
(session as any)[isPovisionalSymbol] = true;
const provisionalPageInitialized = this._wkPage.initializeProvisionalPage(session);
if (targetInfo.isPaused)
provisionalPageInitialized.then(() => this._resumeTarget(targetInfo.targetId));
} }
} }
@ -171,8 +154,7 @@ export class WKPageProxy {
assert(session, 'Unknown target destroyed: ' + targetId); assert(session, 'Unknown target destroyed: ' + targetId);
session.dispose(); session.dispose();
this._sessions.delete(targetId); this._sessions.delete(targetId);
if (this._wkPage) this._wkPage.onSessionDestroyed(session, crashed);
this._wkPage.onSessionDestroyed(session, crashed);
} }
private _onDispatchMessageFromTarget(event: Protocol.Target.dispatchMessageFromTargetPayload) { private _onDispatchMessageFromTarget(event: Protocol.Target.dispatchMessageFromTargetPayload) {
@ -191,7 +173,6 @@ export class WKPageProxy {
// TODO: make some calls like screenshot catch swapped out error and retry. // TODO: make some calls like screenshot catch swapped out error and retry.
oldSession.errorText = 'Target was swapped out.'; oldSession.errorText = 'Target was swapped out.';
(newSession as any)[isPovisionalSymbol] = undefined; (newSession as any)[isPovisionalSymbol] = undefined;
if (this._wkPage) this._wkPage.onProvisionalLoadCommitted(newSession);
this._wkPage.onProvisionalLoadCommitted(newSession);
} }
} }