From 14e54ba40838e8d567c443c2cf88490aebf5fee5 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Mon, 7 Aug 2023 12:51:45 +0200 Subject: [PATCH] chore: target wip --- packages/puppeteer-core/src/api/Target.ts | 4 ++ packages/puppeteer-core/src/common/Browser.ts | 10 ++++- .../src/common/ChromeTargetManager.ts | 45 ++++++++++++++++--- .../puppeteer-core/src/common/Connection.ts | 18 +++++++- .../src/common/FirefoxTargetManager.ts | 7 +++ packages/puppeteer-core/src/common/Frame.ts | 10 +++++ .../puppeteer-core/src/common/FrameManager.ts | 41 +++++++++++++++-- .../src/common/LifecycleWatcher.ts | 10 +++++ packages/puppeteer-core/src/common/Page.ts | 11 +++++ packages/puppeteer-core/src/common/Target.ts | 17 ++++++- .../src/common/TargetManager.ts | 3 +- test/assets/prerender/index.html | 21 +++++++++ test/assets/prerender/target.html | 4 ++ test/src/launcher.spec.ts | 2 +- test/src/prerender.spec.ts | 39 ++++++++++++++++ 15 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 test/assets/prerender/index.html create mode 100644 test/assets/prerender/target.html create mode 100644 test/src/prerender.spec.ts diff --git a/packages/puppeteer-core/src/api/Target.ts b/packages/puppeteer-core/src/api/Target.ts index e699835d96174..57b434f37152f 100644 --- a/packages/puppeteer-core/src/api/Target.ts +++ b/packages/puppeteer-core/src/api/Target.ts @@ -31,6 +31,10 @@ export enum TargetType { BROWSER = 'browser', WEBVIEW = 'webview', OTHER = 'other', + /** + * @internal + */ + TAB = 'tab', } /** diff --git a/packages/puppeteer-core/src/common/Browser.ts b/packages/puppeteer-core/src/common/Browser.ts index 6a0df7095cef8..1e7327794b42f 100644 --- a/packages/puppeteer-core/src/common/Browser.ts +++ b/packages/puppeteer-core/src/common/Browser.ts @@ -438,7 +438,9 @@ export class CDPBrowser extends BrowserBase { url: 'about:blank', browserContextId: contextId || undefined, }); - const target = this.#targetManager.getAvailableTargets().get(targetId); + const target = (await this.waitForTarget(t => { + return (t as CDPTarget)._targetId === targetId; + })) as CDPTarget; if (!target) { throw new Error(`Missing target for page (id = ${targetId})`); } @@ -577,7 +579,11 @@ export class CDPBrowserContext extends BrowserContext { options: {timeout?: number} = {} ): Promise { return this.#browser.waitForTarget(target => { - return target.browserContext() === this && predicate(target); + return ( + target.browserContext() === this && + target.type() !== 'tab' && + predicate(target) + ); }, options); } diff --git a/packages/puppeteer-core/src/common/ChromeTargetManager.ts b/packages/puppeteer-core/src/common/ChromeTargetManager.ts index e31e14f24f2d5..1915c26b8bb74 100644 --- a/packages/puppeteer-core/src/common/ChromeTargetManager.ts +++ b/packages/puppeteer-core/src/common/ChromeTargetManager.ts @@ -85,6 +85,9 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { #initializeDeferred = Deferred.create(); #targetsIdsForInit = new Set(); + #tabMode = true; + #discoveryFilter = this.#tabMode ? [{}] : [{type: 'tab', exclude: true}, {}]; + constructor( connection: Connection, targetFactory: TargetFactory, @@ -104,7 +107,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { this.#connection .send('Target.setDiscoverTargets', { discover: true, - filter: [{type: 'tab', exclude: true}, {}], + filter: this.#discoveryFilter, }) .then(this.#storeExistingTargetsForInit) .catch(debugError); @@ -137,6 +140,15 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { waitForDebuggerOnStart: true, flatten: true, autoAttach: true, + filter: this.#tabMode + ? [ + { + type: 'page', + exclude: true, + }, + ...this.#discoveryFilter, + ] + : this.#discoveryFilter, }); this.#finishInitializationIfReady(); await this.#initializeDeferred.valueOrThrow(); @@ -152,7 +164,13 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { } getAvailableTargets(): Map { - return this.#attachedTargetsByTargetId; + const result = new Map(); + for (const [id, target] of this.#attachedTargetsByTargetId.entries()) { + if (target.type() !== 'tab' && !target._subtype()) { + result.set(id, target); + } + } + return result; } addTargetInterceptor( @@ -279,6 +297,16 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { const wasInitialized = target._initializedDeferred.value() === InitializationStatus.SUCCESS; + if (target._subtype() && !event.targetInfo.subtype) { + const target = this.#attachedTargetsByTargetId.get( + event.targetInfo.targetId + ); + const session = target?._session(); + if (session) { + session.parentSession()?.emit('sessionswapped', session); + } + } + target._targetInfoChanged(event.targetInfo); if (wasInitialized && previousURL !== target.url()) { @@ -344,7 +372,11 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { const target = existingTarget ? this.#attachedTargetsByTargetId.get(targetInfo.targetId)! - : this.#targetFactory(targetInfo, session); + : this.#targetFactory( + targetInfo, + session, + parentSession instanceof CDPSession ? parentSession : undefined + ); if (this.#targetFilterCallback && !this.#targetFilterCallback(target)) { this.#ignoredTargets.add(targetInfo.targetId); @@ -385,7 +417,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { } this.#targetsIdsForInit.delete(target._targetId); - if (!existingTarget) { + if (!existingTarget && target.type() !== 'tab' && !target._subtype()) { this.emit(TargetManagerEmittedEvents.TargetAvailable, target); } this.#finishInitializationIfReady(); @@ -397,6 +429,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { waitForDebuggerOnStart: true, flatten: true, autoAttach: true, + filter: this.#discoveryFilter, }), session.send('Runtime.runIfWaitingForDebugger'), ]).catch(debugError); @@ -422,6 +455,8 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager { } this.#attachedTargetsByTargetId.delete(target._targetId); - this.emit(TargetManagerEmittedEvents.TargetGone, target); + if (target.type() !== 'tab') { + this.emit(TargetManagerEmittedEvents.TargetGone, target); + } }; } diff --git a/packages/puppeteer-core/src/common/Connection.ts b/packages/puppeteer-core/src/common/Connection.ts index 0a5c366a7d80f..175d9d659386c 100644 --- a/packages/puppeteer-core/src/common/Connection.ts +++ b/packages/puppeteer-core/src/common/Connection.ts @@ -24,6 +24,7 @@ import {ConnectionTransport} from './ConnectionTransport.js'; import {debug} from './Debug.js'; import {TargetCloseError, ProtocolError} from './Errors.js'; import {EventEmitter} from './EventEmitter.js'; +import {CDPTarget} from './Target.js'; import {debugError} from './util.js'; const debugProtocolSend = debug('puppeteer:protocol:SEND ►'); @@ -307,6 +308,7 @@ export class Connection extends EventEmitter { const object = JSON.parse(message); if (object.method === 'Target.attachedToTarget') { const sessionId = object.params.sessionId; + const parentSession = this.#sessions.get(object.sessionId); const session = new CDPSessionImpl( this, object.params.targetInfo.type, @@ -315,7 +317,6 @@ export class Connection extends EventEmitter { ); this.#sessions.set(sessionId, session); this.emit('sessionattached', session); - const parentSession = this.#sessions.get(object.sessionId); if (parentSession) { parentSession.emit('sessionattached', session); } @@ -515,6 +516,7 @@ export class CDPSessionImpl extends CDPSession { #callbacks = new CallbackRegistry(); #connection?: Connection; #parentSessionId?: string; + #target?: CDPTarget; /** * @internal @@ -532,6 +534,20 @@ export class CDPSessionImpl extends CDPSession { this.#parentSessionId = parentSessionId; } + /** + * @internal + */ + _setTarget(target: CDPTarget): void { + this.#target = target; + } + + /** + * @internal + */ + _target(): CDPTarget | undefined { + return this.#target; + } + override connection(): Connection | undefined { return this.#connection; } diff --git a/packages/puppeteer-core/src/common/FirefoxTargetManager.ts b/packages/puppeteer-core/src/common/FirefoxTargetManager.ts index 58275425228ae..423d2809f8a34 100644 --- a/packages/puppeteer-core/src/common/FirefoxTargetManager.ts +++ b/packages/puppeteer-core/src/common/FirefoxTargetManager.ts @@ -108,6 +108,13 @@ export class FirefoxTargetManager this.setupAttachmentListeners(this.#connection); } + /** + * @internal + */ + _tabTargetBySession(_session?: CDPSession): CDPSession | undefined { + return undefined; + } + addTargetInterceptor( client: CDPSession | Connection, interceptor: TargetInterceptor diff --git a/packages/puppeteer-core/src/common/Frame.ts b/packages/puppeteer-core/src/common/Frame.ts index 3d93dc4d84206..6249a298266cd 100644 --- a/packages/puppeteer-core/src/common/Frame.ts +++ b/packages/puppeteer-core/src/common/Frame.ts @@ -49,6 +49,7 @@ export const FrameEmittedEvents = { LifecycleEvent: Symbol('Frame.LifecycleEvent'), FrameNavigatedWithinDocument: Symbol('Frame.FrameNavigatedWithinDocument'), FrameDetached: Symbol('Frame.FrameDetached'), + FrameSwappedByActivation: Symbol('Frame.FrameSwappedByActivation'), }; /** @@ -82,6 +83,15 @@ export class Frame extends BaseFrame { this._loaderId = ''; this.updateClient(client); + + this.on(FrameEmittedEvents.FrameSwappedByActivation, () => { + this._onLoadingStarted(); + this._onLoadingStopped(); + }); + } + + updateId(id: string): void { + this._id = id; } updateClient(client: CDPSession): void { diff --git a/packages/puppeteer-core/src/common/FrameManager.ts b/packages/puppeteer-core/src/common/FrameManager.ts index 3f6c6a0b4bc1e..4929a4522f69b 100644 --- a/packages/puppeteer-core/src/common/FrameManager.ts +++ b/packages/puppeteer-core/src/common/FrameManager.ts @@ -18,11 +18,13 @@ import {Protocol} from 'devtools-protocol'; import {Page} from '../api/Page.js'; import {assert} from '../util/assert.js'; +import {Deferred} from '../util/Deferred.js'; import {isErrorLike} from '../util/ErrorLike.js'; import { CDPSession, CDPSessionEmittedEvents, + CDPSessionImpl, isTargetClosedError, } from './Connection.js'; import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js'; @@ -112,14 +114,47 @@ export class FrameManager extends EventEmitter { this.#networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); this.#timeoutSettings = timeoutSettings; this.setupEventListeners(this.#client); - client.once(CDPSessionEmittedEvents.Disconnected, () => { + client.once(CDPSessionEmittedEvents.Disconnected, async () => { const mainFrame = this._frameTree.getMainFrame(); - if (mainFrame) { - this.#removeFramesRecursively(mainFrame); + if (!mainFrame) { + return; + } + const swapped = Deferred.create({ + timeout: 100, + message: 'Frame was not swapped', + }); + mainFrame.once(FrameEmittedEvents.FrameSwappedByActivation, () => { + swapped.resolve(); + }); + try { + await swapped.valueOrThrow(); + for (const child of mainFrame.childFrames()) { + this.#removeFramesRecursively(child); + } + } catch (err) { + if (mainFrame) { + this.#removeFramesRecursively(mainFrame); + } } }); } + async swapFrameTree(client: CDPSession): Promise { + this.#onExecutionContextsCleared(this.#client); + + this.#client = client; + const frame = this._frameTree.getMainFrame(); + if (frame) { + this._frameTree.removeFrame(frame); + frame.updateId((this.#client as CDPSessionImpl)._target()!._targetId); + } + this.setupEventListeners(client); + await this.initialize(client); + if (frame) { + frame.emit(FrameEmittedEvents.FrameSwappedByActivation); + } + } + private setupEventListeners(session: CDPSession) { session.on('Page.frameAttached', event => { this.#onFrameAttached(session, event.frameId, event.parentFrameId); diff --git a/packages/puppeteer-core/src/common/LifecycleWatcher.ts b/packages/puppeteer-core/src/common/LifecycleWatcher.ts index 8ed77393140b5..7e85c31f3cfd1 100644 --- a/packages/puppeteer-core/src/common/LifecycleWatcher.ts +++ b/packages/puppeteer-core/src/common/LifecycleWatcher.ts @@ -117,6 +117,11 @@ export class LifecycleWatcher { FrameEmittedEvents.FrameSwapped, this.#frameSwapped.bind(this) ), + addEventListener( + frame, + FrameEmittedEvents.FrameSwappedByActivation, + this.#frameSwappedByActivation.bind(this) + ), addEventListener( frame, FrameEmittedEvents.FrameDetached, @@ -222,6 +227,11 @@ export class LifecycleWatcher { this.#checkLifecycleComplete(); } + #frameSwappedByActivation(): void { + this.#swapped = true; + this.#checkLifecycleComplete(); + } + #checkLifecycleComplete(): void { // We expect navigation to commit. if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) { diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts index 86319543d6dc2..b85c313a86476 100644 --- a/packages/puppeteer-core/src/common/Page.ts +++ b/packages/puppeteer-core/src/common/Page.ts @@ -46,6 +46,7 @@ import {Binding} from './Binding.js'; import { CDPSession, CDPSessionEmittedEvents, + CDPSessionImpl, isTargetClosedError, } from './Connection.js'; import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js'; @@ -127,6 +128,7 @@ export class CDPPage extends Page { #closed = false; #client: CDPSession; + #tabSession: CDPSession | undefined; #target: CDPTarget; #keyboard: CDPKeyboard; #mouse: CDPMouse; @@ -289,6 +291,7 @@ export class CDPPage extends Page { ) { super(); this.#client = client; + this.#tabSession = client.parentSession(); this.#target = target; this.#keyboard = new CDPKeyboard(client); this.#mouse = new CDPMouse(client, this.#keyboard); @@ -307,6 +310,14 @@ export class CDPPage extends Page { this.#viewport = null; this.#setupEventListeners(); + + this.#tabSession?.on('sessionswapped', async newSession => { + this.#client = newSession; + this.#target = (this.#client as CDPSessionImpl)._target()!; + assert(this.#target, 'Missing target on swap'); + await this.#frameManager.swapFrameTree(newSession); + this.#setupEventListeners(); + }); } #setupEventListeners() { diff --git a/packages/puppeteer-core/src/common/Target.ts b/packages/puppeteer-core/src/common/Target.ts index 4cde895e0e393..1ea8ba97841c3 100644 --- a/packages/puppeteer-core/src/common/Target.ts +++ b/packages/puppeteer-core/src/common/Target.ts @@ -22,7 +22,7 @@ import {Page, PageEmittedEvents} from '../api/Page.js'; import {Target, TargetType} from '../api/Target.js'; import {Deferred} from '../util/Deferred.js'; -import {CDPSession} from './Connection.js'; +import {CDPSession, CDPSessionImpl} from './Connection.js'; import {CDPPage} from './Page.js'; import {Viewport} from './PuppeteerViewport.js'; import {TargetManager} from './TargetManager.js'; @@ -84,6 +84,14 @@ export class CDPTarget extends Target { this.#browserContext = browserContext; this._targetId = targetInfo.targetId; this.#sessionFactory = sessionFactory; + (this.#session as CDPSessionImpl | undefined)?._setTarget(this); + } + + /** + * @internal + */ + _subtype(): string | undefined { + return this.#targetInfo.subtype; } /** @@ -109,7 +117,10 @@ export class CDPTarget extends Target { if (!this.#sessionFactory) { throw new Error('sessionFactory is not initialized'); } - return this.#sessionFactory(false); + return this.#sessionFactory(false).then(session => { + (session as CDPSessionImpl)._setTarget(this); + return session; + }); } override url(): string { @@ -131,6 +142,8 @@ export class CDPTarget extends Target { return TargetType.BROWSER; case 'webview': return TargetType.WEBVIEW; + case 'tab': + return TargetType.TAB; default: return TargetType.OTHER; } diff --git a/packages/puppeteer-core/src/common/TargetManager.ts b/packages/puppeteer-core/src/common/TargetManager.ts index 9e3ea4ceeb0e1..73708b4fce90f 100644 --- a/packages/puppeteer-core/src/common/TargetManager.ts +++ b/packages/puppeteer-core/src/common/TargetManager.ts @@ -25,7 +25,8 @@ import {CDPTarget} from './Target.js'; */ export type TargetFactory = ( targetInfo: Protocol.Target.TargetInfo, - session?: CDPSession + session?: CDPSession, + parentSession?: CDPSession ) => CDPTarget; /** diff --git a/test/assets/prerender/index.html b/test/assets/prerender/index.html new file mode 100644 index 0000000000000..e0eecb717dac9 --- /dev/null +++ b/test/assets/prerender/index.html @@ -0,0 +1,21 @@ + + + + + + + test + diff --git a/test/assets/prerender/target.html b/test/assets/prerender/target.html new file mode 100644 index 0000000000000..469f3d87512c2 --- /dev/null +++ b/test/assets/prerender/target.html @@ -0,0 +1,4 @@ + + + +target diff --git a/test/src/launcher.spec.ts b/test/src/launcher.spec.ts index 9e36b705bad84..613defb73f94c 100644 --- a/test/src/launcher.spec.ts +++ b/test/src/launcher.spec.ts @@ -82,7 +82,7 @@ describe('Launcher specs', function () { }); remote.disconnect(); const error = await watchdog; - expect(error.message).toContain('frame got detached'); + expect(error.message).toContain('Session closed.'); } finally { await close(); } diff --git a/test/src/prerender.spec.ts b/test/src/prerender.spec.ts new file mode 100644 index 0000000000000..c1a3996c420b5 --- /dev/null +++ b/test/src/prerender.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * 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 expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Prerender', function () { + setupTestBrowserHooks(); + + it('can navigate to a prerendered page', async () => { + const {page, server} = await getTestState(); + await page.goto(server.PREFIX + '/prerender/index.html'); + + const button = await page.waitForSelector('button'); + await button?.click(); + + const link = await page.waitForSelector('a'); + await Promise.all([page.waitForNavigation(), link?.click()]); + expect( + await page.evaluate(() => { + return document.body.innerText; + }) + ).toBe('target'); + }); +});