refactor: turn Frame into EventEmitter (#10711)

This commit is contained in:
Alex Rudenko 2023-08-08 16:42:45 +02:00 committed by GitHub
parent 854d488693
commit f70048c84f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 72 additions and 64 deletions

View File

@ -11,9 +11,11 @@ To understand frames, you can think of frames as `<iframe>` elements. Just like
#### Signature: #### Signature:
```typescript ```typescript
export declare class Frame export declare class Frame extends EventEmitter
``` ```
**Extends:** [EventEmitter](./puppeteer.eventemitter.md)
## Remarks ## Remarks
Frame lifecycles are controlled by three events that are all dispatched on the parent [page](./puppeteer.frame.page.md): Frame lifecycles are controlled by three events that are all dispatched on the parent [page](./puppeteer.frame.page.md):

View File

@ -19,6 +19,7 @@ import {HTTPResponse} from '../api/HTTPResponse.js';
import {Page, WaitTimeoutOptions} from '../api/Page.js'; import {Page, WaitTimeoutOptions} from '../api/Page.js';
import {CDPSession} from '../common/Connection.js'; import {CDPSession} from '../common/Connection.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js'; import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {ExecutionContext} from '../common/ExecutionContext.js'; import {ExecutionContext} from '../common/ExecutionContext.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js'; import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import { import {
@ -223,7 +224,7 @@ export interface FrameAddStyleTagOptions {
* *
* @public * @public
*/ */
export class Frame { export class Frame extends EventEmitter {
/** /**
* @internal * @internal
*/ */
@ -251,7 +252,9 @@ export class Frame {
/** /**
* @internal * @internal
*/ */
constructor() {} constructor() {
super();
}
/** /**
* The page associated with the frame. * The page associated with the frame.

View File

@ -37,6 +37,20 @@ import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js'; import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js';
import {withSourcePuppeteerURLIfNone} from './util.js'; import {withSourcePuppeteerURLIfNone} from './util.js';
/**
* We use symbols to prevent external parties listening to these events.
* They are internal to Puppeteer.
*
* @internal
*/
export const FrameEmittedEvents = {
FrameNavigated: Symbol('Frame.FrameNavigated'),
FrameSwapped: Symbol('Frame.FrameSwapped'),
LifecycleEvent: Symbol('Frame.LifecycleEvent'),
FrameNavigatedWithinDocument: Symbol('Frame.FrameNavigatedWithinDocument'),
FrameDetached: Symbol('Frame.FrameDetached'),
};
/** /**
* @internal * @internal
*/ */
@ -106,7 +120,7 @@ export class Frame extends BaseFrame {
let ensureNewDocumentNavigation = false; let ensureNewDocumentNavigation = false;
const watcher = new LifecycleWatcher( const watcher = new LifecycleWatcher(
this._frameManager, this._frameManager.networkManager,
this, this,
waitUntil, waitUntil,
timeout timeout
@ -180,7 +194,7 @@ export class Frame extends BaseFrame {
timeout = this._frameManager.timeoutSettings.navigationTimeout(), timeout = this._frameManager.timeoutSettings.navigationTimeout(),
} = options; } = options;
const watcher = new LifecycleWatcher( const watcher = new LifecycleWatcher(
this._frameManager, this._frameManager.networkManager,
this, this,
waitUntil, waitUntil,
timeout timeout

View File

@ -20,12 +20,15 @@ import {Page} from '../api/Page.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession, isTargetClosedError} from './Connection.js'; import {
CDPSession,
CDPSessionEmittedEvents,
isTargetClosedError,
} from './Connection.js';
import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js'; import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js'; import {Frame, FrameEmittedEvents} from './Frame.js';
import {Frame as CDPFrame} from './Frame.js';
import {FrameTree} from './FrameTree.js'; import {FrameTree} from './FrameTree.js';
import {IsolatedWorld} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
@ -54,8 +57,6 @@ export const FrameManagerEmittedEvents = {
FrameNavigatedWithinDocument: Symbol( FrameNavigatedWithinDocument: Symbol(
'FrameManager.FrameNavigatedWithinDocument' 'FrameManager.FrameNavigatedWithinDocument'
), ),
ExecutionContextCreated: Symbol('FrameManager.ExecutionContextCreated'),
ExecutionContextDestroyed: Symbol('FrameManager.ExecutionContextDestroyed'),
}; };
/** /**
@ -111,6 +112,12 @@ export class FrameManager extends EventEmitter {
this.#networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); this.#networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this.#timeoutSettings = timeoutSettings; this.#timeoutSettings = timeoutSettings;
this.setupEventListeners(this.#client); this.setupEventListeners(this.#client);
client.once(CDPSessionEmittedEvents.Disconnected, () => {
const mainFrame = this._frameTree.getMainFrame();
if (mainFrame) {
this.#removeFramesRecursively(mainFrame);
}
});
} }
private setupEventListeners(session: CDPSession) { private setupEventListeners(session: CDPSession) {
@ -248,6 +255,7 @@ export class FrameManager extends EventEmitter {
} }
frame._onLifecycleEvent(event.loaderId, event.name); frame._onLifecycleEvent(event.loaderId, event.name);
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
frame.emit(FrameEmittedEvents.LifecycleEvent);
} }
#onFrameStartedLoading(frameId: string): void { #onFrameStartedLoading(frameId: string): void {
@ -265,6 +273,7 @@ export class FrameManager extends EventEmitter {
} }
frame._onLoadingStopped(); frame._onLoadingStopped();
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
frame.emit(FrameEmittedEvents.LifecycleEvent);
} }
#handleFrameTree( #handleFrameTree(
@ -309,7 +318,7 @@ export class FrameManager extends EventEmitter {
return; return;
} }
frame = new CDPFrame(this, frameId, parentFrameId, session); frame = new Frame(this, frameId, parentFrameId, session);
this._frameTree.addFrame(frame); this._frameTree.addFrame(frame);
this.emit(FrameManagerEmittedEvents.FrameAttached, frame); this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
} }
@ -335,7 +344,7 @@ export class FrameManager extends EventEmitter {
frame._id = frameId; frame._id = frameId;
} else { } else {
// Initial main frame navigation. // Initial main frame navigation.
frame = new CDPFrame(this, frameId, undefined, this.#client); frame = new Frame(this, frameId, undefined, this.#client);
} }
this._frameTree.addFrame(frame); this._frameTree.addFrame(frame);
} }
@ -343,6 +352,7 @@ export class FrameManager extends EventEmitter {
frame = await this._frameTree.waitForFrame(frameId); frame = await this._frameTree.waitForFrame(frameId);
frame._navigated(framePayload); frame._navigated(framePayload);
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
frame.emit(FrameEmittedEvents.FrameNavigated);
} }
async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> { async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> {
@ -385,7 +395,9 @@ export class FrameManager extends EventEmitter {
} }
frame._navigatedWithinDocument(url); frame._navigatedWithinDocument(url);
this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame); this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame);
frame.emit(FrameEmittedEvents.FrameNavigatedWithinDocument);
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
frame.emit(FrameEmittedEvents.FrameNavigated);
} }
#onFrameDetached( #onFrameDetached(
@ -402,6 +414,7 @@ export class FrameManager extends EventEmitter {
} }
} else if (reason === 'swap') { } else if (reason === 'swap') {
this.emit(FrameManagerEmittedEvents.FrameSwapped, frame); this.emit(FrameManagerEmittedEvents.FrameSwapped, frame);
frame?.emit(FrameEmittedEvents.FrameSwapped);
} }
} }
@ -478,5 +491,6 @@ export class FrameManager extends EventEmitter {
frame._detach(); frame._detach();
this._frameTree.removeFrame(frame); this._frameTree.removeFrame(frame);
this.emit(FrameManagerEmittedEvents.FrameDetached, frame); this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
frame.emit(FrameEmittedEvents.FrameDetached, frame);
} }
} }

View File

@ -293,7 +293,7 @@ export class IsolatedWorld implements Realm {
await setPageContent(this, html); await setPageContent(this, html);
const watcher = new LifecycleWatcher( const watcher = new LifecycleWatcher(
this.#frameManager, this.#frameManager.networkManager,
this.#frame, this.#frame,
waitUntil, waitUntil,
timeout timeout

View File

@ -18,12 +18,10 @@ import {HTTPResponse} from '../api/HTTPResponse.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
import {CDPSessionEmittedEvents} from './Connection.js';
import {TimeoutError} from './Errors.js'; import {TimeoutError} from './Errors.js';
import {Frame} from './Frame.js'; import {Frame, FrameEmittedEvents} from './Frame.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
import {NetworkManagerEmittedEvents} from './NetworkManager.js'; import {NetworkManager, NetworkManagerEmittedEvents} from './NetworkManager.js';
import { import {
addEventListener, addEventListener,
PuppeteerEventListener, PuppeteerEventListener,
@ -62,7 +60,6 @@ const puppeteerToProtocolLifecycle = new Map<
*/ */
export class LifecycleWatcher { export class LifecycleWatcher {
#expectedLifecycle: ProtocolLifeCycleEvent[]; #expectedLifecycle: ProtocolLifeCycleEvent[];
#frameManager: FrameManager;
#frame: Frame; #frame: Frame;
#timeout: number; #timeout: number;
#navigationRequest: HTTPRequest | null = null; #navigationRequest: HTTPRequest | null = null;
@ -80,7 +77,7 @@ export class LifecycleWatcher {
#navigationResponseReceived?: Deferred<void>; #navigationResponseReceived?: Deferred<void>;
constructor( constructor(
frameManager: FrameManager, networkManager: NetworkManager,
frame: Frame, frame: Frame,
waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[], waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[],
timeout: number timeout: number
@ -97,55 +94,46 @@ export class LifecycleWatcher {
return protocolEvent as ProtocolLifeCycleEvent; return protocolEvent as ProtocolLifeCycleEvent;
}); });
this.#frameManager = frameManager;
this.#frame = frame; this.#frame = frame;
this.#timeout = timeout; this.#timeout = timeout;
this.#eventListeners = [ this.#eventListeners = [
addEventListener( addEventListener(
frameManager.client, frame,
CDPSessionEmittedEvents.Disconnected, FrameEmittedEvents.LifecycleEvent,
this.#terminate.bind(
this,
new Error('Navigation failed because browser has disconnected!')
)
),
addEventListener(
this.#frameManager,
FrameManagerEmittedEvents.LifecycleEvent,
this.#checkLifecycleComplete.bind(this) this.#checkLifecycleComplete.bind(this)
), ),
addEventListener( addEventListener(
this.#frameManager, frame,
FrameManagerEmittedEvents.FrameNavigatedWithinDocument, FrameEmittedEvents.FrameNavigatedWithinDocument,
this.#navigatedWithinDocument.bind(this) this.#navigatedWithinDocument.bind(this)
), ),
addEventListener( addEventListener(
this.#frameManager, frame,
FrameManagerEmittedEvents.FrameNavigated, FrameEmittedEvents.FrameNavigated,
this.#navigated.bind(this) this.#navigated.bind(this)
), ),
addEventListener( addEventListener(
this.#frameManager, frame,
FrameManagerEmittedEvents.FrameSwapped, FrameEmittedEvents.FrameSwapped,
this.#frameSwapped.bind(this) this.#frameSwapped.bind(this)
), ),
addEventListener( addEventListener(
this.#frameManager, frame,
FrameManagerEmittedEvents.FrameDetached, FrameEmittedEvents.FrameDetached,
this.#onFrameDetached.bind(this) this.#onFrameDetached.bind(this)
), ),
addEventListener( addEventListener(
this.#frameManager.networkManager, networkManager,
NetworkManagerEmittedEvents.Request, NetworkManagerEmittedEvents.Request,
this.#onRequest.bind(this) this.#onRequest.bind(this)
), ),
addEventListener( addEventListener(
this.#frameManager.networkManager, networkManager,
NetworkManagerEmittedEvents.Response, NetworkManagerEmittedEvents.Response,
this.#onResponse.bind(this) this.#onResponse.bind(this)
), ),
addEventListener( addEventListener(
this.#frameManager.networkManager, networkManager,
NetworkManagerEmittedEvents.RequestFailed, NetworkManagerEmittedEvents.RequestFailed,
this.#onRequestFailed.bind(this) this.#onRequestFailed.bind(this)
), ),
@ -204,10 +192,6 @@ export class LifecycleWatcher {
return this.#navigationRequest ? this.#navigationRequest.response() : null; return this.#navigationRequest ? this.#navigationRequest.response() : null;
} }
#terminate(error: Error): void {
this.#terminationDeferred.resolve(error);
}
sameDocumentNavigationPromise(): Promise<Error | undefined> { sameDocumentNavigationPromise(): Promise<Error | undefined> {
return this.#sameDocumentNavigationDeferred.valueOrThrow(); return this.#sameDocumentNavigationDeferred.valueOrThrow();
} }
@ -224,25 +208,16 @@ export class LifecycleWatcher {
return this.#terminationDeferred.valueOrThrow(); return this.#terminationDeferred.valueOrThrow();
} }
#navigatedWithinDocument(frame: Frame): void { #navigatedWithinDocument(): void {
if (frame !== this.#frame) {
return;
}
this.#hasSameDocumentNavigation = true; this.#hasSameDocumentNavigation = true;
this.#checkLifecycleComplete(); this.#checkLifecycleComplete();
} }
#navigated(frame: Frame): void { #navigated(): void {
if (frame !== this.#frame) {
return;
}
this.#checkLifecycleComplete(); this.#checkLifecycleComplete();
} }
#frameSwapped(frame: Frame): void { #frameSwapped(): void {
if (frame !== this.#frame) {
return;
}
this.#swapped = true; this.#swapped = true;
this.#checkLifecycleComplete(); this.#checkLifecycleComplete();
} }

View File

@ -59,7 +59,7 @@ describe('Launcher specs', function () {
const error = await navigationPromise; const error = await navigationPromise;
expect( expect(
[ [
'Navigation failed because browser has disconnected!', 'Navigating frame was detached',
'Protocol error (Page.navigate): Target closed.', 'Protocol error (Page.navigate): Target closed.',
].includes(error.message) ].includes(error.message)
).toBeTruthy(); ).toBeTruthy();
@ -82,7 +82,7 @@ describe('Launcher specs', function () {
}); });
remote.disconnect(); remote.disconnect();
const error = await watchdog; const error = await watchdog;
expect(error.message).toContain('Protocol error'); expect(error.message).toContain('frame got detached');
} finally { } finally {
await close(); await close();
} }
@ -766,11 +766,6 @@ describe('Launcher specs', function () {
const pages = await remoteBrowser.pages(); const pages = await remoteBrowser.pages();
await page2.close();
await page1.close();
remoteBrowser.disconnect();
await browser.close();
expect( expect(
pages pages
.map((p: Page) => { .map((p: Page) => {
@ -778,6 +773,11 @@ describe('Launcher specs', function () {
}) })
.sort() .sort()
).toEqual(['about:blank', server.EMPTY_PAGE]); ).toEqual(['about:blank', server.EMPTY_PAGE]);
await page2.close();
await page1.close();
remoteBrowser.disconnect();
await browser.close();
} finally { } finally {
await close(); await close();
} }