44 * SPDX-License-Identifier: Apache-2.0
55 */
66
7+ import {
8+ type AggregatedIssue ,
9+ IssueAggregatorEvents ,
10+ IssuesManagerEvents ,
11+ createIssuesFromProtocolIssue ,
12+ IssueAggregator ,
13+ } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js' ;
14+
15+ import { FakeIssuesManager } from './DevtoolsUtils.js' ;
16+ import { logger } from './logger.js' ;
17+ import type { ConsoleMessage , Protocol } from './third_party/index.js' ;
718import {
819 type Browser ,
920 type Frame ,
1021 type Handler ,
1122 type HTTPRequest ,
1223 type Page ,
13- type PageEvents ,
24+ type PageEvents as PuppeteerPageEvents ,
1425} from './third_party/index.js' ;
1526
27+ interface PageEvents extends PuppeteerPageEvents {
28+ issue : AggregatedIssue ;
29+ }
30+
1631export type ListenerMap < EventMap extends PageEvents = PageEvents > = {
1732 [ K in keyof EventMap ] ?: ( event : EventMap [ K ] ) => void ;
1833} ;
@@ -61,22 +76,22 @@ export class PageCollector<T> {
6176 async init ( ) {
6277 const pages = await this . #browser. pages ( this . #includeAllPages) ;
6378 for ( const page of pages ) {
64- this . #initializePage ( page ) ;
79+ this . addPage ( page ) ;
6580 }
6681
6782 this . #browser. on ( 'targetcreated' , async target => {
6883 const page = await target . page ( ) ;
6984 if ( ! page ) {
7085 return ;
7186 }
72- this . #initializePage ( page ) ;
87+ this . addPage ( page ) ;
7388 } ) ;
7489 this . #browser. on ( 'targetdestroyed' , async target => {
7590 const page = await target . page ( ) ;
7691 if ( ! page ) {
7792 return ;
7893 }
79- this . # cleanupPageDestroyed( page ) ;
94+ this . cleanupPageDestroyed ( page ) ;
8095 } ) ;
8196 }
8297
@@ -88,7 +103,6 @@ export class PageCollector<T> {
88103 if ( this . storage . has ( page ) ) {
89104 return ;
90105 }
91-
92106 const idGenerator = createIdGenerator ( ) ;
93107 const storedLists : Array < Array < WithSymbolId < T > > > = [ [ ] ] ;
94108 this . storage . set ( page , storedLists ) ;
@@ -126,7 +140,7 @@ export class PageCollector<T> {
126140 navigations . splice ( this . #maxNavigationSaved) ;
127141 }
128142
129- # cleanupPageDestroyed( page : Page ) {
143+ protected cleanupPageDestroyed ( page : Page ) {
130144 const listeners = this . #listeners. get ( page ) ;
131145 if ( listeners ) {
132146 for ( const [ name , listener ] of Object . entries ( listeners ) ) {
@@ -147,7 +161,6 @@ export class PageCollector<T> {
147161 }
148162
149163 const data : T [ ] = [ ] ;
150-
151164 for ( let index = this . #maxNavigationSaved; index >= 0 ; index -- ) {
152165 if ( navigations [ index ] ) {
153166 data . push ( ...navigations [ index ] ) ;
@@ -194,6 +207,78 @@ export class PageCollector<T> {
194207 }
195208}
196209
210+ export class ConsoleCollector extends PageCollector <
211+ ConsoleMessage | Error | AggregatedIssue
212+ > {
213+ #seenIssueKeys = new WeakMap < Page , Set < string > > ( ) ;
214+ #issuesAggregators = new WeakMap < Page , IssueAggregator > ( ) ;
215+ #mockIssuesManagers = new WeakMap < Page , FakeIssuesManager > ( ) ;
216+
217+ override addPage ( page : Page ) : void {
218+ super . addPage ( page ) ;
219+ void this . subscribeForIssues ( page ) ;
220+ }
221+ async subscribeForIssues ( page : Page ) {
222+ if ( this . #seenIssueKeys. has ( page ) ) return ;
223+
224+ this . #seenIssueKeys. set ( page , new Set ( ) ) ;
225+ const mockManager = new FakeIssuesManager ( ) ;
226+ const aggregator = new IssueAggregator ( mockManager ) ;
227+ this . #mockIssuesManagers. set ( page , mockManager ) ;
228+ this . #issuesAggregators. set ( page , aggregator ) ;
229+
230+ aggregator . addEventListener (
231+ IssueAggregatorEvents . AGGREGATED_ISSUE_UPDATED ,
232+ event => {
233+ const withId = event . data as WithSymbolId < AggregatedIssue > ;
234+ // Emit aggregated issue only if it's a new one
235+ if ( withId [ stableIdSymbol ] ) {
236+ return ;
237+ }
238+ page . emit ( 'issue' , event . data ) ;
239+ } ,
240+ ) ;
241+ try {
242+ const session = await page . createCDPSession ( ) ;
243+ session . on ( 'Audits.issueAdded' , data => {
244+ const inspectorIssue =
245+ data . issue satisfies Protocol . Audits . InspectorIssue ;
246+ // @ts -expect-error Types of protocol from Puppeteer and CDP are incomparable for InspectorIssueCode, one is union, other is enum
247+ const issue = createIssuesFromProtocolIssue ( null , inspectorIssue ) [ 0 ] ;
248+ if ( ! issue ) {
249+ logger ( 'No issue mapping for for the issue: ' , inspectorIssue . code ) ;
250+ return ;
251+ }
252+
253+ const seenKeys = this . #seenIssueKeys. get ( page ) ! ;
254+ const primaryKey = issue . primaryKey ( ) ;
255+ if ( seenKeys . has ( primaryKey ) ) return ;
256+ seenKeys . add ( primaryKey ) ;
257+
258+ const mockManager = this . #mockIssuesManagers. get ( page ) ;
259+ if ( ! mockManager ) return ;
260+
261+ mockManager . dispatchEventToListeners ( IssuesManagerEvents . ISSUE_ADDED , {
262+ issue,
263+ // @ts -expect-error We don't care that issues model is null
264+ issuesModel : null ,
265+ } ) ;
266+ } ) ;
267+
268+ await session . send ( 'Audits.enable' ) ;
269+ } catch ( e ) {
270+ logger ( 'Error subscribing to issues' , e ) ;
271+ }
272+ }
273+
274+ override cleanupPageDestroyed ( page : Page ) {
275+ super . cleanupPageDestroyed ( page ) ;
276+ this . #seenIssueKeys. delete ( page ) ;
277+ this . #issuesAggregators. delete ( page ) ;
278+ this . #mockIssuesManagers. delete ( page ) ;
279+ }
280+ }
281+
197282export class NetworkCollector extends PageCollector < HTTPRequest > {
198283 constructor (
199284 browser : Browser ,
0 commit comments