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> {
const { timeout = 30000 } = options || {};
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);
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
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 {
private readonly _connection: WKConnection;
private readonly _attachToDefaultContext: boolean;
readonly _browserSession: WKSession;
readonly _defaultContext: BrowserContext;
readonly _contexts = new Map<string, WKBrowserContext>();
@ -43,14 +44,15 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
private _firstPageProxyCallback?: () => void;
private readonly _firstPageProxyPromise: Promise<void>;
static async connect(transport: ConnectionTransport, slowMo: number = 0): Promise<WKBrowser> {
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo));
static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> {
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext);
return browser;
}
constructor(transport: ConnectionTransport) {
constructor(transport: ConnectionTransport, attachToDefaultContext: boolean) {
super();
this._connection = new WKConnection(transport, this._onDisconnect.bind(this));
this._attachToDefaultContext = attachToDefaultContext;
this._browserSession = this._connection.browserSession;
this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({}));
@ -108,27 +110,17 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
// lifecycle events.
context = this._contexts.get(pageProxyInfo.browserContextId);
}
if (!context && !this._attachToDefaultContext)
return;
if (!context)
context = this._defaultContext;
const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => {
this._connection.rawSend({ ...message, pageProxyId });
});
const pageProxy = new WKPageProxy(pageProxySession, context, () => {
if (!pageProxyInfo.openerId)
return null;
const opener = this._pageProxies.get(pageProxyInfo.openerId);
if (!opener)
return null;
return opener;
});
const opener = pageProxyInfo.openerId ? this._pageProxies.get(pageProxyInfo.openerId) : undefined;
const pageProxy = new WKPageProxy(pageProxySession, context, opener || null);
this._pageProxies.set(pageProxyId, pageProxy);
if (pageProxyInfo.openerId) {
const opener = this._pageProxies.get(pageProxyInfo.openerId);
if (opener)
opener.onPopupCreated(pageProxy);
}
if (this._firstPageProxyCallback) {
this._firstPageProxyCallback();
this._firstPageProxyCallback = undefined;
@ -137,19 +129,25 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
_onPageProxyDestroyed(event: Protocol.Browser.pageProxyDestroyedPayload) {
const pageProxyId = event.pageProxyId;
const pageProxy = this._pageProxies.get(pageProxyId)!;
const pageProxy = this._pageProxies.get(pageProxyId);
if (!pageProxy)
return;
pageProxy.didClose();
pageProxy.dispose();
this._pageProxies.delete(pageProxyId);
}
_onPageProxyMessageReceived(event: PageProxyMessageReceivedPayload) {
const pageProxy = this._pageProxies.get(event.pageProxyId)!;
const pageProxy = this._pageProxies.get(event.pageProxyId);
if (!pageProxy)
return;
pageProxy.dispatchMessageToSession(event.message);
}
_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);
}
@ -218,14 +216,16 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
async pages(): Promise<Page[]> {
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> {
assertBrowserContextIsNotOwned(this);
const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId });
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[]> {

View File

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

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { BrowserContext } from '../browserContext';
import { Page } from '../page';
import { Protocol } from './protocol';
@ -28,30 +27,34 @@ const isPovisionalSymbol = Symbol('isPovisional');
export class WKPageProxy {
private readonly _pageProxySession: WKSession;
readonly _browserContext: BrowserContext;
private readonly _openerResolver: () => WKPageProxy | null;
private _pagePromise: Promise<Page> | null = null;
private _wkPage: WKPage | null = null;
private readonly _firstTargetPromise: Promise<void>;
private _firstTargetCallback?: () => void;
private _pagePausedOnStart: boolean = false;
private readonly _opener: WKPageProxy | null;
private readonly _pagePromise: Promise<Page | null>;
private _pagePromiseFulfill: (page: Page | null) => void = () => {};
private _pagePromiseReject: (error: Error) => void = () => {};
private readonly _wkPage: WKPage;
private _initialized = false;
private readonly _sessions = new Map<string, WKSession>();
private readonly _eventListeners: RegisteredListener[];
constructor(pageProxySession: WKSession, browserContext: BrowserContext, openerResolver: () => (WKPageProxy | null)) {
constructor(pageProxySession: WKSession, browserContext: BrowserContext, opener: WKPageProxy | null) {
this._pageProxySession = pageProxySession;
this._browserContext = browserContext;
this._openerResolver = openerResolver;
this._firstTargetPromise = new Promise(r => this._firstTargetCallback = r);
this._opener = opener;
this._eventListeners = [
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.dispatchMessageFromTarget', this._onDispatchMessageFromTarget.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() {
if (this._wkPage)
if (this._initialized)
this._wkPage.didClose(false);
}
@ -61,8 +64,7 @@ export class WKPageProxy {
for (const session of this._sessions.values())
session.dispose();
this._sessions.clear();
if (this._wkPage)
this._wkPage.dispose();
this._wkPage.dispose();
}
dispatchMessageToSession(message: any) {
@ -78,7 +80,7 @@ export class WKPageProxy {
}
handleProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) {
if (!this._wkPage)
if (!this._initialized)
return;
if (!this._isProvisionalCrossProcessLoadInProgress())
return;
@ -88,46 +90,15 @@ export class WKPageProxy {
this._wkPage._page._frameManager.provisionalLoadFailed(this._wkPage._page.mainFrame(), event.loaderId, errorText);
}
async page(): Promise<Page> {
if (!this._pagePromise)
this._pagePromise = this._initializeWKPage();
async page(): Promise<Page | null> {
return this._pagePromise;
}
existingPage(): Page | undefined {
return this._wkPage ? this._wkPage._page : undefined;
return this._initialized ? this._wkPage._page : undefined;
}
onPopupCreated(popupPageProxy: WKPageProxy) {
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) {
private async _onTargetCreated(event: Protocol.Target.targetCreatedPayload) {
const { targetInfo } = event;
const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, `The ${targetInfo.type} has been closed.`, (message: any) => {
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);
this._sessions.set(targetInfo.targetId, session);
if (this._firstTargetCallback) {
this._firstTargetCallback();
this._firstTargetCallback = undefined;
}
if (targetInfo.isProvisional) {
(session as any)[isPovisionalSymbol] = true;
if (this._wkPage) {
const provisionalPageInitialized = this._wkPage.initializeProvisionalPage(session);
if (targetInfo.isPaused)
provisionalPageInitialized.then(() => this._resumeTarget(targetInfo.targetId));
} else if (targetInfo.isPaused) {
this._resumeTarget(targetInfo.targetId);
if (!this._initialized) {
assert(!targetInfo.isProvisional);
this._initialized = true;
let page: Page | null = null;
let error: Error | undefined;
try {
await this._wkPage.initialize(session);
page = this._wkPage._page;
} catch (e) {
if (!this._pageProxySession.isDisposed())
error = e;
}
} else if (this._pagePromise) {
assert(!this._pagePausedOnStart);
// This is the first time page target is created, will resume
// after finishing intialization.
this._pagePausedOnStart = !!targetInfo.isPaused;
} else if (targetInfo.isPaused) {
this._resumeTarget(targetInfo.targetId);
if (error)
this._pagePromiseReject(error);
else
this._pagePromiseFulfill(page);
if (targetInfo.isPaused)
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);
session.dispose();
this._sessions.delete(targetId);
if (this._wkPage)
this._wkPage.onSessionDestroyed(session, crashed);
this._wkPage.onSessionDestroyed(session, crashed);
}
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.
oldSession.errorText = 'Target was swapped out.';
(newSession as any)[isPovisionalSymbol] = undefined;
if (this._wkPage)
this._wkPage.onProvisionalLoadCommitted(newSession);
this._wkPage.onProvisionalLoadCommitted(newSession);
}
}