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 > = {
@@ -48,11 +54,10 @@ export class PageCollector<T> {
4854 #maxNavigationSaved = 3 ;
4955 #includeAllPages?: boolean ;
5056
51- /**
52- * This maps a Page to a list of navigations with a sub-list
53- * of all collected resources.
54- * The newer navigations come first.
55- */
57+ // Store an aggregator and a mock manager for each page.
58+ #issuesAggregators = new WeakMap < Page , IssueAggregator > ( ) ;
59+ #mockIssuesManagers = new WeakMap < Page , FakeIssuesManager > ( ) ;
60+
5661 protected storage = new WeakMap < Page , Array < Array < WithSymbolId < T > > > > ( ) ;
5762
5863 constructor (
@@ -95,18 +100,31 @@ export class PageCollector<T> {
95100 }
96101
97102 async #initializePage( page : Page ) {
98- await this . subscribeForIssues ( page ) ;
99103 const idGenerator = createIdGenerator ( ) ;
100104 const storedLists : Array < Array < WithSymbolId < T > > > = [ [ ] ] ;
101105 this . storage . set ( page , storedLists ) ;
102106
103- const listeners = this . #listenersInitializer( value => {
107+ // This is the single function responsible for adding items to storage.
108+ const collector = ( value : T ) => {
104109 const withId = value as WithSymbolId < T > ;
105- withId [ stableIdSymbol ] = idGenerator ( ) ;
110+ // Assign an ID only if it's a new item.
111+ if ( ! withId [ stableIdSymbol ] ) {
112+ withId [ stableIdSymbol ] = idGenerator ( ) ;
113+ }
106114
107115 const navigations = this . storage . get ( page ) ?? [ [ ] ] ;
108- navigations [ 0 ] . push ( withId ) ;
109- } ) ;
116+ const currentNavigation = navigations [ 0 ] ;
117+
118+ // The aggregator sends the same object instance for updates, so we just
119+ // need to ensure it's in the list.
120+ if ( ! currentNavigation . includes ( withId ) ) {
121+ currentNavigation . push ( withId ) ;
122+ }
123+ } ;
124+
125+ await this . subscribeForIssues ( page ) ;
126+
127+ const listeners = this . #listenersInitializer( collector ) ;
110128
111129 listeners [ 'framenavigated' ] = ( frame : Frame ) => {
112130 // Only split the storage on main frame navigation
@@ -124,22 +142,44 @@ export class PageCollector<T> {
124142 }
125143
126144 protected async subscribeForIssues ( page : Page ) {
127- if ( this instanceof NetworkCollector ) return ;
145+ if ( this instanceof NetworkCollector ) {
146+ return ;
147+ }
128148 if ( ! this . #seenIssueKeys. has ( page ) ) {
129149 this . #seenIssueKeys. set ( page , new Set ( ) ) ;
130150 }
151+
152+ const mockManager = new FakeIssuesManager ( ) ;
153+ // @ts -expect-error Aggregator receives partial IssuesManager
154+ const aggregator = new IssueAggregator ( mockManager ) ;
155+ this . #mockIssuesManagers. set ( page , mockManager ) ;
156+ this . #issuesAggregators. set ( page , aggregator ) ;
157+
158+ aggregator . addEventListener (
159+ AggregatorEvents . AGGREGATED_ISSUE_UPDATED ,
160+ event => {
161+ page . emit ( 'issue' , event . data ) ;
162+ } ,
163+ ) ;
164+
131165 const session = await page . createCDPSession ( ) ;
132- session . on ( 'Audits.issueAdded' , data => { // TODO unsubscribe
133- // @ts -expect-error Types of protocol from Puppeteer and CDP are incopatible for Issues
134- const issue = IssuesManager . createIssuesFromProtocolIssue ( null , data . issue ) [ 0 ] ; // returns issue wrapped in array, need to get first element
166+ session . on ( 'Audits.issueAdded' , data => {
167+ // @ts -expect-error Types of protocol from Puppeteer and CDP are incopatible for Issues but it's the same type
168+ const issue = IssuesManager . createIssuesFromProtocolIssue ( null , data . issue , ) [ 0 ] ;
135169 if ( ! issue ) {
136170 return ;
137171 }
138172 const seenKeys = this . #seenIssueKeys. get ( page ) ! ;
139173 const primaryKey = issue . primaryKey ( ) ;
140174 if ( seenKeys . has ( primaryKey ) ) return ;
141175 seenKeys . add ( primaryKey ) ;
142- page . emit ( 'issue' , issue ) ;
176+
177+ // Trigger the aggregator via our mock manager. Do NOT call collector() here.
178+ const mockManager = this . #mockIssuesManagers. get ( page ) ;
179+ if ( mockManager ) {
180+ // @ts -expect-error we don't care about issies model being null
181+ mockManager . dispatchEventToListeners ( IssuesManager . Events . ISSUE_ADDED , { issue, issuesModel : null } ) ;
182+ }
143183 } ) ;
144184 await session . send ( 'Audits.enable' ) ;
145185 }
@@ -149,7 +189,6 @@ export class PageCollector<T> {
149189 if ( ! navigations ) {
150190 return ;
151191 }
152- // Add the latest navigation first
153192 navigations . unshift ( [ ] ) ;
154193 navigations . splice ( this . #maxNavigationSaved) ;
155194 }
@@ -163,6 +202,8 @@ export class PageCollector<T> {
163202 }
164203 this . storage . delete ( page ) ;
165204 this . #seenIssueKeys. delete ( page ) ;
205+ this . #issuesAggregators. delete ( page ) ;
206+ this . #mockIssuesManagers. delete ( page ) ;
166207 }
167208
168209 getData ( page : Page , includePreservedData ?: boolean ) : T [ ] {
@@ -176,7 +217,6 @@ export class PageCollector<T> {
176217 }
177218
178219 const data : T [ ] = [ ] ;
179-
180220 for ( let index = this . #maxNavigationSaved; index >= 0 ; index -- ) {
181221 if ( navigations [ index ] ) {
182222 data . push ( ...navigations [ index ] ) ;
@@ -253,14 +293,11 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
253293 : false ;
254294 } ) ;
255295
256- // Keep all requests since the last navigation request including that
257- // navigation request itself.
258- // Keep the reference
259296 if ( lastRequestIdx !== - 1 ) {
260297 const fromCurrentNavigation = requests . splice ( lastRequestIdx ) ;
261298 navigations . unshift ( fromCurrentNavigation ) ;
262299 } else {
263300 navigations . unshift ( [ ] ) ;
264301 }
265302 }
266- }
303+ }
0 commit comments