mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
refactor: make collectors work per page (#2324)
This commit is contained in:
+80
-61
@@ -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> {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user