diff --git a/src/server/webkit.ts b/src/server/webkit.ts index 19228f2a42..414aceb591 100644 --- a/src/server/webkit.ts +++ b/src/server/webkit.ts @@ -79,7 +79,7 @@ export class WebKit implements BrowserType { async launchPersistent(userDataDir: string, options?: LaunchOptions): Promise { 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; diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 3ccd842d4a..dd0f35dd08 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -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(); @@ -43,14 +44,15 @@ export class WKBrowser extends platform.EventEmitter implements Browser { private _firstPageProxyCallback?: () => void; private readonly _firstPageProxyPromise: Promise; - static async connect(transport: ConnectionTransport, slowMo: number = 0): Promise { - const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo)); + static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise { + 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 { 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 { 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 { diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 02a1693e7c..bd43eebcdb 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -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; + private readonly _opener: WKPageProxy | null; private readonly _requestIdToRequest = new Map(); private readonly _workers: WKWorkers; private readonly _contextIdToContext: Map; @@ -53,9 +54,9 @@ export class WKPage implements PageDelegate { private _sessionListeners: RegisteredListener[] = []; private readonly _bootstrapScripts: string[] = []; - constructor(browserContext: BrowserContext, pageProxySession: WKSession, openerResolver: () => Promise) { + 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 { + const openerPage = this._opener ? await this._opener.page() : null; + return openerPage && !openerPage.isClosed() ? openerPage : null; } async reload(): Promise { diff --git a/src/webkit/wkPageProxy.ts b/src/webkit/wkPageProxy.ts index 78c572b87f..ec7e718a06 100644 --- a/src/webkit/wkPageProxy.ts +++ b/src/webkit/wkPageProxy.ts @@ -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 | null = null; - private _wkPage: WKPage | null = null; - private readonly _firstTargetPromise: Promise; - private _firstTargetCallback?: () => void; - private _pagePausedOnStart: boolean = false; + private readonly _opener: WKPageProxy | null; + private readonly _pagePromise: Promise; + private _pagePromiseFulfill: (page: Page | null) => void = () => {}; + private _pagePromiseReject: (error: Error) => void = () => {}; + private readonly _wkPage: WKPage; + private _initialized = false; private readonly _sessions = new Map(); 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 { - if (!this._pagePromise) - this._pagePromise = this._initializeWKPage(); + async page(): Promise { 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 { - 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); } }