refactor: move console event handling (#12407)

This commit is contained in:
Alex Rudenko 2024-05-07 16:44:23 +02:00 committed by GitHub
parent bc17e339bc
commit 7712dffffe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 95 additions and 52 deletions

View File

@ -6,7 +6,7 @@
import type {Protocol} from 'devtools-protocol'; import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js'; import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js'; import type {ElementHandle} from '../api/ElementHandle.js';
import type {JSHandle} from '../api/JSHandle.js'; import type {JSHandle} from '../api/JSHandle.js';
import {EventEmitter} from '../common/EventEmitter.js'; import {EventEmitter} from '../common/EventEmitter.js';
@ -66,6 +66,7 @@ export class ExecutionContext
extends EventEmitter<{ extends EventEmitter<{
/** Emitted when this execution context is disposed. */ /** Emitted when this execution context is disposed. */
disposed: undefined; disposed: undefined;
consoleapicalled: Protocol.Runtime.ConsoleAPICalledEvent;
}> }>
implements Disposable implements Disposable
{ {
@ -98,6 +99,10 @@ export class ExecutionContext
clientEmitter.on('Runtime.executionContextsCleared', async () => { clientEmitter.on('Runtime.executionContextsCleared', async () => {
this[disposeSymbol](); this[disposeSymbol]();
}); });
clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this));
clientEmitter.on(CDPSessionEvent.Disconnected, () => {
this[disposeSymbol]();
});
} }
// Contains mapping from functions that should be bound to Puppeteer functions. // Contains mapping from functions that should be bound to Puppeteer functions.
@ -179,6 +184,13 @@ export class ExecutionContext
} }
} }
#onConsoleAPI(event: Protocol.Runtime.ConsoleAPICalledEvent): void {
if (event.executionContextId !== this._contextId) {
return;
}
this.emit('consoleapicalled', event);
}
#bindingsInstalled = false; #bindingsInstalled = false;
#puppeteerUtil?: Promise<JSHandle<PuppeteerUtil>>; #puppeteerUtil?: Promise<JSHandle<PuppeteerUtil>>;
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> { get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {

View File

@ -20,6 +20,7 @@ import type {
DeviceRequestPromptManager, DeviceRequestPromptManager,
} from './DeviceRequestPrompt.js'; } from './DeviceRequestPrompt.js';
import type {FrameManager} from './FrameManager.js'; import type {FrameManager} from './FrameManager.js';
import {FrameManagerEvent} from './FrameManagerEvents.js';
import type {IsolatedWorldChart} from './IsolatedWorld.js'; import type {IsolatedWorldChart} from './IsolatedWorld.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';
@ -74,6 +75,20 @@ export class CdpFrame extends Frame {
this._onLoadingStarted(); this._onLoadingStarted();
this._onLoadingStopped(); this._onLoadingStopped();
}); });
this.worlds[MAIN_WORLD].emitter.on(
'consoleapicalled',
this.#onMainWorldConsoleApiCalled.bind(this)
);
}
#onMainWorldConsoleApiCalled(
event: Protocol.Runtime.ConsoleAPICalledEvent
): void {
this._frameManager.emit(FrameManagerEvent.ConsoleApiCalled, [
this.worlds[MAIN_WORLD],
event,
]);
} }
/** /**

View File

@ -4,9 +4,12 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type Protocol from 'devtools-protocol';
import type {EventType} from '../common/EventEmitter.js'; import type {EventType} from '../common/EventEmitter.js';
import type {CdpFrame} from './Frame.js'; import type {CdpFrame} from './Frame.js';
import type {IsolatedWorld} from './IsolatedWorld.js';
/** /**
* We use symbols to prevent external parties listening to these events. * We use symbols to prevent external parties listening to these events.
@ -24,6 +27,7 @@ export namespace FrameManagerEvent {
export const FrameNavigatedWithinDocument = Symbol( export const FrameNavigatedWithinDocument = Symbol(
'FrameManager.FrameNavigatedWithinDocument' 'FrameManager.FrameNavigatedWithinDocument'
); );
export const ConsoleApiCalled = Symbol('FrameManager.ConsoleApiCalled');
} }
/** /**
@ -36,4 +40,9 @@ export interface FrameManagerEvents extends Record<EventType, unknown> {
[FrameManagerEvent.FrameSwapped]: CdpFrame; [FrameManagerEvent.FrameSwapped]: CdpFrame;
[FrameManagerEvent.LifecycleEvent]: CdpFrame; [FrameManagerEvent.LifecycleEvent]: CdpFrame;
[FrameManagerEvent.FrameNavigatedWithinDocument]: CdpFrame; [FrameManagerEvent.FrameNavigatedWithinDocument]: CdpFrame;
// Emitted when a new console message is logged.
[FrameManagerEvent.ConsoleApiCalled]: [
IsolatedWorld,
Protocol.Runtime.ConsoleAPICalledEvent,
];
} }

View File

@ -47,14 +47,21 @@ export interface IsolatedWorldChart {
/** /**
* @internal * @internal
*/ */
export class IsolatedWorld extends Realm { type IsolatedWorldEmitter = EventEmitter<{
#context?: ExecutionContext;
#emitter = new EventEmitter<{
// Emitted when the isolated world gets a new execution context. // Emitted when the isolated world gets a new execution context.
context: ExecutionContext; context: ExecutionContext;
// Emitted when the isolated world is disposed. // Emitted when the isolated world is disposed.
disposed: undefined; disposed: undefined;
}>(); // Emitted when a new console message is logged.
consoleapicalled: Protocol.Runtime.ConsoleAPICalledEvent;
}>;
/**
* @internal
*/
export class IsolatedWorld extends Realm {
#context?: ExecutionContext;
#emitter: IsolatedWorldEmitter = new EventEmitter();
readonly #frameOrWorker: CdpFrame | CdpWebWorker; readonly #frameOrWorker: CdpFrame | CdpWebWorker;
@ -74,17 +81,30 @@ export class IsolatedWorld extends Realm {
return this.#frameOrWorker.client; return this.#frameOrWorker.client;
} }
get emitter(): IsolatedWorldEmitter {
return this.#emitter;
}
setContext(context: ExecutionContext): void { setContext(context: ExecutionContext): void {
this.#context?.[disposeSymbol](); this.#context?.[disposeSymbol]();
context.once('disposed', () => { context.once('disposed', this.#onContextDisposed.bind(this));
context.on('consoleapicalled', this.#onContextConsoleApiCalled.bind(this));
this.#context = context;
this.#emitter.emit('context', context);
void this.taskManager.rerunAll();
}
#onContextDisposed(): void {
this.#context = undefined; this.#context = undefined;
if ('clearDocumentHandle' in this.#frameOrWorker) { if ('clearDocumentHandle' in this.#frameOrWorker) {
this.#frameOrWorker.clearDocumentHandle(); this.#frameOrWorker.clearDocumentHandle();
} }
}); }
this.#context = context;
this.#emitter.emit('context', context); #onContextConsoleApiCalled(
void this.taskManager.rerunAll(); event: Protocol.Runtime.ConsoleAPICalledEvent
): void {
this.#emitter.emit('consoleapicalled', event);
} }
hasContext(): boolean { hasContext(): boolean {
@ -94,7 +114,7 @@ export class IsolatedWorld extends Realm {
#executionContext(): ExecutionContext | undefined { #executionContext(): ExecutionContext | undefined {
if (this.disposed) { if (this.disposed) {
throw new Error( throw new Error(
`Execution context is not available in detached frame "${this.environment.url()}" (are you trying to evaluate?)` `Execution context is not available in detached frame or worker "${this.environment.url()}" (are you trying to evaluate?)`
); );
} }
return this.#context; return this.#context;
@ -226,5 +246,6 @@ export class IsolatedWorld extends Realm {
this.#context?.[disposeSymbol](); this.#context?.[disposeSymbol]();
this.#emitter.emit('disposed', undefined); this.#emitter.emit('disposed', undefined);
super[disposeSymbol](); super[disposeSymbol]();
this.#emitter.removeAllListeners();
} }
} }

View File

@ -69,6 +69,7 @@ import type {CdpFrame} from './Frame.js';
import {FrameManager} from './FrameManager.js'; import {FrameManager} from './FrameManager.js';
import {FrameManagerEvent} from './FrameManagerEvents.js'; import {FrameManagerEvent} from './FrameManagerEvents.js';
import {CdpKeyboard, CdpMouse, CdpTouchscreen} from './Input.js'; import {CdpKeyboard, CdpMouse, CdpTouchscreen} from './Input.js';
import type {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD} from './IsolatedWorlds.js'; import {MAIN_WORLD} from './IsolatedWorlds.js';
import {releaseObject} from './JSHandle.js'; import {releaseObject} from './JSHandle.js';
import type {NetworkConditions} from './NetworkManager.js'; import type {NetworkConditions} from './NetworkManager.js';
@ -216,7 +217,6 @@ export class CdpPage extends Page {
return this.emit(PageEvent.Load, undefined); return this.emit(PageEvent.Load, undefined);
}, },
], ],
['Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this)],
['Runtime.bindingCalled', this.#onBindingCalled.bind(this)], ['Runtime.bindingCalled', this.#onBindingCalled.bind(this)],
['Page.javascriptDialogOpening', this.#onDialog.bind(this)], ['Page.javascriptDialogOpening', this.#onDialog.bind(this)],
['Runtime.exceptionThrown', this.#handleException.bind(this)], ['Runtime.exceptionThrown', this.#handleException.bind(this)],
@ -249,6 +249,16 @@ export class CdpPage extends Page {
this.#frameManager.on(eventName, handler); this.#frameManager.on(eventName, handler);
} }
this.#frameManager.on(
FrameManagerEvent.ConsoleApiCalled,
([world, event]: [
IsolatedWorld,
Protocol.Runtime.ConsoleAPICalledEvent,
]) => {
this.#onConsoleAPI(world, event);
}
);
for (const [eventName, handler] of this.#networkManagerHandlers) { for (const [eventName, handler] of this.#networkManagerHandlers) {
// TODO: Remove any. // TODO: Remove any.
this.#frameManager.networkManager.on(eventName, handler as any); this.#frameManager.networkManager.on(eventName, handler as any);
@ -778,41 +788,12 @@ export class CdpPage extends Page {
); );
} }
async #onConsoleAPI( #onConsoleAPI(
world: IsolatedWorld,
event: Protocol.Runtime.ConsoleAPICalledEvent event: Protocol.Runtime.ConsoleAPICalledEvent
): Promise<void> { ): void {
if (event.executionContextId === 0) {
// DevTools protocol stores the last 1000 console messages. These
// messages are always reported even for removed execution contexts. In
// this case, they are marked with executionContextId = 0 and are
// reported upon enabling Runtime agent.
//
// Ignore these messages since:
// - there's no execution context we can use to operate with message
// arguments
// - these messages are reported before Puppeteer clients can subscribe
// to the 'console'
// page event.
//
// @see https://github.com/puppeteer/puppeteer/issues/3865
return;
}
const context = this.#frameManager.getExecutionContextById(
event.executionContextId,
this.#primaryTargetClient
);
if (!context) {
debugError(
new Error(
`ExecutionContext not found for a console message: ${JSON.stringify(
event
)}`
)
);
return;
}
const values = event.args.map(arg => { const values = event.args.map(arg => {
return context._world.createCdpHandle(arg); return world.createCdpHandle(arg);
}); });
this.#addConsoleMessage( this.#addConsoleMessage(
convertConsoleMessageLevel(event.type), convertConsoleMessageLevel(event.type),

View File

@ -5,7 +5,7 @@
*/ */
import type {Protocol} from 'devtools-protocol'; import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js'; import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import type {Realm} from '../api/Realm.js'; import type {Realm} from '../api/Realm.js';
import {TargetType} from '../api/Target.js'; import {TargetType} from '../api/Target.js';
import {WebWorker} from '../api/WebWorker.js'; import {WebWorker} from '../api/WebWorker.js';
@ -60,7 +60,7 @@ export class CdpWebWorker extends WebWorker {
new ExecutionContext(client, event.context, this.#world) new ExecutionContext(client, event.context, this.#world)
); );
}); });
this.#client.on('Runtime.consoleAPICalled', async event => { this.#world.emitter.on('consoleapicalled', async event => {
try { try {
return consoleAPICalled( return consoleAPICalled(
event.type, event.type,
@ -74,6 +74,9 @@ export class CdpWebWorker extends WebWorker {
} }
}); });
this.#client.on('Runtime.exceptionThrown', exceptionThrown); this.#client.on('Runtime.exceptionThrown', exceptionThrown);
this.#client.once(CDPSessionEvent.Disconnected, () => {
this.#world.dispose();
});
// This might fail if the target is closed before we receive all execution contexts. // This might fail if the target is closed before we receive all execution contexts.
this.#client.send('Runtime.enable').catch(debugError); this.#client.send('Runtime.enable').catch(debugError);

View File

@ -73,7 +73,9 @@ describe('Launcher specs', function () {
}); });
await remote.disconnect(); await remote.disconnect();
const error = await watchdog; const error = await watchdog;
expect(error.message).toContain('Session closed.'); expect(error.message).toContain(
'Waiting for selector `div` failed: waitForFunction failed: frame got detached.'
);
} finally { } finally {
await close(); await close();
} }

View File

@ -53,8 +53,8 @@ describe('Workers', function () {
return error; return error;
}); });
expect(error.message).atLeastOneToContain([ expect(error.message).atLeastOneToContain([
'Most likely the worker has been closed.',
'Realm already destroyed.', 'Realm already destroyed.',
'Execution context is not available in detached frame',
]); ]);
}); });
it('should report console logs', async () => { it('should report console logs', async () => {