Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/PageCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,19 @@ import {
type PageEvents as PuppeteerPageEvents,
} from './third_party/index.js';

export class UncaughtError {
readonly message: string;
readonly stackTrace?: Protocol.Runtime.StackTrace;

constructor(message: string, stackTrace?: Protocol.Runtime.StackTrace) {
this.message = message;
this.stackTrace = stackTrace;
}
}

interface PageEvents extends PuppeteerPageEvents {
issue: DevTools.AggregatedIssue;
uncaughtError: UncaughtError;
}

export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
Expand Down Expand Up @@ -272,6 +283,7 @@ class PageIssueSubscriber {
this.#resetIssueAggregator();
this.#page.on('framenavigated', this.#onFrameNavigated);
this.#session.on('Audits.issueAdded', this.#onIssueAdded);
this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
try {
await this.#session.send('Audits.enable');
} catch (error) {
Expand All @@ -284,6 +296,7 @@ class PageIssueSubscriber {
this.#seenIssues.clear();
this.#page.off('framenavigated', this.#onFrameNavigated);
this.#session.off('Audits.issueAdded', this.#onIssueAdded);
this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown);
if (this.#issueAggregator) {
this.#issueAggregator.removeEventListener(
DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED,
Expand All @@ -305,6 +318,14 @@ class PageIssueSubscriber {
this.#page.emit('issue', event.data);
};

#onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent) => {
const {exception, text, stackTrace} = event.exceptionDetails;
const messageWithRest = exception?.description?.split('\n at ', 2) ?? [];
const message = text + ' ' + (messageWithRest[0] ?? '');

this.#page.emit('uncaughtError', new UncaughtError(message, stackTrace));
};

// On navigation, we reset issue aggregation.
#onFrameNavigated = (frame: Frame) => {
// Only split the storage on main frame navigation
Expand Down
32 changes: 32 additions & 0 deletions tests/PageCollector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,36 @@ describe('ConsoleCollector', () => {
assert.equal(collectedIssue.code(), 'MixedContentIssue');
assert.equal(collectedIssue.getAggregatedIssuesCount(), 1);
});

it('emits UncaughtErrors for Runtime.exceptionThrown CDP events', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
// @ts-expect-error internal API.
const cdpSession = page._client();
const onUncaughtErrorListener = sinon.spy();
const collector = new ConsoleCollector(browser, () => {
return {
uncaughtError: onUncaughtErrorListener,
} as ListenerMap;
});
await collector.init([page]);

cdpSession.emit('Runtime.exceptionThrown', {
exceptionDetails: {
exception: {description: 'SyntaxError: Expected {'},
text: 'Uncaught',
stackTrace: {callFrames: []},
},
});

sinon.assert.calledOnceWithMatch(
onUncaughtErrorListener,
sinon.match(e => {
return (
e.message === 'Uncaught SyntaxError: Expected {' &&
e.stackTrace.callFrames.length === 0
);
}),
);
});
});