refactor: make collectors work per page (#2324)

This commit is contained in:
Alex Rudenko
2026-07-09 12:03:03 +02:00
committed by GitHub
parent 2812902ba9
commit 9bc61b43d6
5 changed files with 222 additions and 327 deletions
+80 -61
View File
@@ -25,7 +25,6 @@ import type {
import {McpPage} from './McpPage.js';
import {
NetworkCollector,
ConsoleCollector,
type ListenerMap,
type UncaughtError,
} from './PageCollector.js';
@@ -56,6 +55,7 @@ import type {
ExtensionServiceWorker,
} from './types.js';
import {getTempFilePath, resolveCanonicalPath} from './utils/files.js';
import {type WithSymbolId, stableIdSymbol} from './utils/id.js';
import {getNetworkMultiplierFromString} from './WaitForHelper.js';
interface McpContextOptions {
@@ -93,8 +93,6 @@ export class McpContext implements Context {
#mcpPages = new Map<Page, McpPage>();
#selectedPage?: McpPage;
#selectedPageFallback?: {wasClosed: boolean};
#networkCollector: NetworkCollector;
#consoleCollector: ConsoleCollector;
#devtoolsUniverseManager: UniverseManager;
#serviceWorkerConsoleCollector: ServiceWorkerConsoleCollector;
@@ -134,21 +132,6 @@ export class McpContext implements Context {
this.#options = options;
this.#allowUnrestrictedPaths = options.allowUnrestrictedPaths ?? false;
this.#networkCollector = new NetworkCollector(this.browser);
this.#consoleCollector = new ConsoleCollector(this.browser, collect => {
return {
console: event => {
collect(event);
},
uncaughtError: event => {
collect(event);
},
devtoolsAggregatedIssue: event => {
collect(event);
},
} as ListenerMap;
});
this.#serviceWorkerConsoleCollector = new ServiceWorkerConsoleCollector(
this.browser,
);
@@ -158,15 +141,15 @@ export class McpContext implements Context {
async #init() {
const pages = await this.createPagesSnapshot();
const workers = await this.createExtensionServiceWorkersSnapshot();
await this.#networkCollector.init(pages);
await this.#consoleCollector.init(pages);
await this.#devtoolsUniverseManager.init(pages);
await this.#serviceWorkerConsoleCollector.init(workers);
this.browser.on('targetcreated', this.#onTargetCreated);
this.browser.on('targetdestroyed', this.#onTargetDestroyed);
}
dispose() {
this.#networkCollector.dispose();
this.#consoleCollector.dispose();
this.browser.off('targetcreated', this.#onTargetCreated);
this.browser.off('targetdestroyed', this.#onTargetDestroyed);
this.#devtoolsUniverseManager.dispose();
this.#serviceWorkerConsoleCollector.dispose();
for (const mcpPage of this.#mcpPages.values()) {
@@ -179,6 +162,40 @@ export class McpContext implements Context {
this.#isolatedContexts.clear();
}
#onTargetCreated = async (target: Target) => {
try {
const page = await target.page();
if (!page) {
return;
}
this.#createMcpPage(page);
} catch (err) {
this.logger?.('Error handling targetcreated', err);
}
};
#onTargetDestroyed = (target: Target) => {
try {
let foundPage: Page | undefined;
for (const page of this.#mcpPages.keys()) {
if (page.target() === target) {
foundPage = page;
break;
}
}
if (!foundPage) {
return;
}
const mcpPage = this.#mcpPages.get(foundPage);
if (mcpPage) {
mcpPage.dispose();
this.#mcpPages.delete(foundPage);
}
} catch (err) {
this.logger?.('Error handling targetdestroyed', err);
}
};
static async from(
browser: Browser,
logger: Logger,
@@ -294,7 +311,7 @@ export class McpContext implements Context {
this.logger?.('no network request');
return;
}
const request = this.#networkCollector.find(page.pptrPage, request => {
const request = page.networkCollector.find(request => {
// @ts-expect-error id is internal.
return request.id === cdpRequestId;
});
@@ -302,27 +319,21 @@ export class McpContext implements Context {
this.logger?.('no network request for ' + cdpRequestId);
return;
}
return this.#networkCollector.getIdForResource(request);
return page.networkCollector.getIdForResource(request);
}
getNetworkRequests(
page: McpPage,
includePreservedRequests?: boolean,
): HTTPRequest[] {
return this.#networkCollector.getData(
page.pptrPage,
includePreservedRequests,
);
return page.networkCollector.getData(includePreservedRequests);
}
getConsoleData(
page: McpPage,
includePreservedMessages?: boolean,
): Array<ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError> {
return this.#consoleCollector.getData(
page.pptrPage,
includePreservedMessages,
);
return page.consoleCollector.getData(includePreservedMessages);
}
getDevToolsUniverse(page: McpPage): TargetUniverse | null {
@@ -332,14 +343,14 @@ export class McpContext implements Context {
getConsoleMessageStableId(
message: ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError,
): number {
return this.#consoleCollector.getIdForResource(message);
return (message as WithSymbolId<typeof message>)[stableIdSymbol] ?? -1;
}
getConsoleMessageById(
page: McpPage,
id: number,
): ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError {
return this.#consoleCollector.getById(page.pptrPage, id);
return page.consoleCollector.getById(id);
}
async newPage(
@@ -357,11 +368,10 @@ export class McpContext implements Context {
} else {
page = await this.browser.newPage({background});
}
const mcpPage = this.#createMcpPage(page);
await this.createPagesSnapshot();
this.selectPage(this.#getMcpPage(page));
this.#networkCollector.addPage(page);
this.#consoleCollector.addPage(page);
return this.#getMcpPage(page);
this.selectPage(mcpPage);
return mcpPage;
}
async closePage(pageId: number): Promise<void> {
if (this.#pages.length === 1) {
@@ -376,7 +386,7 @@ export class McpContext implements Context {
}
getNetworkRequestById(page: McpPage, reqid: number): HTTPRequest {
return this.#networkCollector.getById(page.pptrPage, reqid);
return page.networkCollector.getById(reqid);
}
async restoreEmulation(page: McpPage) {
@@ -628,7 +638,7 @@ export class McpContext implements Context {
async createExtensionServiceWorkersSnapshot(): Promise<
ExtensionServiceWorker[]
> {
const allTargets = await this.browser.targets();
const allTargets = this.browser.targets();
const serviceWorkers = allTargets.filter(target => {
return (
@@ -663,19 +673,24 @@ export class McpContext implements Context {
return this.#serviceWorkerConsoleCollector.getData(extensionId);
}
#createMcpPage(page: Page): McpPage {
let mcpPage = this.#mcpPages.get(page);
if (!mcpPage) {
mcpPage = new McpPage(page, this.#nextPageId++);
this.#mcpPages.set(page, mcpPage);
// We emulate a focused page for all pages to support multi-agent workflows.
void page.emulateFocusedPage(true).catch(error => {
this.logger?.('Error turning on focused page emulation', error);
});
}
return mcpPage;
}
async createPagesSnapshot(): Promise<Page[]> {
const {pages: allPages, isolatedContextNames} = await this.#getAllPages();
for (const page of allPages) {
let mcpPage = this.#mcpPages.get(page);
if (!mcpPage) {
mcpPage = new McpPage(page, this.#nextPageId++);
this.#mcpPages.set(page, mcpPage);
// We emulate a focused page for all pages to support multi-agent workflows.
void page.emulateFocusedPage(true).catch(error => {
this.logger?.('Error turning on focused page emulation', error);
});
}
const mcpPage = this.#createMcpPage(page);
mcpPage.isolatedContextName = isolatedContextNames.get(page);
}
@@ -877,7 +892,7 @@ export class McpContext implements Context {
}
getNetworkRequestStableId(request: HTTPRequest): number {
return this.#networkCollector.getIdForResource(request);
return (request as WithSymbolId<typeof request>)[stableIdSymbol] ?? -1;
}
waitForTextOnPage(
@@ -908,18 +923,22 @@ export class McpContext implements Context {
* We need to ignore favicon request as they make our test flaky
*/
async setUpNetworkCollectorForTesting() {
this.#networkCollector = new NetworkCollector(this.browser, collect => {
return {
request: req => {
if (req.url().includes('favicon.ico')) {
return;
}
collect(req);
for (const mcpPage of this.#mcpPages.values()) {
mcpPage.networkCollector.dispose();
mcpPage.networkCollector = new NetworkCollector(
mcpPage.pptrPage,
collect => {
return {
request: req => {
if (req.url().includes('favicon.ico')) {
return;
}
collect(req);
},
} as ListenerMap;
},
} as ListenerMap;
});
const {pages} = await this.#getAllPages();
await this.#networkCollector.init(pages);
);
}
}
async installExtension(extensionPath: string): Promise<string> {
+25
View File
@@ -5,6 +5,11 @@
*/
import {logger} from './logger.js';
import {
ConsoleCollector,
NetworkCollector,
type ListenerMap,
} from './PageCollector.js';
import {TextSnapshot} from './TextSnapshot.js';
import type {
Dialog,
@@ -61,6 +66,9 @@ export class McpPage implements ContextPage {
thirdPartyDeveloperTools: ToolGroups = [];
networkCollector: NetworkCollector;
consoleCollector: ConsoleCollector;
constructor(page: Page, id: number) {
this.pptrPage = page;
this.id = id;
@@ -68,6 +76,21 @@ export class McpPage implements ContextPage {
this.#dialog = dialog;
};
page.on('dialog', this.#dialogHandler);
this.networkCollector = new NetworkCollector(page);
this.consoleCollector = new ConsoleCollector(page, collect => {
return {
console: event => {
collect(event);
},
uncaughtError: event => {
collect(event);
},
devtoolsAggregatedIssue: event => {
collect(event);
},
} as ListenerMap;
});
}
get dialog(): Dialog | undefined {
@@ -143,6 +166,8 @@ export class McpPage implements ContextPage {
dispose(): void {
this.pptrPage.off('dialog', this.#dialogHandler);
this.networkCollector.dispose();
this.consoleCollector.dispose();
}
async executeThirdPartyDeveloperTool(
+63 -144
View File
@@ -7,7 +7,6 @@
import {FakeIssuesManager} from './devtools/DevtoolsUtils.js';
import {logger} from './logger.js';
import type {
Target,
CDPSession,
ConsoleMessage,
Protocol,
@@ -15,7 +14,6 @@ import type {
} from './third_party/index.js';
import {DevTools} from './third_party/index.js';
import {
type Browser,
type Frame,
type Handler,
type HTTPRequest,
@@ -48,11 +46,8 @@ export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
};
export class PageCollector<T> {
#browser: Browser;
#listenersInitializer: (
collector: (item: T) => void,
) => ListenerMap<PageEvents>;
#listeners = new WeakMap<Page, ListenerMap>();
protected pptrPage: Page;
#listeners?: ListenerMap<PageEvents>;
protected maxNavigationSaved = 3;
/**
@@ -60,123 +55,60 @@ export class PageCollector<T> {
* of all collected resources.
* The newer navigations come first.
*/
protected storage = new WeakMap<Page, Array<Array<WithSymbolId<T>>>>();
protected storage: Array<Array<WithSymbolId<T>>> = [[]];
constructor(
browser: Browser,
page: Page,
listeners: (collector: (item: T) => void) => ListenerMap<PageEvents>,
) {
this.#browser = browser;
this.#listenersInitializer = listeners;
}
this.pptrPage = page;
async init(pages: Page[]) {
for (const page of pages) {
this.addPage(page);
const idGenerator = createIdGenerator();
const listenerMap = listeners(value => {
const withId = value as WithSymbolId<T>;
withId[stableIdSymbol] = idGenerator();
this.storage[0].push(withId);
});
listenerMap['framenavigated'] = (frame: Frame) => {
// Only split the storage on main frame navigation
if (frame !== this.pptrPage.mainFrame()) {
return;
}
this.splitAfterNavigation();
};
for (const [name, listener] of Object.entries(listenerMap)) {
this.pptrPage.on(name, listener as Handler<unknown>);
}
this.#browser.on('targetcreated', this.#onTargetCreated);
this.#browser.on('targetdestroyed', this.#onTargetDestroyed);
this.#listeners = listenerMap;
}
dispose() {
this.#browser.off('targetcreated', this.#onTargetCreated);
this.#browser.off('targetdestroyed', this.#onTargetDestroyed);
if (this.#listeners) {
for (const [name, listener] of Object.entries(this.#listeners)) {
this.pptrPage.off(name, listener as Handler<unknown>);
}
}
}
#onTargetCreated = async (target: Target) => {
try {
const page = await target.page();
if (!page) {
return;
}
this.addPage(page);
} catch (err) {
logger?.('Error getting a page for a target onTargetCreated', err);
}
};
#onTargetDestroyed = async (target: Target) => {
try {
const page = await target.page();
if (!page) {
return;
}
this.cleanupPageDestroyed(page);
} catch (err) {
logger?.('Error getting a page for a target onTargetDestroyed', err);
}
};
public addPage(page: Page) {
this.#initializePage(page);
}
#initializePage(page: Page) {
if (this.storage.has(page)) {
return;
}
const idGenerator = createIdGenerator();
const storedLists: Array<Array<WithSymbolId<T>>> = [[]];
this.storage.set(page, storedLists);
const listeners = this.#listenersInitializer(value => {
const withId = value as WithSymbolId<T>;
withId[stableIdSymbol] = idGenerator();
const navigations = this.storage.get(page) ?? [[]];
navigations[0].push(withId);
});
listeners['framenavigated'] = (frame: Frame) => {
// Only split the storage on main frame navigation
if (frame !== page.mainFrame()) {
return;
}
this.splitAfterNavigation(page);
};
for (const [name, listener] of Object.entries(listeners)) {
page.on(name, listener as Handler<unknown>);
}
this.#listeners.set(page, listeners);
}
protected splitAfterNavigation(page: Page) {
const navigations = this.storage.get(page);
if (!navigations) {
return;
}
protected splitAfterNavigation() {
// Add the latest navigation first
navigations.unshift([]);
navigations.splice(this.maxNavigationSaved);
this.storage.unshift([]);
this.storage.splice(this.maxNavigationSaved);
}
protected cleanupPageDestroyed(page: Page) {
const listeners = this.#listeners.get(page);
if (listeners) {
for (const [name, listener] of Object.entries(listeners)) {
page.off(name, listener as Handler<unknown>);
}
}
this.storage.delete(page);
}
getData(page: Page, includePreservedData?: boolean): T[] {
const navigations = this.storage.get(page);
if (!navigations) {
return [];
}
getData(includePreservedData?: boolean): T[] {
if (!includePreservedData) {
return navigations[0];
return this.storage[0];
}
const data: T[] = [];
for (let index = this.maxNavigationSaved; index >= 0; index--) {
if (navigations[index]) {
data.push(...navigations[index]);
if (this.storage[index]) {
data.push(...this.storage[index]);
}
}
return data;
@@ -186,13 +118,8 @@ export class PageCollector<T> {
return resource[stableIdSymbol] ?? -1;
}
getById(page: Page, stableId: number): T {
const navigations = this.storage.get(page);
if (!navigations) {
throw new Error('No requests found for selected page');
}
const item = this.find(page, item => item[stableIdSymbol] === stableId);
getById(stableId: number): T {
const item = this.find(item => item[stableIdSymbol] === stableId);
if (!item) {
throw new Error('Request not found for selected page');
@@ -202,15 +129,9 @@ export class PageCollector<T> {
}
find(
page: Page,
filter: (item: WithSymbolId<T>) => boolean,
): WithSymbolId<T> | undefined {
const navigations = this.storage.get(page);
if (!navigations) {
return;
}
for (const navigation of navigations) {
for (const navigation of this.storage) {
const item = navigation.find(filter);
if (item) {
return item;
@@ -223,21 +144,24 @@ export class PageCollector<T> {
export class ConsoleCollector extends PageCollector<
ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError
> {
#subscribedPages = new WeakMap<Page, PageEventSubscriber>();
#subscriber?: PageEventSubscriber;
override addPage(page: Page): void {
super.addPage(page);
if (!this.#subscribedPages.has(page)) {
const subscriber = new PageEventSubscriber(page);
this.#subscribedPages.set(page, subscriber);
void subscriber.subscribe();
}
constructor(
page: Page,
listeners: (
collector: (
item: ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError,
) => void,
) => ListenerMap<PageEvents>,
) {
super(page, listeners);
this.#subscriber = new PageEventSubscriber(this.pptrPage);
this.#subscriber.subscribe();
}
protected override cleanupPageDestroyed(page: Page): void {
super.cleanupPageDestroyed(page);
this.#subscribedPages.get(page)?.unsubscribe();
this.#subscribedPages.delete(page);
override dispose(): void {
super.dispose();
this.#subscriber?.unsubscribe();
}
}
@@ -273,7 +197,7 @@ class PageEventSubscriber {
);
}
async subscribe() {
subscribe() {
this.#resetIssueAggregator();
this.#page.on('framenavigated', this.#onFrameNavigated);
this.#page.on('issue', this.#onIssueAdded);
@@ -360,7 +284,7 @@ class PageEventSubscriber {
export class NetworkCollector extends PageCollector<HTTPRequest> {
constructor(
browser: Browser,
page: Page,
listeners: (
collector: (item: HTTPRequest) => void,
) => ListenerMap<PageEvents> = collect => {
@@ -371,18 +295,13 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
} as ListenerMap;
},
) {
super(browser, listeners);
super(page, listeners);
}
override splitAfterNavigation(page: Page) {
const navigations = this.storage.get(page) ?? [];
if (!navigations) {
return;
}
const requests = navigations[0];
override splitAfterNavigation() {
const requests = this.storage[0];
const lastRequestIdx = requests.findLastIndex(request => {
return request.frame() === page.mainFrame()
return request.frame() === this.pptrPage.mainFrame()
? request.isNavigationRequest()
: false;
});
@@ -392,10 +311,10 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
// Keep the reference
if (lastRequestIdx !== -1) {
const fromCurrentNavigation = requests.splice(lastRequestIdx);
navigations.unshift(fromCurrentNavigation);
this.storage.unshift(fromCurrentNavigation);
} else {
navigations.unshift([]);
this.storage.unshift([]);
}
navigations.splice(this.maxNavigationSaved);
this.storage.splice(this.maxNavigationSaved);
}
}
-1
View File
@@ -212,7 +212,6 @@ describe('McpContext', () => {
response.setIncludeNetworkRequests(true);
const result = await response.handle('test', context);
t.assert.snapshot(JSON.stringify(result.structuredContent, null, 2));
});
});
+54 -121
View File
@@ -7,7 +7,7 @@
import assert from 'node:assert';
import {afterEach, beforeEach, describe, it} from 'node:test';
import type {Frame, HTTPRequest, Target, Protocol} from 'puppeteer-core';
import type {Frame, HTTPRequest, Protocol} from 'puppeteer-core';
import sinon from 'sinon';
import type {ListenerMap} from '../src/PageCollector.js';
@@ -25,17 +25,17 @@ describe('PageCollector', () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
const request = getMockRequest();
const collector = new PageCollector(browser, collect => {
const collector = new PageCollector(page, collect => {
return {
request: req => {
collect(req);
},
} as ListenerMap;
});
await collector.init([page]);
page.emit('request', request);
assert.equal(collector.getData(page)[0], request);
assert.equal(collector.getData()[0], request);
});
it('clean up after navigation', async () => {
@@ -43,38 +43,38 @@ describe('PageCollector', () => {
const page = (await browser.pages())[0];
const mainFrame = page.mainFrame();
const request = getMockRequest();
const collector = new PageCollector(browser, collect => {
const collector = new PageCollector(page, collect => {
return {
request: req => {
collect(req);
},
} as ListenerMap;
});
await collector.init([page]);
page.emit('request', request);
assert.equal(collector.getData(page)[0], request);
assert.equal(collector.getData()[0], request);
page.emit('framenavigated', mainFrame);
assert.equal(collector.getData(page).length, 0);
assert.equal(collector.getData().length, 0);
});
it('does not clean up after sub frame navigation', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
const request = getMockRequest();
const collector = new PageCollector(browser, collect => {
const collector = new PageCollector(page, collect => {
return {
request: req => {
collect(req);
},
} as ListenerMap;
});
await collector.init([page]);
page.emit('request', request);
page.emit('framenavigated', {} as Frame);
assert.equal(collector.getData(page).length, 1);
assert.equal(collector.getData().length, 1);
});
it('clean up after navigation and be able to add data after', async () => {
@@ -82,85 +82,24 @@ describe('PageCollector', () => {
const page = (await browser.pages())[0];
const mainFrame = page.mainFrame();
const request = getMockRequest();
const collector = new PageCollector(browser, collect => {
const collector = new PageCollector(page, collect => {
return {
request: req => {
collect(req);
},
} as ListenerMap;
});
await collector.init([page]);
page.emit('request', request);
assert.equal(collector.getData(page)[0], request);
assert.equal(collector.getData()[0], request);
page.emit('framenavigated', mainFrame);
assert.equal(collector.getData(page).length, 0);
assert.equal(collector.getData().length, 0);
page.emit('request', request);
assert.equal(collector.getData(page).length, 1);
});
it('should only subscribe once', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
const request = getMockRequest();
const collector = new PageCollector(browser, collect => {
return {
request: req => {
collect(req);
},
} as ListenerMap;
});
await collector.init([page]);
browser.emit('targetcreated', {
page() {
return Promise.resolve(page);
},
} as Target);
// The page inside part is async so we need to await some time
await new Promise<void>(res => res());
assert.equal(collector.getData(page).length, 0);
page.emit('request', request);
assert.equal(collector.getData(page).length, 1);
page.emit('request', request);
assert.equal(collector.getData(page).length, 2);
});
it('should clear data on page destroy', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
const request = getMockRequest();
const collector = new PageCollector(browser, collect => {
return {
request: req => {
collect(req);
},
} as ListenerMap;
});
await collector.init([page]);
page.emit('request', request);
assert.equal(collector.getData(page).length, 1);
browser.emit('targetdestroyed', {
page() {
return Promise.resolve(page);
},
} as Target);
// The page inside part is async so we need to await some time
await new Promise<void>(res => res());
assert.equal(collector.getData(page).length, 0);
assert.equal(collector.getData().length, 1);
});
it('should assign ids to requests', async () => {
@@ -168,19 +107,18 @@ describe('PageCollector', () => {
const page = (await browser.pages())[0];
const request1 = getMockRequest();
const request2 = getMockRequest();
const collector = new PageCollector<HTTPRequest>(browser, collect => {
const collector = new PageCollector<HTTPRequest>(page, collect => {
return {
request: req => {
collect(req);
},
} as ListenerMap;
});
await collector.init([page]);
page.emit('request', request1);
page.emit('request', request2);
assert.equal(collector.getData(page).length, 2);
assert.equal(collector.getData().length, 2);
assert.equal(collector.getIdForResource(request1), 1);
assert.equal(collector.getIdForResource(request2), 2);
@@ -198,23 +136,23 @@ describe('NetworkCollector', () => {
frame: page.mainFrame(),
});
const request2 = getMockRequest();
const collector = new NetworkCollector(browser);
await collector.init([page]);
const collector = new NetworkCollector(page);
page.emit('request', request);
page.emit('request', navRequest);
assert.equal(collector.getData(page)[0], request);
assert.equal(collector.getData(page)[1], navRequest);
assert.equal(collector.getData()[0], request);
assert.equal(collector.getData()[1], navRequest);
page.emit('framenavigated', mainFrame);
assert.equal(collector.getData(page).length, 1);
assert.equal(collector.getData(page)[0], navRequest);
assert.equal(collector.getData().length, 1);
assert.equal(collector.getData()[0], navRequest);
page.emit('request', request2);
assert.equal(collector.getData(page).length, 2);
assert.equal(collector.getData(page)[0], navRequest);
assert.equal(collector.getData(page)[1], request2);
assert.equal(collector.getData().length, 2);
assert.equal(collector.getData()[0], navRequest);
assert.equal(collector.getData()[1], request2);
});
it('correctly picks up after multiple back to back navigations', async () => {
@@ -231,26 +169,26 @@ describe('NetworkCollector', () => {
});
const request = getMockRequest();
const collector = new NetworkCollector(browser);
await collector.init([page]);
const collector = new NetworkCollector(page);
page.emit('request', navRequest);
assert.equal(collector.getData(page)[0], navRequest);
assert.equal(collector.getData()[0], navRequest);
page.emit('framenavigated', mainFrame);
assert.equal(collector.getData(page).length, 1);
assert.equal(collector.getData(page)[0], navRequest);
assert.equal(collector.getData().length, 1);
assert.equal(collector.getData()[0], navRequest);
page.emit('request', navRequest2);
assert.equal(collector.getData(page).length, 2);
assert.equal(collector.getData(page)[0], navRequest);
assert.equal(collector.getData(page)[1], navRequest2);
assert.equal(collector.getData().length, 2);
assert.equal(collector.getData()[0], navRequest);
assert.equal(collector.getData()[1], navRequest2);
page.emit('framenavigated', mainFrame);
assert.equal(collector.getData(page).length, 1);
assert.equal(collector.getData(page)[0], navRequest2);
assert.equal(collector.getData().length, 1);
assert.equal(collector.getData()[0], navRequest2);
page.emit('request', request);
assert.equal(collector.getData(page).length, 2);
assert.equal(collector.getData().length, 2);
});
it('works with previous navigations', async () => {
@@ -267,30 +205,29 @@ describe('NetworkCollector', () => {
});
const request = getMockRequest();
const collector = new NetworkCollector(browser);
await collector.init([page]);
const collector = new NetworkCollector(page);
page.emit('request', navRequest);
assert.equal(collector.getData(page, true).length, 1);
assert.equal(collector.getData(true).length, 1);
page.emit('framenavigated', mainFrame);
assert.equal(collector.getData(page, true).length, 1);
assert.equal(collector.getData(true).length, 1);
page.emit('request', navRequest2);
assert.equal(collector.getData(page, true).length, 2);
assert.equal(collector.getData(true).length, 2);
page.emit('framenavigated', mainFrame);
assert.equal(collector.getData(page, true).length, 2);
assert.equal(collector.getData(true).length, 2);
page.emit('request', request);
assert.equal(collector.getData(page, true).length, 3);
assert.equal(collector.getData(true).length, 3);
});
it('should not grow beyond maxNavigationSaved', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
const mainFrame = page.mainFrame();
const collector = new NetworkCollector(browser);
await collector.init([page]);
const collector = new NetworkCollector(page);
// Simulate 5 navigations (maxNavigationSaved is 3)
for (let i = 0; i < 5; i++) {
@@ -305,7 +242,7 @@ describe('NetworkCollector', () => {
// We expect 3 arrays in navigations (current + 2 saved)
// Each navigation has 1 request, so total should be 3
assert.equal(collector.getData(page, true).length, 3);
assert.equal(collector.getData(true).length, 3);
});
});
@@ -332,14 +269,13 @@ describe('ConsoleCollector', () => {
it('collects issues', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
const collector = new ConsoleCollector(browser, collect => {
const collector = new ConsoleCollector(page, collect => {
return {
devtoolsAggregatedIssue: issue => {
collect(issue);
},
} as ListenerMap;
});
await collector.init([page]);
const issue2 = {
code: 'ElementAccessibilityIssue' as const,
@@ -354,7 +290,7 @@ describe('ConsoleCollector', () => {
page.emit('issue', issue);
page.emit('issue', issue2);
const data = collector.getData(page);
const data = collector.getData();
assert.equal(data.length, 2);
});
@@ -363,14 +299,13 @@ describe('ConsoleCollector', () => {
const page = (await browser.pages())[0];
const warnStub = sinon.stub(console, 'warn');
const collector = new ConsoleCollector(browser, collect => {
const collector = new ConsoleCollector(page, collect => {
return {
devtoolsAggregatedIssue: issue => {
collect(issue);
},
} as ListenerMap;
});
await collector.init([page]);
const performanceIssue = {
code: 'PerformanceIssue',
@@ -383,7 +318,7 @@ describe('ConsoleCollector', () => {
page.emit('issue', performanceIssue);
assert.equal(collector.getData(page).length, 0);
assert.equal(collector.getData().length, 0);
sinon.assert.notCalled(warnStub);
});
@@ -391,18 +326,17 @@ describe('ConsoleCollector', () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
const collector = new ConsoleCollector(browser, collect => {
const collector = new ConsoleCollector(page, collect => {
return {
devtoolsAggregatedIssue: issue => {
collect(issue);
},
} as ListenerMap;
});
await collector.init([page]);
page.emit('issue', issue);
page.emit('issue', issue);
const data = collector.getData(page);
const data = collector.getData();
assert.equal(data.length, 1);
const collectedIssue = data[0];
assert(collectedIssue instanceof DevTools.AggregatedIssue);
@@ -416,12 +350,11 @@ describe('ConsoleCollector', () => {
// @ts-expect-error internal API.
const cdpSession = page._client();
const onUncaughtErrorListener = sinon.spy();
const collector = new ConsoleCollector(browser, () => {
new ConsoleCollector(page, () => {
return {
uncaughtError: onUncaughtErrorListener,
} as ListenerMap;
});
await collector.init([page]);
cdpSession.emit('Runtime.exceptionThrown', {
exceptionDetails: {