Skip to content

Commit 3b3a1ed

Browse files
committed
refactor: clear issue aggregator on page navigation
1 parent 3b016f1 commit 3b3a1ed

File tree

3 files changed

+117
-51
lines changed

3 files changed

+117
-51
lines changed

src/PageCollector.ts

Lines changed: 72 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import type {AggregatedIssue} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
78
import {
8-
type AggregatedIssue,
99
IssueAggregatorEvents,
1010
IssuesManagerEvents,
1111
createIssuesFromProtocolIssue,
@@ -14,7 +14,11 @@ import {
1414

1515
import {FakeIssuesManager} from './DevtoolsUtils.js';
1616
import {logger} from './logger.js';
17-
import type {CDPSession, ConsoleMessage} from './third_party/index.js';
17+
import type {
18+
CDPSession,
19+
ConsoleMessage,
20+
Protocol,
21+
} from './third_party/index.js';
1822
import {
1923
type Browser,
2024
type Frame,
@@ -210,68 +214,92 @@ export class PageCollector<T> {
210214
export class ConsoleCollector extends PageCollector<
211215
ConsoleMessage | Error | AggregatedIssue
212216
> {
217+
#subscribedPages = new WeakSet<Page>();
218+
#issueManager = new FakeIssuesManager();
219+
#seenKeys = new Set<string>();
220+
#seenIssues = new Set<AggregatedIssue>();
221+
213222
override addPage(page: Page): void {
214-
const subscribed = this.storage.has(page);
215223
super.addPage(page);
216-
if (!subscribed) {
217-
void this.subscribeForIssues(page);
218-
}
224+
void this.subscribeForIssues(page);
219225
}
220226

221-
async subscribeForIssues(page: Page) {
222-
const seenKeys = new Set<string>();
223-
const mockManager = new FakeIssuesManager();
224-
const aggregator = new IssueAggregator(mockManager);
225-
aggregator.addEventListener(
227+
#resetIssueAggregator(page: Page) {
228+
this.#issueManager = new FakeIssuesManager();
229+
new IssueAggregator(this.#issueManager).addEventListener(
226230
IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED,
227231
event => {
228-
const withId = event.data as WithSymbolId<AggregatedIssue>;
229-
// Emit aggregated issue only if it's a new one
230-
if (withId[stableIdSymbol]) {
232+
if (this.#seenIssues.has(event.data)) {
231233
return;
232234
}
235+
this.#seenIssues.add(event.data);
233236
page.emit('issue', event.data);
234237
},
235238
);
239+
}
236240

241+
async subscribeForIssues(page: Page) {
242+
if (this.#subscribedPages.has(page)) {
243+
return;
244+
}
245+
this.#subscribedPages.add(page);
246+
this.#resetIssueAggregator(page);
237247
try {
238248
// @ts-expect-error use existing CDP client (internal Puppeteer API).
239249
const session = page._client() as CDPSession;
240-
session.on('Audits.issueAdded', data => {
241-
try {
242-
const inspectorIssue = data.issue;
243-
// @ts-expect-error Types of protocol from Puppeteer and CDP are
244-
// incomparable for InspectorIssueCode, one is union, other is enum.
245-
const issue = createIssuesFromProtocolIssue(null, inspectorIssue)[0];
246-
if (!issue) {
247-
logger('No issue mapping for for the issue: ', inspectorIssue.code);
248-
return;
249-
}
250-
251-
const primaryKey = issue.primaryKey();
252-
if (seenKeys.has(primaryKey)) {
253-
return;
254-
}
255-
seenKeys.add(primaryKey);
256-
257-
mockManager.dispatchEventToListeners(
258-
IssuesManagerEvents.ISSUE_ADDED,
259-
{
260-
issue,
261-
// @ts-expect-error We don't care that issues model is null
262-
issuesModel: null,
263-
},
264-
);
265-
} catch (error) {
266-
logger('Error creating a new issue', error);
267-
}
268-
});
269-
250+
page.on('framenavigated', this.#onFrameNavigated);
251+
session.on('Audits.issueAdded', this.#onIssueAdded);
270252
await session.send('Audits.enable');
271253
} catch (error) {
272254
logger('Error subscribing to issues', error);
273255
}
274256
}
257+
258+
// On navigation, we reset issue aggregation.
259+
#onFrameNavigated = (frame: Frame) => {
260+
// Only split the storage on main frame navigation
261+
if (frame !== frame.page().mainFrame()) {
262+
return;
263+
}
264+
this.#seenKeys.clear();
265+
this.#seenIssues.clear();
266+
this.#resetIssueAggregator(frame.page());
267+
};
268+
269+
#onIssueAdded = (data: Protocol.Audits.IssueAddedEvent) => {
270+
try {
271+
const inspectorIssue = data.issue;
272+
// @ts-expect-error Types of protocol from Puppeteer and CDP are
273+
// incomparable for InspectorIssueCode, one is union, other is enum.
274+
const issue = createIssuesFromProtocolIssue(null, inspectorIssue)[0];
275+
if (!issue) {
276+
logger('No issue mapping for for the issue: ', inspectorIssue.code);
277+
return;
278+
}
279+
280+
const primaryKey = issue.primaryKey();
281+
if (this.#seenKeys.has(primaryKey)) {
282+
return;
283+
}
284+
this.#seenKeys.add(primaryKey);
285+
this.#issueManager.dispatchEventToListeners(
286+
IssuesManagerEvents.ISSUE_ADDED,
287+
{
288+
issue,
289+
// @ts-expect-error We don't care that issues model is null
290+
issuesModel: null,
291+
},
292+
);
293+
} catch (error) {
294+
logger('Error creating a new issue', error);
295+
}
296+
};
297+
298+
protected override cleanupPageDestroyed(page: Page): void {
299+
super.cleanupPageDestroyed(page);
300+
this.#subscribedPages.delete(page);
301+
page.off('framenavigated', this.#onFrameNavigated);
302+
}
275303
}
276304

277305
export class NetworkCollector extends PageCollector<HTTPRequest> {

tests/PageCollector.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {
1212
HTTPRequest,
1313
Page,
1414
Target,
15-
CDPSession,
1615
Protocol,
1716
} from 'puppeteer-core';
1817
import sinon from 'sinon';
@@ -60,9 +59,6 @@ function getMockPage(): Page {
6059
mainFrame() {
6160
return mainFrame;
6261
},
63-
createCDPSession() {
64-
return Promise.resolve(cdpSession as unknown as CDPSession);
65-
},
6662
...mockListener(),
6763
// @ts-expect-error internal API.
6864
_client() {

tests/tools/console.test.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,14 @@ describe('console', () => {
4141
});
4242
});
4343

44-
it('lists issues messages', async () => {
44+
it('lists issues', async () => {
4545
await withBrowser(async (response, context) => {
4646
const page = await context.newPage();
4747
const issuePromise = new Promise<void>(resolve => {
48-
page.on('issue', () => {
48+
page.once('issue', () => {
4949
resolve();
5050
});
5151
});
52-
5352
await page.setContent('<input type="text" name="username" />');
5453
await issuePromise;
5554
await listConsoleMessages.handler({params: {}}, response, context);
@@ -63,6 +62,49 @@ describe('console', () => {
6362
});
6463
});
6564

65+
it('lists issues after a page reload', async () => {
66+
await withBrowser(async (response, context) => {
67+
const page = await context.newPage();
68+
const issuePromise = new Promise<void>(resolve => {
69+
page.once('issue', () => {
70+
resolve();
71+
});
72+
});
73+
74+
await page.setContent('<input type="text" name="username" />');
75+
await issuePromise;
76+
await listConsoleMessages.handler({params: {}}, response, context);
77+
{
78+
const formattedResponse = await response.handle('test', context);
79+
const textContent = formattedResponse[0] as {text: string};
80+
assert.ok(
81+
textContent.text.includes(
82+
`msgid=1 [issue] An element doesn't have an autocomplete attribute (count: 1)`,
83+
),
84+
);
85+
}
86+
87+
const anotherIssuePromise = new Promise<void>(resolve => {
88+
page.once('issue', () => {
89+
resolve();
90+
});
91+
});
92+
await page.reload();
93+
await page.setContent('<input type="text" name="username" />');
94+
await anotherIssuePromise;
95+
{
96+
const formattedResponse = await response.handle('test', context);
97+
const textContent = formattedResponse[0] as {text: string};
98+
console.log(textContent.text);
99+
assert.ok(
100+
textContent.text.includes(
101+
`msgid=2 [issue] An element doesn't have an autocomplete attribute (count: 1)`,
102+
),
103+
);
104+
}
105+
});
106+
});
107+
66108
it('work with primitive unhandled errors', async () => {
67109
await withBrowser(async (response, context) => {
68110
const page = await context.newPage();

0 commit comments

Comments
 (0)