@mcp-b/chrome-devtools-mcp 2.3.0 → 2.3.1-beta.20260528050333

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/package.json +1 -1
  2. package/build/src/DevToolsConnectionAdapter.js +0 -70
  3. package/build/src/DevtoolsUtils.js +0 -290
  4. package/build/src/McpContext.js +0 -687
  5. package/build/src/McpPage.js +0 -95
  6. package/build/src/McpResponse.js +0 -588
  7. package/build/src/Mutex.js +0 -37
  8. package/build/src/PageCollector.js +0 -308
  9. package/build/src/SlimMcpResponse.js +0 -18
  10. package/build/src/WaitForHelper.js +0 -135
  11. package/build/src/bin/chrome-devtools-cli-options.js +0 -651
  12. package/build/src/bin/chrome-devtools-mcp-cli-options.js +0 -317
  13. package/build/src/bin/chrome-devtools-mcp-main.js +0 -35
  14. package/build/src/bin/chrome-devtools-mcp.js +0 -21
  15. package/build/src/bin/chrome-devtools.js +0 -185
  16. package/build/src/bin/cliDefinitions.js +0 -615
  17. package/build/src/browser.js +0 -198
  18. package/build/src/daemon/client.js +0 -152
  19. package/build/src/daemon/daemon.js +0 -206
  20. package/build/src/daemon/types.js +0 -6
  21. package/build/src/daemon/utils.js +0 -108
  22. package/build/src/formatters/ConsoleFormatter.js +0 -234
  23. package/build/src/formatters/IssueFormatter.js +0 -192
  24. package/build/src/formatters/NetworkFormatter.js +0 -215
  25. package/build/src/formatters/SnapshotFormatter.js +0 -131
  26. package/build/src/index.js +0 -202
  27. package/build/src/issue-descriptions.js +0 -39
  28. package/build/src/logger.js +0 -36
  29. package/build/src/polyfill.js +0 -7
  30. package/build/src/telemetry/ClearcutLogger.js +0 -102
  31. package/build/src/telemetry/WatchdogClient.js +0 -60
  32. package/build/src/telemetry/flagUtils.js +0 -45
  33. package/build/src/telemetry/metricUtils.js +0 -14
  34. package/build/src/telemetry/persistence.js +0 -53
  35. package/build/src/telemetry/types.js +0 -33
  36. package/build/src/telemetry/watchdog/ClearcutSender.js +0 -203
  37. package/build/src/telemetry/watchdog/main.js +0 -127
  38. package/build/src/third_party/devtools-formatter-worker.js +0 -7
  39. package/build/src/third_party/index.js +0 -26
  40. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +0 -54183
  41. package/build/src/tools/ToolDefinition.js +0 -72
  42. package/build/src/tools/categories.js +0 -24
  43. package/build/src/tools/console.js +0 -85
  44. package/build/src/tools/emulation.js +0 -55
  45. package/build/src/tools/extensions.js +0 -96
  46. package/build/src/tools/input.js +0 -368
  47. package/build/src/tools/lighthouse.js +0 -123
  48. package/build/src/tools/memory.js +0 -28
  49. package/build/src/tools/network.js +0 -120
  50. package/build/src/tools/pages.js +0 -319
  51. package/build/src/tools/performance.js +0 -190
  52. package/build/src/tools/screencast.js +0 -79
  53. package/build/src/tools/screenshot.js +0 -84
  54. package/build/src/tools/script.js +0 -119
  55. package/build/src/tools/slim/tools.js +0 -81
  56. package/build/src/tools/snapshot.js +0 -56
  57. package/build/src/tools/tools.js +0 -52
  58. package/build/src/tools/webmcp.js +0 -416
  59. package/build/src/trace-processing/parse.js +0 -84
  60. package/build/src/types.js +0 -6
  61. package/build/src/utils/ExtensionRegistry.js +0 -35
  62. package/build/src/utils/files.js +0 -19
  63. package/build/src/utils/keyboard.js +0 -296
  64. package/build/src/utils/pagination.js +0 -49
  65. package/build/src/utils/string.js +0 -36
  66. package/build/src/utils/types.js +0 -6
  67. package/build/src/version.js +0 -9
@@ -1,687 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- import fs from 'node:fs/promises';
7
- import path from 'node:path';
8
- import { UniverseManager } from './DevtoolsUtils.js';
9
- import { McpPage } from './McpPage.js';
10
- import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
11
- import { Locator } from './third_party/index.js';
12
- import { PredefinedNetworkConditions } from './third_party/index.js';
13
- import { listPages } from './tools/pages.js';
14
- import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
15
- import { ExtensionRegistry } from './utils/ExtensionRegistry.js';
16
- import { saveTemporaryFile } from './utils/files.js';
17
- import { WaitForHelper } from './WaitForHelper.js';
18
- const DEFAULT_TIMEOUT = 5_000;
19
- const NAVIGATION_TIMEOUT = 10_000;
20
- function getNetworkMultiplierFromString(condition) {
21
- const puppeteerCondition = condition;
22
- switch (puppeteerCondition) {
23
- case 'Fast 4G':
24
- return 1;
25
- case 'Slow 4G':
26
- return 2.5;
27
- case 'Fast 3G':
28
- return 5;
29
- case 'Slow 3G':
30
- return 10;
31
- }
32
- return 1;
33
- }
34
- export class McpContext {
35
- browser;
36
- logger;
37
- // Maps LLM-provided isolatedContext name → Puppeteer BrowserContext.
38
- #isolatedContexts = new Map();
39
- // Auto-generated name counter for when no name is provided.
40
- #nextIsolatedContextId = 1;
41
- #pages = [];
42
- #extensionServiceWorkers = [];
43
- #mcpPages = new Map();
44
- #selectedPage;
45
- #networkCollector;
46
- #consoleCollector;
47
- #devtoolsUniverseManager;
48
- #extensionRegistry = new ExtensionRegistry();
49
- #isRunningTrace = false;
50
- #screenRecorderData = null;
51
- #nextPageId = 1;
52
- #extensionPages = new WeakMap();
53
- #extensionServiceWorkerMap = new WeakMap();
54
- #nextExtensionServiceWorkerId = 1;
55
- #nextSnapshotId = 1;
56
- #traceResults = [];
57
- #locatorClass;
58
- #options;
59
- constructor(browser, logger, options, locatorClass) {
60
- this.browser = browser;
61
- this.logger = logger;
62
- this.#locatorClass = locatorClass;
63
- this.#options = options;
64
- this.#networkCollector = new NetworkCollector(this.browser);
65
- this.#consoleCollector = new ConsoleCollector(this.browser, (collect) => {
66
- return {
67
- console: (event) => {
68
- collect(event);
69
- },
70
- uncaughtError: (event) => {
71
- collect(event);
72
- },
73
- issue: (event) => {
74
- collect(event);
75
- },
76
- };
77
- });
78
- this.#devtoolsUniverseManager = new UniverseManager(this.browser);
79
- }
80
- async #init() {
81
- const pages = await this.createPagesSnapshot();
82
- await this.createExtensionServiceWorkersSnapshot();
83
- await this.#networkCollector.init(pages);
84
- await this.#consoleCollector.init(pages);
85
- await this.#devtoolsUniverseManager.init(pages);
86
- }
87
- dispose() {
88
- this.#networkCollector.dispose();
89
- this.#consoleCollector.dispose();
90
- this.#devtoolsUniverseManager.dispose();
91
- for (const mcpPage of this.#mcpPages.values()) {
92
- mcpPage.dispose();
93
- }
94
- this.#mcpPages.clear();
95
- // Isolated contexts are intentionally not closed here.
96
- // Either the entire browser will be closed or we disconnect
97
- // without destroying browser state.
98
- this.#isolatedContexts.clear();
99
- }
100
- static async from(browser, logger, opts,
101
- /* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */
102
- locatorClass = Locator) {
103
- const context = new McpContext(browser, logger, opts, locatorClass);
104
- await context.#init();
105
- return context;
106
- }
107
- resolveCdpRequestId(page, cdpRequestId) {
108
- if (!cdpRequestId) {
109
- this.logger('no network request');
110
- return;
111
- }
112
- const request = this.#networkCollector.find(page.pptrPage, (request) => {
113
- // @ts-expect-error id is internal.
114
- return request.id === cdpRequestId;
115
- });
116
- if (!request) {
117
- this.logger('no network request for ' + cdpRequestId);
118
- return;
119
- }
120
- return this.#networkCollector.getIdForResource(request);
121
- }
122
- resolveCdpElementId(page, cdpBackendNodeId) {
123
- if (!cdpBackendNodeId) {
124
- this.logger('no cdpBackendNodeId');
125
- return;
126
- }
127
- const snapshot = page.textSnapshot;
128
- if (!snapshot) {
129
- this.logger('no text snapshot');
130
- return;
131
- }
132
- // TODO: index by backendNodeId instead.
133
- const queue = [snapshot.root];
134
- while (queue.length) {
135
- const current = queue.pop();
136
- if (current.backendNodeId === cdpBackendNodeId) {
137
- return current.id;
138
- }
139
- for (const child of current.children) {
140
- queue.push(child);
141
- }
142
- }
143
- return;
144
- }
145
- getNetworkRequests(page, includePreservedRequests) {
146
- return this.#networkCollector.getData(page.pptrPage, includePreservedRequests);
147
- }
148
- getConsoleData(page, includePreservedMessages) {
149
- return this.#consoleCollector.getData(page.pptrPage, includePreservedMessages);
150
- }
151
- getDevToolsUniverse(page) {
152
- return this.#devtoolsUniverseManager.get(page.pptrPage);
153
- }
154
- getConsoleMessageStableId(message) {
155
- return this.#consoleCollector.getIdForResource(message);
156
- }
157
- getConsoleMessageById(page, id) {
158
- return this.#consoleCollector.getById(page.pptrPage, id);
159
- }
160
- async newPage(background, isolatedContextName) {
161
- let page;
162
- if (isolatedContextName !== undefined) {
163
- let ctx = this.#isolatedContexts.get(isolatedContextName);
164
- if (!ctx) {
165
- ctx = await this.browser.createBrowserContext();
166
- this.#isolatedContexts.set(isolatedContextName, ctx);
167
- }
168
- page = await ctx.newPage();
169
- }
170
- else {
171
- page = await this.browser.newPage({ background });
172
- }
173
- await this.createPagesSnapshot();
174
- this.selectPage(this.#getMcpPage(page));
175
- this.#networkCollector.addPage(page);
176
- this.#consoleCollector.addPage(page);
177
- return this.#getMcpPage(page);
178
- }
179
- async closePage(pageId) {
180
- if (this.#pages.length === 1) {
181
- throw new Error(CLOSE_PAGE_ERROR);
182
- }
183
- const page = this.getPageById(pageId);
184
- if (page) {
185
- page.dispose();
186
- this.#mcpPages.delete(page.pptrPage);
187
- }
188
- await page.pptrPage.close({ runBeforeUnload: false });
189
- }
190
- getNetworkRequestById(page, reqid) {
191
- return this.#networkCollector.getById(page.pptrPage, reqid);
192
- }
193
- async restoreEmulation(page) {
194
- const currentSetting = page.emulationSettings;
195
- await this.emulate(currentSetting, page.pptrPage);
196
- }
197
- async emulate(options, targetPage) {
198
- const page = targetPage ?? this.getSelectedPptrPage();
199
- const mcpPage = this.#getMcpPage(page);
200
- const newSettings = { ...mcpPage.emulationSettings };
201
- if (!options.networkConditions) {
202
- await page.emulateNetworkConditions(null);
203
- delete newSettings.networkConditions;
204
- }
205
- else if (options.networkConditions === 'Offline') {
206
- await page.emulateNetworkConditions({
207
- offline: true,
208
- download: 0,
209
- upload: 0,
210
- latency: 0,
211
- });
212
- newSettings.networkConditions = 'Offline';
213
- }
214
- else if (options.networkConditions in PredefinedNetworkConditions) {
215
- const networkCondition = PredefinedNetworkConditions[options.networkConditions];
216
- await page.emulateNetworkConditions(networkCondition);
217
- newSettings.networkConditions = options.networkConditions;
218
- }
219
- if (!options.cpuThrottlingRate) {
220
- await page.emulateCPUThrottling(1);
221
- delete newSettings.cpuThrottlingRate;
222
- }
223
- else {
224
- await page.emulateCPUThrottling(options.cpuThrottlingRate);
225
- newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
226
- }
227
- if (!options.geolocation) {
228
- await page.setGeolocation({ latitude: 0, longitude: 0 });
229
- delete newSettings.geolocation;
230
- }
231
- else {
232
- await page.setGeolocation(options.geolocation);
233
- newSettings.geolocation = options.geolocation;
234
- }
235
- if (!options.userAgent) {
236
- await page.setUserAgent({ userAgent: undefined });
237
- delete newSettings.userAgent;
238
- }
239
- else {
240
- await page.setUserAgent({ userAgent: options.userAgent });
241
- newSettings.userAgent = options.userAgent;
242
- }
243
- if (!options.colorScheme || options.colorScheme === 'auto') {
244
- await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: '' }]);
245
- delete newSettings.colorScheme;
246
- }
247
- else {
248
- await page.emulateMediaFeatures([
249
- { name: 'prefers-color-scheme', value: options.colorScheme },
250
- ]);
251
- newSettings.colorScheme = options.colorScheme;
252
- }
253
- if (!options.viewport) {
254
- await page.setViewport(null);
255
- delete newSettings.viewport;
256
- }
257
- else {
258
- const defaults = {
259
- deviceScaleFactor: 1,
260
- isMobile: false,
261
- hasTouch: false,
262
- isLandscape: false,
263
- };
264
- const viewport = { ...defaults, ...options.viewport };
265
- await page.setViewport(viewport);
266
- newSettings.viewport = viewport;
267
- }
268
- mcpPage.emulationSettings = Object.keys(newSettings).length ? newSettings : {};
269
- this.#updateSelectedPageTimeouts();
270
- }
271
- setIsRunningPerformanceTrace(x) {
272
- this.#isRunningTrace = x;
273
- }
274
- isRunningPerformanceTrace() {
275
- return this.#isRunningTrace;
276
- }
277
- getScreenRecorder() {
278
- return this.#screenRecorderData;
279
- }
280
- setScreenRecorder(data) {
281
- this.#screenRecorderData = data;
282
- }
283
- isCruxEnabled() {
284
- return this.#options.performanceCrux;
285
- }
286
- getSelectedPptrPage() {
287
- const page = this.#selectedPage;
288
- if (!page) {
289
- throw new Error('No page selected');
290
- }
291
- if (page.pptrPage.isClosed()) {
292
- throw new Error(`The selected page has been closed. Call ${listPages().name} to see open pages.`);
293
- }
294
- return page.pptrPage;
295
- }
296
- getSelectedMcpPage() {
297
- const page = this.getSelectedPptrPage();
298
- return this.#getMcpPage(page);
299
- }
300
- getPageById(pageId) {
301
- const page = this.#mcpPages.values().find((mcpPage) => mcpPage.id === pageId);
302
- if (!page) {
303
- throw new Error('No page found');
304
- }
305
- return page;
306
- }
307
- getPageId(page) {
308
- return this.#mcpPages.get(page)?.id;
309
- }
310
- #getMcpPage(page) {
311
- const mcpPage = this.#mcpPages.get(page);
312
- if (!mcpPage) {
313
- throw new Error('No McpPage found for the given page.');
314
- }
315
- return mcpPage;
316
- }
317
- #getSelectedMcpPage() {
318
- return this.#getMcpPage(this.getSelectedPptrPage());
319
- }
320
- isPageSelected(page) {
321
- return this.#selectedPage?.pptrPage === page;
322
- }
323
- selectPage(newPage) {
324
- this.#selectedPage = newPage;
325
- this.#updateSelectedPageTimeouts();
326
- }
327
- #updateSelectedPageTimeouts() {
328
- const page = this.#getSelectedMcpPage();
329
- // For waiters 5sec timeout should be sufficient.
330
- // Increased in case we throttle the CPU
331
- const cpuMultiplier = page.cpuThrottlingRate;
332
- page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
333
- // 10sec should be enough for the load event to be emitted during
334
- // navigations.
335
- // Increased in case we throttle the network requests
336
- const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
337
- page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
338
- }
339
- // Linear scan over per-page snapshots. The page count is small (typically
340
- // 2-10) so a reverse index isn't worthwhile given the uid-reuse lifecycle
341
- // complexity it would introduce.
342
- getAXNodeByUid(uid) {
343
- for (const mcpPage of this.#mcpPages.values()) {
344
- const node = mcpPage.textSnapshot?.idToNode.get(uid);
345
- if (node) {
346
- return node;
347
- }
348
- }
349
- return undefined;
350
- }
351
- /**
352
- * Creates a snapshot of the extension service workers.
353
- */
354
- async createExtensionServiceWorkersSnapshot() {
355
- const allTargets = await this.browser.targets();
356
- const serviceWorkers = allTargets.filter((target) => {
357
- return target.type() === 'service_worker' && target.url().includes('chrome-extension://');
358
- });
359
- for (const serviceWorker of serviceWorkers) {
360
- if (!this.#extensionServiceWorkerMap.has(serviceWorker)) {
361
- this.#extensionServiceWorkerMap.set(serviceWorker, 'sw-' + this.#nextExtensionServiceWorkerId++);
362
- }
363
- }
364
- this.#extensionServiceWorkers = serviceWorkers.map((serviceWorker) => {
365
- return {
366
- target: serviceWorker,
367
- id: this.#extensionServiceWorkerMap.get(serviceWorker),
368
- url: serviceWorker.url(),
369
- };
370
- });
371
- return this.#extensionServiceWorkers;
372
- }
373
- async createPagesSnapshot() {
374
- const { pages: allPages, isolatedContextNames } = await this.#getAllPages();
375
- for (const page of allPages) {
376
- let mcpPage = this.#mcpPages.get(page);
377
- if (!mcpPage) {
378
- mcpPage = new McpPage(page, this.#nextPageId++);
379
- this.#mcpPages.set(page, mcpPage);
380
- // We emulate a focused page for all pages to support multi-agent workflows.
381
- void page.emulateFocusedPage(true).catch((error) => {
382
- this.logger('Error turning on focused page emulation', error);
383
- });
384
- }
385
- mcpPage.isolatedContextName = isolatedContextNames.get(page);
386
- }
387
- // Prune orphaned #mcpPages entries (pages that no longer exist).
388
- const currentPages = new Set(allPages);
389
- for (const [page, mcpPage] of this.#mcpPages) {
390
- if (!currentPages.has(page)) {
391
- mcpPage.dispose();
392
- this.#mcpPages.delete(page);
393
- }
394
- }
395
- this.#pages = allPages.filter((page) => {
396
- return this.#options.experimentalDevToolsDebugging || !page.url().startsWith('devtools://');
397
- });
398
- if ((!this.#selectedPage || this.#pages.indexOf(this.#selectedPage.pptrPage) === -1) &&
399
- this.#pages[0]) {
400
- this.selectPage(this.#getMcpPage(this.#pages[0]));
401
- }
402
- await this.detectOpenDevToolsWindows();
403
- return this.#pages;
404
- }
405
- async #getAllPages() {
406
- const defaultCtx = this.browser.defaultBrowserContext();
407
- const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
408
- const allTargets = this.browser.targets();
409
- const extensionTargets = allTargets.filter((target) => {
410
- return target.url().startsWith('chrome-extension://') && target.type() === 'page';
411
- });
412
- for (const target of extensionTargets) {
413
- // Right now target.page() returns null for popup and side panel pages.
414
- let page = await target.page();
415
- if (!page) {
416
- // We need to cache pages instances for targets because target.asPage()
417
- // returns a new page instance every time.
418
- page = this.#extensionPages.get(target) ?? null;
419
- if (!page) {
420
- try {
421
- page = await target.asPage();
422
- this.#extensionPages.set(target, page);
423
- }
424
- catch (e) {
425
- this.logger('Failed to get page for extension target', e);
426
- }
427
- }
428
- }
429
- if (page && !allPages.includes(page)) {
430
- allPages.push(page);
431
- }
432
- }
433
- // Build a reverse lookup from BrowserContext instance → name.
434
- const contextToName = new Map();
435
- for (const [name, ctx] of this.#isolatedContexts) {
436
- contextToName.set(ctx, name);
437
- }
438
- // Auto-discover BrowserContexts not in our mapping (e.g., externally
439
- // created incognito contexts) and assign generated names.
440
- const knownContexts = new Set(this.#isolatedContexts.values());
441
- for (const ctx of this.browser.browserContexts()) {
442
- if (ctx !== defaultCtx && !ctx.closed && !knownContexts.has(ctx)) {
443
- const name = `isolated-context-${this.#nextIsolatedContextId++}`;
444
- this.#isolatedContexts.set(name, ctx);
445
- contextToName.set(ctx, name);
446
- }
447
- }
448
- // Map each page to its isolated context name (if any).
449
- const isolatedContextNames = new Map();
450
- for (const page of allPages) {
451
- const ctx = page.browserContext();
452
- const name = contextToName.get(ctx);
453
- if (name) {
454
- isolatedContextNames.set(page, name);
455
- }
456
- }
457
- return { pages: allPages, isolatedContextNames };
458
- }
459
- async detectOpenDevToolsWindows() {
460
- this.logger('Detecting open DevTools windows');
461
- const { pages } = await this.#getAllPages();
462
- await Promise.all(pages.map(async (page) => {
463
- const mcpPage = this.#mcpPages.get(page);
464
- if (!mcpPage) {
465
- return;
466
- }
467
- // Prior to Chrome 144.0.7559.59, the command fails,
468
- // Some Electron apps still use older version
469
- // Fall back to not exposing DevTools at all.
470
- try {
471
- if (await page.hasDevTools()) {
472
- mcpPage.devToolsPage = await page.openDevTools();
473
- }
474
- else {
475
- mcpPage.devToolsPage = undefined;
476
- }
477
- }
478
- catch {
479
- mcpPage.devToolsPage = undefined;
480
- }
481
- }));
482
- }
483
- getExtensionServiceWorkers() {
484
- return this.#extensionServiceWorkers;
485
- }
486
- getExtensionServiceWorkerId(extensionServiceWorker) {
487
- return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
488
- }
489
- getPages() {
490
- return this.#pages;
491
- }
492
- getIsolatedContextName(page) {
493
- return this.#mcpPages.get(page)?.isolatedContextName;
494
- }
495
- getDevToolsPage(page) {
496
- return this.#mcpPages.get(page)?.devToolsPage;
497
- }
498
- async getDevToolsData(page) {
499
- try {
500
- this.logger('Getting DevTools UI data');
501
- const devtoolsPage = this.getDevToolsPage(page.pptrPage);
502
- if (!devtoolsPage) {
503
- this.logger('No DevTools page detected');
504
- return {};
505
- }
506
- const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
507
- // @ts-expect-error no types
508
- const UI = await import('/bundled/ui/legacy/legacy.js');
509
- // @ts-expect-error no types
510
- const SDK = await import('/bundled/core/sdk/sdk.js');
511
- const request = UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest);
512
- const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
513
- return {
514
- cdpRequestId: request?.requestId(),
515
- cdpBackendNodeId: node?.backendNodeId(),
516
- };
517
- });
518
- return { cdpBackendNodeId, cdpRequestId };
519
- }
520
- catch (err) {
521
- this.logger('error getting devtools data', err);
522
- }
523
- return {};
524
- }
525
- /**
526
- * Creates a text snapshot of a page.
527
- */
528
- async createTextSnapshot(page, verbose = false, devtoolsData = undefined) {
529
- const rootNode = await page.pptrPage.accessibility.snapshot({
530
- includeIframes: true,
531
- interestingOnly: !verbose,
532
- });
533
- if (!rootNode) {
534
- return;
535
- }
536
- const { uniqueBackendNodeIdToMcpId } = page;
537
- const snapshotId = this.#nextSnapshotId++;
538
- // Iterate through the whole accessibility node tree and assign node ids that
539
- // will be used for the tree serialization and mapping ids back to nodes.
540
- let idCounter = 0;
541
- const idToNode = new Map();
542
- const seenUniqueIds = new Set();
543
- const assignIds = (node) => {
544
- let id = '';
545
- // @ts-expect-error untyped loaderId & backendNodeId.
546
- const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
547
- if (uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
548
- // Re-use MCP exposed ID if the uniqueId is the same.
549
- id = uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
550
- }
551
- else {
552
- // Only generate a new ID if we have not seen the node before.
553
- id = `${snapshotId}_${idCounter++}`;
554
- uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
555
- }
556
- seenUniqueIds.add(uniqueBackendId);
557
- const nodeWithId = {
558
- ...node,
559
- id,
560
- children: node.children ? node.children.map((child) => assignIds(child)) : [],
561
- };
562
- // The AXNode for an option doesn't contain its `value`.
563
- // Therefore, set text content of the option as value.
564
- if (node.role === 'option') {
565
- const optionText = node.name;
566
- if (optionText) {
567
- nodeWithId.value = optionText.toString();
568
- }
569
- }
570
- idToNode.set(nodeWithId.id, nodeWithId);
571
- return nodeWithId;
572
- };
573
- const rootNodeWithId = assignIds(rootNode);
574
- const snapshot = {
575
- root: rootNodeWithId,
576
- snapshotId: String(snapshotId),
577
- idToNode,
578
- hasSelectedElement: false,
579
- verbose,
580
- };
581
- page.textSnapshot = snapshot;
582
- const data = devtoolsData ?? (await this.getDevToolsData(page));
583
- if (data?.cdpBackendNodeId) {
584
- snapshot.hasSelectedElement = true;
585
- snapshot.selectedElementUid = this.resolveCdpElementId(page, data?.cdpBackendNodeId);
586
- }
587
- // Clean up unique IDs that we did not see anymore.
588
- for (const key of uniqueBackendNodeIdToMcpId.keys()) {
589
- if (!seenUniqueIds.has(key)) {
590
- uniqueBackendNodeIdToMcpId.delete(key);
591
- }
592
- }
593
- }
594
- async saveTemporaryFile(data, filename) {
595
- return await saveTemporaryFile(data, filename);
596
- }
597
- async saveFile(data, filename) {
598
- try {
599
- const filePath = path.resolve(filename);
600
- await fs.mkdir(path.dirname(filePath), { recursive: true });
601
- await fs.writeFile(filePath, data);
602
- return { filename: filePath };
603
- }
604
- catch (err) {
605
- this.logger(err);
606
- throw new Error('Could not save a file', { cause: err });
607
- }
608
- }
609
- storeTraceRecording(result) {
610
- // Clear the trace results because we only consume the latest trace currently.
611
- this.#traceResults = [];
612
- this.#traceResults.push(result);
613
- }
614
- recordedTraces() {
615
- return this.#traceResults;
616
- }
617
- getWaitForHelper(page, cpuMultiplier, networkMultiplier) {
618
- return new WaitForHelper(page, cpuMultiplier, networkMultiplier);
619
- }
620
- waitForEventsAfterAction(action, options) {
621
- const page = this.#getSelectedMcpPage();
622
- const cpuMultiplier = page.cpuThrottlingRate;
623
- const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
624
- const waitForHelper = this.getWaitForHelper(page.pptrPage, cpuMultiplier, networkMultiplier);
625
- return waitForHelper.waitForEventsAfterAction(action, options);
626
- }
627
- getNetworkRequestStableId(request) {
628
- return this.#networkCollector.getIdForResource(request);
629
- }
630
- waitForTextOnPage(text, timeout, targetPage) {
631
- const page = targetPage ?? this.getSelectedPptrPage();
632
- const frames = page.frames();
633
- let locator = this.#locatorClass.race(frames.flatMap((frame) => text.flatMap((value) => [frame.locator(`aria/${value}`), frame.locator(`text/${value}`)])));
634
- if (timeout) {
635
- locator = locator.setTimeout(timeout);
636
- }
637
- return locator.wait();
638
- }
639
- /**
640
- * We need to ignore favicon request as they make our test flaky
641
- */
642
- async setUpNetworkCollectorForTesting() {
643
- this.#networkCollector = new NetworkCollector(this.browser, (collect) => {
644
- return {
645
- request: (req) => {
646
- if (req.url().includes('favicon.ico')) {
647
- return;
648
- }
649
- collect(req);
650
- },
651
- };
652
- });
653
- const { pages } = await this.#getAllPages();
654
- await this.#networkCollector.init(pages);
655
- }
656
- async installExtension(extensionPath) {
657
- const id = await this.browser.installExtension(extensionPath);
658
- await this.#extensionRegistry.registerExtension(id, extensionPath);
659
- return id;
660
- }
661
- async uninstallExtension(id) {
662
- await this.browser.uninstallExtension(id);
663
- this.#extensionRegistry.remove(id);
664
- }
665
- async triggerExtensionAction(id) {
666
- const page = this.getSelectedPptrPage();
667
- // @ts-expect-error internal puppeteer api is needed since we don't have a way to get
668
- // a tab id at the moment
669
- const theTarget = page._tabId;
670
- const session = await this.browser.target().createCDPSession();
671
- try {
672
- await session.send('Extensions.triggerAction', {
673
- id,
674
- targetId: theTarget,
675
- });
676
- }
677
- finally {
678
- await session.detach();
679
- }
680
- }
681
- listExtensions() {
682
- return this.#extensionRegistry.list();
683
- }
684
- getExtension(id) {
685
- return this.#extensionRegistry.getById(id);
686
- }
687
- }