diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 30c19c7752506..517836fe54c66 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -27,7 +27,6 @@ import * as types from '../types'; import { Protocol } from './protocol'; import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection'; import { WKPage } from './wkPage'; -import { WKPageProxy } from './wkPageProxy'; const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15'; @@ -37,11 +36,11 @@ export class WKBrowser extends platform.EventEmitter implements Browser { readonly _browserSession: WKSession; readonly _defaultContext: WKBrowserContext; readonly _contexts = new Map(); - readonly _pageProxies = new Map(); + readonly _wkPages = new Map(); private readonly _eventListeners: RegisteredListener[]; - private _firstPageProxyCallback?: () => void; - private readonly _firstPageProxyPromise: Promise; + private _firstPageCallback?: () => void; + private readonly _firstPagePromise: Promise; static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise { const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext); @@ -63,15 +62,15 @@ export class WKBrowser extends platform.EventEmitter implements Browser { helper.addEventListener(this._browserSession, kPageProxyMessageReceived, this._onPageProxyMessageReceived.bind(this)), ]; - this._firstPageProxyPromise = new Promise(resolve => this._firstPageProxyCallback = resolve); + this._firstPagePromise = new Promise(resolve => this._firstPageCallback = resolve); } _onDisconnect() { for (const context of this._contexts.values()) context._browserClosed(); - for (const pageProxy of this._pageProxies.values()) - pageProxy.dispose(); - this._pageProxies.clear(); + for (const wkPage of this._wkPages.values()) + wkPage.dispose(); + this._wkPages.clear(); this.emit(Events.Browser.Disconnected); } @@ -94,8 +93,8 @@ export class WKBrowser extends platform.EventEmitter implements Browser { } async _waitForFirstPageTarget(): Promise { - assert(!this._pageProxies.size); - return this._firstPageProxyPromise; + assert(!this._wkPages.size); + return this._firstPagePromise; } _onPageProxyCreated(event: Protocol.Browser.pageProxyCreatedPayload) { @@ -116,16 +115,16 @@ export class WKBrowser extends platform.EventEmitter implements Browser { const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => { this._connection.rawSend({ ...message, pageProxyId }); }); - const opener = pageProxyInfo.openerId ? this._pageProxies.get(pageProxyInfo.openerId) : undefined; - const pageProxy = new WKPageProxy(pageProxySession, context, opener || null); - this._pageProxies.set(pageProxyId, pageProxy); + const opener = pageProxyInfo.openerId ? this._wkPages.get(pageProxyInfo.openerId) : undefined; + const wkPage = new WKPage(context, pageProxySession, opener || null); + this._wkPages.set(pageProxyId, wkPage); - if (this._firstPageProxyCallback) { - this._firstPageProxyCallback(); - this._firstPageProxyCallback = undefined; + if (this._firstPageCallback) { + this._firstPageCallback(); + this._firstPageCallback = undefined; } - const pageEvent = new PageEvent(pageProxy.pageOrError()); + const pageEvent = new PageEvent(wkPage.pageOrError()); context.emit(Events.BrowserContext.Page, pageEvent); if (!opener) return; @@ -137,26 +136,26 @@ export class WKBrowser extends platform.EventEmitter implements Browser { _onPageProxyDestroyed(event: Protocol.Browser.pageProxyDestroyedPayload) { const pageProxyId = event.pageProxyId; - const pageProxy = this._pageProxies.get(pageProxyId); - if (!pageProxy) + const wkPage = this._wkPages.get(pageProxyId); + if (!wkPage) return; - pageProxy.didClose(); - pageProxy.dispose(); - this._pageProxies.delete(pageProxyId); + wkPage.didClose(false); + wkPage.dispose(); + this._wkPages.delete(pageProxyId); } _onPageProxyMessageReceived(event: PageProxyMessageReceivedPayload) { - const pageProxy = this._pageProxies.get(event.pageProxyId); - if (!pageProxy) + const wkPage = this._wkPages.get(event.pageProxyId); + if (!wkPage) return; - pageProxy.dispatchMessageToSession(event.message); + wkPage.dispatchMessageToSession(event.message); } _onProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) { - const pageProxy = this._pageProxies.get(event.pageProxyId); - if (!pageProxy) + const wkPage = this._wkPages.get(event.pageProxyId); + if (!wkPage) return; - pageProxy.handleProvisionalLoadFailed(event); + wkPage.handleProvisionalLoadFailed(event); } isConnected(): boolean { @@ -203,10 +202,10 @@ export class WKBrowserContext extends BrowserContextBase { _existingPages(): Page[] { const pages: Page[] = []; - for (const pageProxy of this._browser._pageProxies.values()) { - if (pageProxy._browserContext !== this) + for (const wkPage of this._browser._wkPages.values()) { + if (wkPage._browserContext !== this) continue; - const page = pageProxy.existingPage(); + const page = wkPage._initializedPage(); if (page) pages.push(page); } @@ -214,16 +213,16 @@ export class WKBrowserContext extends BrowserContextBase { } async pages(): Promise { - const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this); - const pages = await Promise.all(pageProxies.map(proxy => proxy.pageOrError())); + const wkPages = Array.from(this._browser._wkPages.values()).filter(wkPage => wkPage._browserContext === this); + const pages = await Promise.all(wkPages.map(wkPage => wkPage.pageOrError())); return pages.filter(page => page instanceof Page && !page.isClosed()) 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)!; - const result = await pageProxy.pageOrError(); + const wkPage = this._browser._wkPages.get(pageProxyId)!; + const result = await wkPage.pageOrError(); if (result instanceof Page) { if (result.isClosed()) throw new Error('Page has been closed.'); diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 8278faa39fc79..dabde85b053d7 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -33,11 +33,11 @@ import * as accessibility from '../accessibility'; import * as platform from '../platform'; import { getAccessibilityTree } from './wkAccessibility'; import { WKProvisionalPage } from './wkProvisionalPage'; -import { WKPageProxy } from './wkPageProxy'; import { WKBrowserContext } from './wkBrowser'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; +const isPovisionalSymbol = Symbol('isPovisional'); export class WKPage implements PageDelegate { readonly rawMouse: RawMouseImpl; @@ -45,17 +45,24 @@ export class WKPage implements PageDelegate { _session: WKSession; private _provisionalPage: WKProvisionalPage | null = null; readonly _page: Page; + private readonly _pagePromise: Promise; + private _pagePromiseCallback: (page: Page | Error) => void = () => {}; private readonly _pageProxySession: WKSession; - private readonly _opener: WKPageProxy | null; + private readonly _opener: WKPage | null; private readonly _requestIdToRequest = new Map(); private readonly _workers: WKWorkers; private readonly _contextIdToContext: Map; private _mainFrameContextId?: number; private _sessionListeners: RegisteredListener[] = []; + private _eventListeners: RegisteredListener[]; private readonly _evaluateOnNewDocumentSources: string[] = []; - private readonly _browserContext: WKBrowserContext; + readonly _browserContext: WKBrowserContext; + private _initialized = false; - constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) { + // TODO: we should be able to just use |this._session| and |this._provisionalPage|. + private readonly _sessions = new Map(); + + constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) { this._pageProxySession = pageProxySession; this._opener = opener; this.rawKeyboard = new RawKeyboardImpl(pageProxySession); @@ -66,6 +73,17 @@ export class WKPage implements PageDelegate { this._session = undefined as any as WKSession; this._browserContext = browserContext; this._page.on(Events.Page.FrameDetached, frame => this._removeContextsForFrame(frame, false)); + 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 => this._pagePromiseCallback = f); + } + + _initializedPage(): Page | undefined { + return this._initialized ? this._page : undefined; } private async _initializePageProxySession() { @@ -90,14 +108,6 @@ export class WKPage implements PageDelegate { this._workers.setSession(session); } - async initialize(session: WKSession) { - this._setSession(session); - await Promise.all([ - this._initializePageProxySession(), - this._initializeSession(this._session, ({frameTree}) => this._handleFrameTree(frameTree)), - ]); - } - // This method is called for provisional targets as well. The session passed as the parameter // may be different from the current session and may be destroyed without becoming current. async _initializeSession(session: WKSession, resourceTreeHandler: (r: Protocol.Page.getResourceTreeReturnValue) => void) { @@ -152,22 +162,28 @@ export class WKPage implements PageDelegate { await Promise.all(promises); } - initializeProvisionalPage(provisionalSession: WKSession): Promise { - assert(!this._provisionalPage); - this._provisionalPage = new WKProvisionalPage(provisionalSession, this); - return this._provisionalPage.initializationPromise; - } - - onProvisionalLoadCommitted(session: WKSession) { + private _onDidCommitProvisionalTarget(event: Protocol.Target.didCommitProvisionalTargetPayload) { + const { oldTargetId, newTargetId } = event; + const newSession = this._sessions.get(newTargetId); + assert(newSession, 'Unknown new target: ' + newTargetId); + const oldSession = this._sessions.get(oldTargetId); + assert(oldSession, 'Unknown old target: ' + oldTargetId); + oldSession.errorText = 'Target was swapped out.'; + (newSession as any)[isPovisionalSymbol] = undefined; assert(this._provisionalPage); - assert(this._provisionalPage._session === session); + assert(this._provisionalPage._session === newSession); this._provisionalPage.commit(); this._provisionalPage.dispose(); this._provisionalPage = null; - this._setSession(session); + this._setSession(newSession); } - onSessionDestroyed(session: WKSession, crashed: boolean) { + private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) { + const { targetId, crashed } = event; + const session = this._sessions.get(targetId); + assert(session, 'Unknown target destroyed: ' + targetId); + session.dispose(); + this._sessions.delete(targetId); if (this._provisionalPage && this._provisionalPage._session === session) { this._provisionalPage.dispose(); this._provisionalPage = null; @@ -186,6 +202,11 @@ export class WKPage implements PageDelegate { } dispose() { + this._pageProxySession.dispose(); + helper.removeEventListeners(this._eventListeners); + for (const session of this._sessions.values()) + session.dispose(); + this._sessions.clear(); if (this._provisionalPage) { this._provisionalPage.dispose(); this._provisionalPage = null; @@ -193,6 +214,72 @@ export class WKPage implements PageDelegate { this._page._didDisconnect(); } + dispatchMessageToSession(message: any) { + this._pageProxySession.dispatchMessage(message); + } + + handleProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) { + if (!this._initialized || !this._provisionalPage) + return; + let errorText = event.error; + if (errorText.includes('cancelled')) + errorText += '; maybe frame was detached?'; + this._page._frameManager.provisionalLoadFailed(this._page.mainFrame(), event.loaderId, errorText); + } + + async pageOrError(): Promise { + return this._pagePromise; + } + + 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', { + message: JSON.stringify(message), targetId: targetInfo.targetId + }).catch(e => { + session.dispatchMessage({ id: message.id, error: { message: e.message } }); + }); + }); + assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type); + this._sessions.set(targetInfo.targetId, session); + + if (!this._initialized) { + assert(!targetInfo.isProvisional); + let pageOrError: Page | Error; + try { + this._setSession(session); + await Promise.all([ + this._initializePageProxySession(), + this._initializeSession(session, ({frameTree}) => this._handleFrameTree(frameTree)), + ]); + pageOrError = this._page; + } catch (e) { + pageOrError = e; + } + if (targetInfo.isPaused) + this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError); + this._initialized = true; + this._pagePromiseCallback(pageOrError); + } else { + assert(targetInfo.isProvisional); + (session as any)[isPovisionalSymbol] = true; + assert(!this._provisionalPage); + this._provisionalPage = new WKProvisionalPage(session, this); + if (targetInfo.isPaused) { + this._provisionalPage.initializationPromise.then(() => { + this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError); + }); + } + } + } + + private _onDispatchMessageFromTarget(event: Protocol.Target.dispatchMessageFromTargetPayload) { + const { targetId, message } = event; + const session = this._sessions.get(targetId); + assert(session, 'Unknown target: ' + targetId); + session.dispatchMessage(JSON.parse(message)); + } + private _addSessionListeners() { this._sessionListeners = [ helper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), diff --git a/src/webkit/wkPageProxy.ts b/src/webkit/wkPageProxy.ts deleted file mode 100644 index 350ac64e34a03..0000000000000 --- a/src/webkit/wkPageProxy.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * 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 { assert, debugError, helper, RegisteredListener } from '../helper'; -import { Page } from '../page'; -import { Protocol } from './protocol'; -import { WKBrowserContext } from './wkBrowser'; -import { WKSession } from './wkConnection'; -import { WKPage } from './wkPage'; - -const isPovisionalSymbol = Symbol('isPovisional'); - -export class WKPageProxy { - private readonly _pageProxySession: WKSession; - readonly _browserContext: WKBrowserContext; - private readonly _opener: WKPageProxy | null; - private readonly _pagePromise: Promise; - private _pagePromiseCallback: (page: Page | Error) => void = () => {}; - private readonly _wkPage: WKPage; - private _initialized = false; - private readonly _sessions = new Map(); - private readonly _eventListeners: RegisteredListener[]; - - constructor(pageProxySession: WKSession, browserContext: WKBrowserContext, opener: WKPageProxy | null) { - this._pageProxySession = pageProxySession; - this._browserContext = browserContext; - 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 => this._pagePromiseCallback = f); - this._wkPage = new WKPage(this._browserContext, this._pageProxySession, this._opener); - } - - didClose() { - if (this._initialized) - this._wkPage.didClose(false); - } - - dispose() { - this._pageProxySession.dispose(); - helper.removeEventListeners(this._eventListeners); - for (const session of this._sessions.values()) - session.dispose(); - this._sessions.clear(); - this._wkPage.dispose(); - } - - dispatchMessageToSession(message: any) { - this._pageProxySession.dispatchMessage(message); - } - - private _isProvisionalCrossProcessLoadInProgress(): boolean { - for (const anySession of this._sessions.values()) { - if ((anySession as any)[isPovisionalSymbol]) - return true; - } - return false; - } - - handleProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) { - if (!this._initialized) - return; - if (!this._isProvisionalCrossProcessLoadInProgress()) - return; - let errorText = event.error; - if (errorText.includes('cancelled')) - errorText += '; maybe frame was detached?'; - this._wkPage._page._frameManager.provisionalLoadFailed(this._wkPage._page.mainFrame(), event.loaderId, errorText); - } - - async pageOrError(): Promise { - return this._pagePromise; - } - - existingPage(): Page | undefined { - return this._initialized ? this._wkPage._page : undefined; - } - - 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', { - message: JSON.stringify(message), targetId: targetInfo.targetId - }).catch(e => { - session.dispatchMessage({ id: message.id, error: { message: e.message } }); - }); - }); - assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type); - this._sessions.set(targetInfo.targetId, session); - - if (!this._initialized) { - assert(!targetInfo.isProvisional); - this._initialized = true; - let pageOrError: Page | Error; - try { - await this._wkPage.initialize(session); - pageOrError = this._wkPage._page; - } catch (e) { - pageOrError = e; - } - if (targetInfo.isPaused) - this._resumeTarget(targetInfo.targetId); - this._pagePromiseCallback(pageOrError); - } else { - assert(targetInfo.isProvisional); - (session as any)[isPovisionalSymbol] = true; - const provisionalPageInitialized = this._wkPage.initializeProvisionalPage(session); - if (targetInfo.isPaused) - provisionalPageInitialized.then(() => this._resumeTarget(targetInfo.targetId)); - } - } - - private _resumeTarget(targetId: string) { - this._pageProxySession.send('Target.resume', { targetId }).catch(debugError); - } - - private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) { - const { targetId, crashed } = event; - const session = this._sessions.get(targetId); - assert(session, 'Unknown target destroyed: ' + targetId); - session.dispose(); - this._sessions.delete(targetId); - this._wkPage.onSessionDestroyed(session, crashed); - } - - private _onDispatchMessageFromTarget(event: Protocol.Target.dispatchMessageFromTargetPayload) { - const { targetId, message } = event; - const session = this._sessions.get(targetId); - assert(session, 'Unknown target: ' + targetId); - session.dispatchMessage(JSON.parse(message)); - } - - private _onDidCommitProvisionalTarget(event: Protocol.Target.didCommitProvisionalTargetPayload) { - const { oldTargetId, newTargetId } = event; - const newSession = this._sessions.get(newTargetId); - assert(newSession, 'Unknown new target: ' + newTargetId); - const oldSession = this._sessions.get(oldTargetId); - assert(oldSession, 'Unknown old target: ' + oldTargetId); - // TODO: make some calls like screenshot catch swapped out error and retry. - oldSession.errorText = 'Target was swapped out.'; - (newSession as any)[isPovisionalSymbol] = undefined; - this._wkPage.onProvisionalLoadCommitted(newSession); - } -}