Skip to content

Commit 35648dd

Browse files
committed
chore: emit UncaughtError events for Runtime.exceptionThrown
1 parent 1c14c0e commit 35648dd

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

src/PageCollector.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,19 @@ import {
2222
type PageEvents as PuppeteerPageEvents,
2323
} from './third_party/index.js';
2424

25+
export class UncaughtError {
26+
readonly message: string;
27+
readonly stackTrace?: Protocol.Runtime.StackTrace;
28+
29+
constructor(message: string, stackTrace?: Protocol.Runtime.StackTrace) {
30+
this.message = message;
31+
this.stackTrace = stackTrace;
32+
}
33+
}
34+
2535
interface PageEvents extends PuppeteerPageEvents {
2636
issue: DevTools.AggregatedIssue;
37+
uncaughtError: UncaughtError;
2738
}
2839

2940
export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
@@ -272,6 +283,7 @@ class PageIssueSubscriber {
272283
this.#resetIssueAggregator();
273284
this.#page.on('framenavigated', this.#onFrameNavigated);
274285
this.#session.on('Audits.issueAdded', this.#onIssueAdded);
286+
this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
275287
try {
276288
await this.#session.send('Audits.enable');
277289
} catch (error) {
@@ -284,6 +296,7 @@ class PageIssueSubscriber {
284296
this.#seenIssues.clear();
285297
this.#page.off('framenavigated', this.#onFrameNavigated);
286298
this.#session.off('Audits.issueAdded', this.#onIssueAdded);
299+
this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown);
287300
if (this.#issueAggregator) {
288301
this.#issueAggregator.removeEventListener(
289302
DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED,
@@ -305,6 +318,14 @@ class PageIssueSubscriber {
305318
this.#page.emit('issue', event.data);
306319
};
307320

321+
#onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent) => {
322+
const {exception, text, stackTrace} = event.exceptionDetails;
323+
const messageWithRest = exception?.description?.split('\n at ', 2) ?? [];
324+
const message = text + ' ' + (messageWithRest[0] ?? '');
325+
326+
this.#page.emit('uncaughtError', new UncaughtError(message, stackTrace));
327+
};
328+
308329
// On navigation, we reset issue aggregation.
309330
#onFrameNavigated = (frame: Frame) => {
310331
// Only split the storage on main frame navigation

tests/PageCollector.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,4 +382,36 @@ describe('ConsoleCollector', () => {
382382
assert.equal(collectedIssue.code(), 'MixedContentIssue');
383383
assert.equal(collectedIssue.getAggregatedIssuesCount(), 1);
384384
});
385+
386+
it('emits UncaughtErrors for Runtime.exceptionThrown CDP events', async () => {
387+
const browser = getMockBrowser();
388+
const page = (await browser.pages())[0];
389+
// @ts-expect-error internal API.
390+
const cdpSession = page._client();
391+
const onUncaughtErrorListener = sinon.spy();
392+
const collector = new ConsoleCollector(browser, () => {
393+
return {
394+
uncaughtError: onUncaughtErrorListener,
395+
} as ListenerMap;
396+
});
397+
await collector.init([page]);
398+
399+
cdpSession.emit('Runtime.exceptionThrown', {
400+
exceptionDetails: {
401+
exception: {description: 'SyntaxError: Expected {'},
402+
text: 'Uncaught',
403+
stackTrace: {callFrames: []},
404+
},
405+
});
406+
407+
sinon.assert.calledOnceWithMatch(
408+
onUncaughtErrorListener,
409+
sinon.match(e => {
410+
return (
411+
e.message === 'Uncaught SyntaxError: Expected {' &&
412+
e.stackTrace.callFrames.length === 0
413+
);
414+
}),
415+
);
416+
});
385417
});

0 commit comments

Comments
 (0)