44 * SPDX-License-Identifier: Apache-2.0
55 */
66
7- import { IssuesManager , Issue } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js' ;
7+ import {
8+ type AggregatedIssue ,
9+ AggregatorEvents ,
10+ IssuesManager ,
11+ IssueAggregator ,
12+ } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js' ;
813
14+ import { FakeIssuesManager } from './DevtoolsUtils.js' ;
915import {
1016 type Browser ,
1117 type Frame ,
@@ -16,7 +22,7 @@ import {
1622} from './third_party/index.js' ;
1723
1824interface PageEvents extends PuppeteerPageEvents {
19- issue : Issue . Issue ;
25+ issue : AggregatedIssue ;
2026}
2127
2228export type ListenerMap < EventMap extends PageEvents = PageEvents > = {
@@ -47,11 +53,10 @@ export class PageCollector<T> {
4753 #seenIssueKeys = new WeakMap < Page , Set < string > > ( ) ;
4854 #maxNavigationSaved = 3 ;
4955
50- /**
51- * This maps a Page to a list of navigations with a sub-list
52- * of all collected resources.
53- * The newer navigations come first.
54- */
56+ // Store an aggregator and a mock manager for each page.
57+ #issuesAggregators = new WeakMap < Page , IssueAggregator > ( ) ;
58+ #mockIssuesManagers = new WeakMap < Page , FakeIssuesManager > ( ) ;
59+
5560 protected storage = new WeakMap < Page , Array < Array < WithSymbolId < T > > > > ( ) ;
5661
5762 constructor (
@@ -92,18 +97,31 @@ export class PageCollector<T> {
9297 }
9398
9499 async #initializePage( page : Page ) {
95- await this . subscribeForIssues ( page ) ;
96100 const idGenerator = createIdGenerator ( ) ;
97101 const storedLists : Array < Array < WithSymbolId < T > > > = [ [ ] ] ;
98102 this . storage . set ( page , storedLists ) ;
99103
100- const listeners = this . #listenersInitializer( value => {
104+ // This is the single function responsible for adding items to storage.
105+ const collector = ( value : T ) => {
101106 const withId = value as WithSymbolId < T > ;
102- withId [ stableIdSymbol ] = idGenerator ( ) ;
107+ // Assign an ID only if it's a new item.
108+ if ( ! withId [ stableIdSymbol ] ) {
109+ withId [ stableIdSymbol ] = idGenerator ( ) ;
110+ }
103111
104112 const navigations = this . storage . get ( page ) ?? [ [ ] ] ;
105- navigations [ 0 ] . push ( withId ) ;
106- } ) ;
113+ const currentNavigation = navigations [ 0 ] ;
114+
115+ // The aggregator sends the same object instance for updates, so we just
116+ // need to ensure it's in the list.
117+ if ( ! currentNavigation . includes ( withId ) ) {
118+ currentNavigation . push ( withId ) ;
119+ }
120+ } ;
121+
122+ await this . subscribeForIssues ( page ) ;
123+
124+ const listeners = this . #listenersInitializer( collector ) ;
107125
108126 listeners [ 'framenavigated' ] = ( frame : Frame ) => {
109127 // Only split the storage on main frame navigation
@@ -121,22 +139,44 @@ export class PageCollector<T> {
121139 }
122140
123141 protected async subscribeForIssues ( page : Page ) {
124- if ( this instanceof NetworkCollector ) return ;
142+ if ( this instanceof NetworkCollector ) {
143+ return ;
144+ }
125145 if ( ! this . #seenIssueKeys. has ( page ) ) {
126146 this . #seenIssueKeys. set ( page , new Set ( ) ) ;
127147 }
148+
149+ const mockManager = new FakeIssuesManager ( ) ;
150+ // @ts -expect-error Aggregator receives partial IssuesManager
151+ const aggregator = new IssueAggregator ( mockManager ) ;
152+ this . #mockIssuesManagers. set ( page , mockManager ) ;
153+ this . #issuesAggregators. set ( page , aggregator ) ;
154+
155+ aggregator . addEventListener (
156+ AggregatorEvents . AGGREGATED_ISSUE_UPDATED ,
157+ event => {
158+ page . emit ( 'issue' , event . data ) ;
159+ } ,
160+ ) ;
161+
128162 const session = await page . createCDPSession ( ) ;
129- session . on ( 'Audits.issueAdded' , data => { // TODO unsubscribe
130- // @ts -expect-error Types of protocol from Puppeteer and CDP are incopatible for Issues
131- const issue = IssuesManager . createIssuesFromProtocolIssue ( null , data . issue ) [ 0 ] ; // returns issue wrapped in array, need to get first element
163+ session . on ( 'Audits.issueAdded' , data => {
164+ // @ts -expect-error Types of protocol from Puppeteer and CDP are incopatible for Issues but it's the same type
165+ const issue = IssuesManager . createIssuesFromProtocolIssue ( null , data . issue , ) [ 0 ] ;
132166 if ( ! issue ) {
133167 return ;
134168 }
135169 const seenKeys = this . #seenIssueKeys. get ( page ) ! ;
136170 const primaryKey = issue . primaryKey ( ) ;
137171 if ( seenKeys . has ( primaryKey ) ) return ;
138172 seenKeys . add ( primaryKey ) ;
139- page . emit ( 'issue' , issue ) ;
173+
174+ // Trigger the aggregator via our mock manager. Do NOT call collector() here.
175+ const mockManager = this . #mockIssuesManagers. get ( page ) ;
176+ if ( mockManager ) {
177+ // @ts -expect-error we don't care about issies model being null
178+ mockManager . dispatchEventToListeners ( IssuesManager . Events . ISSUE_ADDED , { issue, issuesModel : null } ) ;
179+ }
140180 } ) ;
141181 await session . send ( 'Audits.enable' ) ;
142182 }
@@ -146,7 +186,6 @@ export class PageCollector<T> {
146186 if ( ! navigations ) {
147187 return ;
148188 }
149- // Add the latest navigation first
150189 navigations . unshift ( [ ] ) ;
151190 navigations . splice ( this . #maxNavigationSaved) ;
152191 }
@@ -160,6 +199,8 @@ export class PageCollector<T> {
160199 }
161200 this . storage . delete ( page ) ;
162201 this . #seenIssueKeys. delete ( page ) ;
202+ this . #issuesAggregators. delete ( page ) ;
203+ this . #mockIssuesManagers. delete ( page ) ;
163204 }
164205
165206 getData ( page : Page , includePreservedData ?: boolean ) : T [ ] {
@@ -173,7 +214,6 @@ export class PageCollector<T> {
173214 }
174215
175216 const data : T [ ] = [ ] ;
176-
177217 for ( let index = this . #maxNavigationSaved; index >= 0 ; index -- ) {
178218 if ( navigations [ index ] ) {
179219 data . push ( ...navigations [ index ] ) ;
@@ -249,14 +289,11 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
249289 : false ;
250290 } ) ;
251291
252- // Keep all requests since the last navigation request including that
253- // navigation request itself.
254- // Keep the reference
255292 if ( lastRequestIdx !== - 1 ) {
256293 const fromCurrentNavigation = requests . splice ( lastRequestIdx ) ;
257294 navigations . unshift ( fromCurrentNavigation ) ;
258295 } else {
259296 navigations . unshift ( [ ] ) ;
260297 }
261298 }
262- }
299+ }
0 commit comments