playwright-e2e-mcp 0.4.1 → 0.5.0

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 (59) hide show
  1. package/README.md +215 -43
  2. package/dist/cli/init.js +3 -2
  3. package/dist/cli/init.js.map +1 -1
  4. package/dist/http.d.ts +6 -1
  5. package/dist/http.js +13 -3
  6. package/dist/http.js.map +1 -1
  7. package/dist/oauth.d.ts +34 -0
  8. package/dist/oauth.js +346 -0
  9. package/dist/oauth.js.map +1 -0
  10. package/dist/server.d.ts +2 -2
  11. package/dist/server.js +31 -4
  12. package/dist/server.js.map +1 -1
  13. package/dist/tools/analyze-history.d.ts +12 -1
  14. package/dist/tools/analyze-history.js +89 -5
  15. package/dist/tools/analyze-history.js.map +1 -1
  16. package/dist/tools/audit-accessibility.d.ts +85 -0
  17. package/dist/tools/audit-accessibility.js +4 -1
  18. package/dist/tools/audit-accessibility.js.map +1 -1
  19. package/dist/tools/audit-performance.d.ts +333 -0
  20. package/dist/tools/audit-performance.js +377 -0
  21. package/dist/tools/audit-performance.js.map +1 -0
  22. package/dist/tools/compare-visual-state.d.ts +84 -0
  23. package/dist/tools/generate-e2e-test.d.ts +90 -0
  24. package/dist/tools/generate-e2e-test.js +223 -34
  25. package/dist/tools/generate-e2e-test.js.map +1 -1
  26. package/dist/tools/get-failure.js +2 -1
  27. package/dist/tools/get-failure.js.map +1 -1
  28. package/dist/tools/inspect-page.d.ts +94 -0
  29. package/dist/tools/inspect-page.js +42 -5
  30. package/dist/tools/inspect-page.js.map +1 -1
  31. package/dist/tools/load-report.d.ts +48 -0
  32. package/dist/tools/load-report.js +167 -0
  33. package/dist/tools/load-report.js.map +1 -0
  34. package/dist/tools/probe-script.d.ts +7 -0
  35. package/dist/tools/probe-script.js +256 -7
  36. package/dist/tools/probe-script.js.map +1 -1
  37. package/dist/tools/shared.d.ts +137 -2
  38. package/dist/tools/shared.js +155 -8
  39. package/dist/tools/shared.js.map +1 -1
  40. package/dist/tools/suggest-fix.d.ts +35 -1
  41. package/dist/tools/suggest-fix.js +197 -17
  42. package/dist/tools/suggest-fix.js.map +1 -1
  43. package/dist/tools/validate-selector.d.ts +84 -0
  44. package/dist/tools/validate-selector.js +7 -1
  45. package/dist/tools/validate-selector.js.map +1 -1
  46. package/dist/types/index.d.ts +28 -0
  47. package/dist/utils/project-detector.d.ts +7 -0
  48. package/dist/utils/project-detector.js +11 -0
  49. package/dist/utils/project-detector.js.map +1 -1
  50. package/dist/utils/report-parser.d.ts +28 -3
  51. package/dist/utils/report-parser.js +77 -11
  52. package/dist/utils/report-parser.js.map +1 -1
  53. package/dist/utils/run-history.d.ts +66 -3
  54. package/dist/utils/run-history.js +149 -3
  55. package/dist/utils/run-history.js.map +1 -1
  56. package/package.json +5 -3
  57. package/skill/playwright-e2e-mcp/SKILL.md +71 -15
  58. package/skill/playwright-e2e-mcp/references/setup.md +1 -1
  59. package/skill/playwright-e2e-mcp/references/tools.md +67 -6
@@ -15,6 +15,13 @@
15
15
  * on the live page or on a DOM snapshot taken from a trace
16
16
  * - a11y run axe-core on the page (or a scoped part) and describe the
17
17
  * offending elements, each with a verified locator
18
+ * - perf lab Web Vitals (LCP, CLS, TBT, INP during actions, FCP, TTFB)
19
+ * and the network cost per resource type, optionally under
20
+ * CPU and network throttling; the LCP element and the elements
21
+ * that shifted come back with verified locators
22
+ *
23
+ * Any mode that navigates applies the caller's network mocks (cfg.routes)
24
+ * first and reports how many requests each one handled.
18
25
  *
19
26
  * Every element the script reports carries Playwright locators that were
20
27
  * proven, in this same page, to resolve to exactly that one element.
@@ -30,7 +37,7 @@ function send(payload) {
30
37
  function classifyError(message) {
31
38
  const m = String(message);
32
39
  if (/Executable doesn't exist|Please run the following command to install/i.test(m)) {
33
- return { kind: 'NO_PLAYWRIGHT', hint: 'Playwright browsers are not installed for this project. Run: npx playwright install chromium' };
40
+ return { kind: 'NO_PLAYWRIGHT', hint: 'Playwright browsers are not installed for this project. Run: npx playwright install chromium (or firefox / webkit for those engines)' };
34
41
  }
35
42
  if (/ECONNREFUSED|ERR_CONNECTION_REFUSED|ERR_NAME_NOT_RESOLVED|ERR_EMPTY_RESPONSE|getaddrinfo|net::ERR_/i.test(m)) {
36
43
  return { kind: 'SERVER_NOT_RUNNING', hint: 'The URL is not reachable. Start your dev server (e.g. npm run dev / npm start) and retry.' };
@@ -429,6 +436,152 @@ async function runActions(page, actions, timeout) {
429
436
  return done;
430
437
  }
431
438
 
439
+ /* ---------------- performance (mode: perf) ---------------- */
440
+
441
+ // Runs in every new document before its own scripts, so no early paint,
442
+ // shift or long task is missed.
443
+ function perfInit() {
444
+ const P = { lcp: null, shifts: [], longTasks: [], events: [] };
445
+ Object.defineProperty(window, '__pwmcpPerf', { value: P, configurable: true });
446
+ const watch = (type, fn, extra) => {
447
+ try {
448
+ new PerformanceObserver((list) => list.getEntries().forEach(fn)).observe(Object.assign({ type: type, buffered: true }, extra || {}));
449
+ } catch (err) { /* entry type not supported */ }
450
+ };
451
+ watch('largest-contentful-paint', (e) => { P.lcp = e; });
452
+ watch('layout-shift', (e) => { if (!e.hadRecentInput) P.shifts.push(e); });
453
+ watch('longtask', (e) => { P.longTasks.push({ start: e.startTime, duration: e.duration }); });
454
+ watch('event', (e) => { if (e.interactionId) P.events.push({ id: e.interactionId, duration: e.duration, name: e.name, target: e.target }); }, { durationThreshold: 16 });
455
+ }
456
+
457
+ function collectPerf() {
458
+ const h = window.__pwmcp;
459
+ const P = window.__pwmcpPerf || { lcp: null, shifts: [], longTasks: [], events: [] };
460
+ h.els = [];
461
+ const keep = (el) => {
462
+ if (!el || el.nodeType !== 1 || !el.isConnected) return -1;
463
+ const at = h.els.indexOf(el);
464
+ if (at >= 0) return at;
465
+ h.els.push(el);
466
+ return h.els.length - 1;
467
+ };
468
+ const nav = performance.getEntriesByType('navigation')[0];
469
+ const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0];
470
+ const fcp = fcpEntry ? fcpEntry.startTime : undefined;
471
+
472
+ // CLS: the worst session window (shifts < 1s apart, window <= 5s).
473
+ let cls = 0, win = 0, winStart = 0, prev = 0, cur = [], worst = [];
474
+ for (const s of P.shifts) {
475
+ if (cur.length && (s.startTime - prev > 1000 || s.startTime - winStart > 5000)) { win = 0; cur = []; }
476
+ if (!cur.length) winStart = s.startTime;
477
+ win += s.value; cur.push(s); prev = s.startTime;
478
+ if (win > cls) { cls = win; worst = cur.slice(); }
479
+ }
480
+ const shiftElements = [];
481
+ for (const s of worst) {
482
+ for (const src of s.sources || []) {
483
+ const i = keep(src.node);
484
+ if (i >= 0 && !shiftElements.includes(i) && shiftElements.length < 5) shiftElements.push(i);
485
+ }
486
+ }
487
+
488
+ // TBT: blocking time of long tasks after first paint.
489
+ let tbt = 0, longest = 0;
490
+ for (const t of P.longTasks) {
491
+ longest = Math.max(longest, t.duration);
492
+ if (fcp === undefined || t.start >= fcp) tbt += Math.max(0, t.duration - 50);
493
+ }
494
+
495
+ // INP (lab): the slowest interaction, events grouped by interactionId.
496
+ const byInteraction = new Map();
497
+ for (const e of P.events) {
498
+ const prior = byInteraction.get(e.id);
499
+ if (!prior || e.duration > prior.duration) byInteraction.set(e.id, e);
500
+ }
501
+ let inp;
502
+ for (const e of byInteraction.values()) if (!inp || e.duration > inp.duration) inp = e;
503
+ let inpTarget;
504
+ if (inp) {
505
+ const i = keep(inp.target);
506
+ inpTarget = inp.name + (i >= 0 ? ' on ' + h.describe(inp.target).tag : '');
507
+ }
508
+
509
+ const lcpEl = P.lcp ? P.lcp.element : null;
510
+ const lcpElement = keep(lcpEl);
511
+ let lcpMs;
512
+ if (P.lcp) lcpMs = P.lcp.renderTime || P.lcp.loadTime || P.lcp.startTime;
513
+
514
+ const timings = {};
515
+ for (const r of performance.getEntriesByType('resource')) {
516
+ timings[r.name] = { duration: r.duration, blocking: r.renderBlockingStatus === 'blocking' };
517
+ }
518
+ let heap;
519
+ try { heap = performance.memory ? performance.memory.usedJSHeapSize : undefined; } catch (err) { /* not exposed */ }
520
+ return {
521
+ title: document.title,
522
+ finalUrl: location.href,
523
+ viewport: { width: window.innerWidth, height: window.innerHeight },
524
+ elements: h.els.map((el) => h.describe(el)),
525
+ perf: {
526
+ ttfbMs: nav ? nav.responseStart : undefined,
527
+ fcpMs: fcp,
528
+ lcpMs: lcpMs,
529
+ lcpElement: lcpElement,
530
+ lcpTag: lcpEl ? lcpEl.tagName.toLowerCase() : undefined,
531
+ lcpUrl: P.lcp && P.lcp.url ? String(P.lcp.url).slice(0, 300) : undefined,
532
+ cls: Math.round(cls * 1000) / 1000,
533
+ shiftElements: shiftElements,
534
+ tbtMs: Math.round(tbt),
535
+ longTasks: P.longTasks.length,
536
+ longestTaskMs: Math.round(longest),
537
+ inpMs: inp ? Math.round(inp.duration) : undefined,
538
+ inpTarget: inpTarget,
539
+ domContentLoadedMs: nav && nav.domContentLoadedEventEnd ? nav.domContentLoadedEventEnd : undefined,
540
+ loadMs: nav && nav.loadEventEnd ? nav.loadEventEnd : undefined,
541
+ domNodes: document.getElementsByTagName('*').length,
542
+ jsHeapBytes: heap,
543
+ },
544
+ timings: timings,
545
+ };
546
+ }
547
+
548
+ function registrableDomain(host) {
549
+ const parts = String(host).split('.');
550
+ return parts.length <= 2 ? host : parts.slice(-2).join('.');
551
+ }
552
+
553
+ function summariseRequests(requests, timings, pageUrl) {
554
+ const site = registrableDomain(new URL(pageUrl).hostname);
555
+ const byType = {};
556
+ const all = [];
557
+ const thirdParty = new Set();
558
+ let thirdPartyBytes = 0;
559
+ let total = 0;
560
+ for (const r of requests.values()) {
561
+ if (!r.url || /^data:|^blob:/.test(r.url)) continue;
562
+ const bytes = r.bytes || 0;
563
+ total += bytes;
564
+ const type = (r.type || 'Other').toLowerCase();
565
+ if (!byType[type]) byType[type] = { type: type, requests: 0, transferBytes: 0 };
566
+ byType[type].requests += 1;
567
+ byType[type].transferBytes += bytes;
568
+ let host = '';
569
+ try { host = new URL(r.url).hostname; } catch (err) { /* opaque */ }
570
+ if (host && registrableDomain(host) !== site) { thirdParty.add(host); thirdPartyBytes += bytes; }
571
+ const t = timings[r.url] || {};
572
+ all.push({ url: r.url.slice(0, 200), type: type, transferBytes: bytes, durationMs: Math.round(t.duration || 0), renderBlocking: !!t.blocking });
573
+ }
574
+ return {
575
+ requests: all.length,
576
+ transferBytes: total,
577
+ byType: Object.values(byType).sort((a, b) => b.transferBytes - a.transferBytes),
578
+ largest: all.slice().sort((a, b) => b.transferBytes - a.transferBytes).slice(0, 5),
579
+ renderBlocking: all.filter((r) => r.renderBlocking).slice(0, 8),
580
+ thirdPartyBytes: thirdPartyBytes,
581
+ thirdPartyOrigins: thirdParty.size,
582
+ };
583
+ }
584
+
432
585
  (async () => {
433
586
  const { createRequire } = require('node:module');
434
587
  const req = createRequire(cfg.projectRoot + '/package.json');
@@ -450,8 +603,16 @@ async function runActions(page, actions, timeout) {
450
603
  }
451
604
 
452
605
  let browser = null;
606
+ let openContext = null;
453
607
  let actionsDone = [];
608
+ // A recorded HAR is written when its context closes, so close the
609
+ // context before the browser.
610
+ const closeBrowser = async () => {
611
+ if (openContext) await openContext.close().catch(() => undefined);
612
+ await browser.close();
613
+ };
454
614
  try {
615
+ const engine = cfg.browser || 'chromium';
455
616
  const launchOptions = { headless: true };
456
617
  if (cfg.blockPrivate) {
457
618
  // SSRF guard: every browser connection (each redirect hop and
@@ -461,16 +622,73 @@ async function runActions(page, actions, timeout) {
461
622
  launchOptions.proxy = { server: 'http://127.0.0.1:' + proxyPort };
462
623
  // WebRTC STUN/TURN over UDP does not go through the proxy; keep it off
463
624
  // so a page cannot send UDP to (or scan) the private network.
464
- launchOptions.args = ['--proxy-bypass-list=<-loopback>', '--force-webrtc-ip-handling-policy=disable_non_proxied_udp'];
625
+ if (engine === 'firefox') {
626
+ launchOptions.firefoxUserPrefs = { 'network.proxy.allow_hijacking_localhost': true, 'media.peerconnection.enabled': false };
627
+ } else {
628
+ launchOptions.args = ['--proxy-bypass-list=<-loopback>', '--force-webrtc-ip-handling-policy=disable_non_proxied_udp'];
629
+ }
465
630
  }
466
- browser = await pw.chromium.launch(launchOptions);
631
+ browser = await pw[engine].launch(launchOptions);
467
632
  const contextOptions = { viewport: cfg.viewport || { width: 1280, height: 720 } };
633
+ if (cfg.mobile) Object.assign(contextOptions, { isMobile: true, hasTouch: true, deviceScaleFactor: 1.75 });
468
634
  if (cfg.storageState) contextOptions.storageState = cfg.storageState;
469
635
  if (cfg.headers) contextOptions.extraHTTPHeaders = cfg.headers;
470
636
  if (cfg.testIdAttribute) pw.selectors.setTestIdAttribute(cfg.testIdAttribute);
637
+ const recording = cfg.har && cfg.har.mode === 'record' && !cfg.snapshotHtmlPath;
638
+ if (recording) contextOptions.recordHar = { path: cfg.har.path, urlFilter: cfg.har.url, mode: 'minimal' };
471
639
  const context = await browser.newContext(contextOptions);
640
+ openContext = context;
641
+ // Replay goes first so the network mocks below, registered later, win.
642
+ if (cfg.har && cfg.har.mode === 'replay' && !cfg.snapshotHtmlPath) {
643
+ await context.routeFromHAR(cfg.har.path, { url: cfg.har.url, notFound: cfg.har.notFound || 'abort' });
644
+ }
645
+
646
+ // Network mocks. Playwright runs the most recently registered handler
647
+ // first, so register in reverse to make the first listed mock win.
648
+ const routeHits = (cfg.routes || []).map(() => 0);
649
+ if (cfg.routes && cfg.routes.length && !cfg.snapshotHtmlPath) {
650
+ for (let i = cfg.routes.length - 1; i >= 0; i -= 1) {
651
+ const r = cfg.routes[i];
652
+ await context.route(r.url, async (route) => {
653
+ if (r.method && route.request().method() !== r.method) return route.fallback();
654
+ routeHits[i] += 1;
655
+ if (r.delayMs) await new Promise((resolve) => setTimeout(resolve, r.delayMs));
656
+ if (r.abort) return route.abort('failed');
657
+ if (r.status !== undefined) {
658
+ return route.fulfill({ status: r.status, body: r.body === undefined ? '' : r.body, contentType: r.contentType, headers: r.headers });
659
+ }
660
+ return route.fallback();
661
+ });
662
+ }
663
+ }
664
+
665
+ if (cfg.mode === 'perf') await context.addInitScript(perfInit);
472
666
  const page = await context.newPage();
473
667
 
668
+ // perf: count bytes per request over CDP (cross-origin sizes included)
669
+ // and apply CPU / network throttling before the first byte.
670
+ const requests = new Map();
671
+ if (cfg.mode === 'perf') {
672
+ const cdp = await context.newCDPSession(page);
673
+ cdp.on('Network.responseReceived', (e) => {
674
+ if (requests.size < 2000 || requests.has(e.requestId)) requests.set(e.requestId, Object.assign(requests.get(e.requestId) || {}, { url: e.response.url, type: e.type }));
675
+ });
676
+ cdp.on('Network.loadingFinished', (e) => {
677
+ const r = requests.get(e.requestId);
678
+ if (r) r.bytes = e.encodedDataLength;
679
+ });
680
+ await cdp.send('Network.enable');
681
+ if (cfg.network) {
682
+ await cdp.send('Network.emulateNetworkConditions', {
683
+ offline: false,
684
+ latency: cfg.network.latencyMs,
685
+ downloadThroughput: cfg.network.downloadBps,
686
+ uploadThroughput: cfg.network.uploadBps,
687
+ });
688
+ }
689
+ if (cfg.cpuThrottle && cfg.cpuThrottle > 1) await cdp.send('Emulation.setCPUThrottlingRate', { rate: cfg.cpuThrottle });
690
+ }
691
+
474
692
  const consoleMessages = [];
475
693
  page.on('console', (msg) => {
476
694
  if (consoleMessages.length < 50) consoleMessages.push({ type: msg.type(), text: String(msg.text()).slice(0, 300) });
@@ -487,7 +705,15 @@ async function runActions(page, actions, timeout) {
487
705
  } else {
488
706
  await page.goto(cfg.url, { waitUntil: cfg.waitUntil || 'domcontentloaded', timeout: cfg.gotoTimeout || 15000 });
489
707
  if (cfg.waitFor) await page.locator(cfg.waitFor).first().waitFor({ state: 'visible', timeout: cfg.waitTimeout || 5000 });
490
- if (cfg.actions && cfg.actions.length) actionsDone = await runActions(page, cfg.actions, cfg.actionTimeout || 10000);
708
+ // perf: let late images, fonts and shifts land before anything else.
709
+ if (cfg.mode === 'perf' && cfg.settleMs) await page.waitForTimeout(cfg.settleMs);
710
+ if (cfg.actions && cfg.actions.length) {
711
+ actionsDone = await runActions(page, cfg.actions, cfg.actionTimeout || 10000);
712
+ if (cfg.mode === 'perf') {
713
+ await page.waitForLoadState('load', { timeout: cfg.waitTimeout || 5000 }).catch(() => undefined);
714
+ await page.waitForTimeout(Math.min(cfg.settleMs || 1000, 2000));
715
+ }
716
+ }
491
717
  }
492
718
 
493
719
  if (cfg.mode === 'screenshot') {
@@ -507,10 +733,11 @@ async function runActions(page, actions, timeout) {
507
733
  matchCount: 0,
508
734
  elements: [],
509
735
  actionsDone: actionsDone,
736
+ routeHits: routeHits,
510
737
  durationMs: Date.now() - cfg.startedAt,
511
738
  },
512
739
  });
513
- await browser.close();
740
+ await closeBrowser();
514
741
  process.exit(0);
515
742
  }
516
743
 
@@ -584,6 +811,13 @@ async function runActions(page, actions, timeout) {
584
811
  };
585
812
  }, { tags: cfg.axeTags || [], selector: cfg.selector, maxNodes: cfg.maxNodes || 3 });
586
813
  await attachLocators(page, data.elements, 40, 2);
814
+ } else if (cfg.mode === 'perf') {
815
+ const collected = await page.evaluate(collectPerf);
816
+ const network = summariseRequests(requests, collected.timings, page.url());
817
+ delete collected.timings;
818
+ Object.assign(collected.perf, network);
819
+ data = Object.assign(collected, { matchCount: 0, elementCount: collected.elements.length });
820
+ await attachLocators(page, data.elements, 12, 2);
587
821
  } else if (cfg.mode === 'heal') {
588
822
  data = await page.evaluate((input) => {
589
823
  const h = window.__pwmcp;
@@ -612,6 +846,20 @@ async function runActions(page, actions, timeout) {
612
846
  return { matchCount: elements.length, elementCount: document.querySelectorAll('*').length, elements: elements, title: document.title };
613
847
  }, { tokens: cfg.healTokens || [], role: cfg.healRole });
614
848
  await attachLocators(page, data.elements, 5, 3);
849
+ } else if (cfg.view === 'aria') {
850
+ const scope = cfg.selector ? page.locator(cfg.selector) : page.locator('body');
851
+ const count = await scope.count();
852
+ data = { matchCount: count, elementCount: count, elements: [], title: await page.title(), finalUrl: page.url() };
853
+ if (count > 0) {
854
+ if (typeof scope.ariaSnapshot !== 'function') {
855
+ throw new Error('ariaSnapshot needs Playwright 1.49 or newer');
856
+ }
857
+ const snapshot = await scope.first().ariaSnapshot({ timeout: cfg.waitTimeout || 10000 });
858
+ const max = cfg.maxHtmlChars || 12000;
859
+ data.ariaSnapshot = snapshot.slice(0, max);
860
+ data.ariaTruncated = snapshot.length > max;
861
+ }
862
+ data.viewport = page.viewportSize() || undefined;
615
863
  } else {
616
864
  data = await page.evaluate((input) => {
617
865
  const h = window.__pwmcp;
@@ -671,16 +919,17 @@ async function runActions(page, actions, timeout) {
671
919
 
672
920
  data.consoleMessages = consoleMessages;
673
921
  data.actionsDone = actionsDone;
922
+ data.routeHits = routeHits;
674
923
  data.finalUrl = data.finalUrl || page.url();
675
924
  data.durationMs = Date.now() - cfg.startedAt;
676
925
  send({ ok: true, data: data });
677
- await browser.close();
926
+ await closeBrowser();
678
927
  process.exit(0);
679
928
  } catch (err) {
680
929
  const message = err && err.message ? err.message : String(err);
681
930
  const classified = classifyError(message);
682
931
  send({ ok: false, kind: classified.kind, error: message, hint: classified.hint, actionsDone: err && err.actionsDone });
683
- try { if (browser) await browser.close(); } catch (closeErr) { /* ignore */ }
932
+ try { if (browser) await closeBrowser(); } catch (closeErr) { /* ignore */ }
684
933
  process.exit(1);
685
934
  }
686
935
  })().catch((err) => {
@@ -1 +1 @@
1
- {"version":3,"file":"probe-script.js","sourceRoot":"","sources":["../../src/tools/probe-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4pBrC,CAAC"}
1
+ {"version":3,"file":"probe-script.js","sourceRoot":"","sources":["../../src/tools/probe-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA84BrC,CAAC"}
@@ -72,6 +72,21 @@ export declare const sessionShape: {
72
72
  storageState: z.ZodOptional<z.ZodString>;
73
73
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
74
74
  };
75
+ /**
76
+ * A network mock, applied with context.route before the page loads: fulfil
77
+ * with a canned response, abort, or only delay the real request.
78
+ */
79
+ export declare const routeMockSchema: z.ZodObject<{
80
+ url: z.ZodString;
81
+ method: z.ZodOptional<z.ZodString>;
82
+ status: z.ZodOptional<z.ZodNumber>;
83
+ json: z.ZodOptional<z.ZodUnknown>;
84
+ body: z.ZodOptional<z.ZodString>;
85
+ contentType: z.ZodOptional<z.ZodString>;
86
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
87
+ abort: z.ZodOptional<z.ZodBoolean>;
88
+ delayMs: z.ZodOptional<z.ZodNumber>;
89
+ }, z.core.$strip>;
75
90
  /** Inputs shared by every tool that opens a page. */
76
91
  export declare const pageStateShape: {
77
92
  storageState: z.ZodOptional<z.ZodString>;
@@ -94,19 +109,67 @@ export declare const pageStateShape: {
94
109
  url: z.ZodOptional<z.ZodString>;
95
110
  ms: z.ZodOptional<z.ZodNumber>;
96
111
  }, z.core.$strip>>>;
112
+ routes: z.ZodOptional<z.ZodArray<z.ZodObject<{
113
+ url: z.ZodString;
114
+ method: z.ZodOptional<z.ZodString>;
115
+ status: z.ZodOptional<z.ZodNumber>;
116
+ json: z.ZodOptional<z.ZodUnknown>;
117
+ body: z.ZodOptional<z.ZodString>;
118
+ contentType: z.ZodOptional<z.ZodString>;
119
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
120
+ abort: z.ZodOptional<z.ZodBoolean>;
121
+ delayMs: z.ZodOptional<z.ZodNumber>;
122
+ }, z.core.$strip>>>;
97
123
  viewport: z.ZodOptional<z.ZodObject<{
98
124
  width: z.ZodNumber;
99
125
  height: z.ZodNumber;
100
126
  }, z.core.$strip>>;
127
+ har: z.ZodOptional<z.ZodObject<{
128
+ path: z.ZodString;
129
+ mode: z.ZodOptional<z.ZodEnum<{
130
+ record: "record";
131
+ replay: "replay";
132
+ }>>;
133
+ url: z.ZodOptional<z.ZodString>;
134
+ notFound: z.ZodOptional<z.ZodEnum<{
135
+ abort: "abort";
136
+ fallback: "fallback";
137
+ }>>;
138
+ }, z.core.$strip>>;
139
+ browser: z.ZodOptional<z.ZodEnum<{
140
+ chromium: "chromium";
141
+ firefox: "firefox";
142
+ webkit: "webkit";
143
+ }>>;
101
144
  };
102
145
  export interface PageStateInput {
103
146
  storageState?: string;
104
147
  headers?: Record<string, string>;
105
148
  actions?: Array<z.infer<typeof pageActionSchema>>;
149
+ routes?: Array<z.infer<typeof routeMockSchema>>;
106
150
  viewport?: {
107
151
  width: number;
108
152
  height: number;
109
153
  };
154
+ har?: {
155
+ path: string;
156
+ mode?: 'replay' | 'record';
157
+ url?: string;
158
+ notFound?: 'abort' | 'fallback';
159
+ };
160
+ browser?: BrowserEngine;
161
+ }
162
+ export type BrowserEngine = 'chromium' | 'firefox' | 'webkit';
163
+ export interface ResolvedRouteMock {
164
+ url: string;
165
+ label: string;
166
+ method?: string;
167
+ status?: number;
168
+ body?: string;
169
+ contentType?: string;
170
+ headers?: Record<string, string>;
171
+ abort?: boolean;
172
+ delayMs?: number;
110
173
  }
111
174
  export interface ResolvedPageState {
112
175
  storageState?: string;
@@ -119,11 +182,21 @@ export interface ResolvedPageState {
119
182
  url?: string;
120
183
  ms?: number;
121
184
  }>;
185
+ routes?: ResolvedRouteMock[];
122
186
  viewport?: {
123
187
  width: number;
124
188
  height: number;
125
189
  };
190
+ har?: {
191
+ path: string;
192
+ mode: 'replay' | 'record';
193
+ url?: string;
194
+ notFound: 'abort' | 'fallback';
195
+ };
196
+ browser?: BrowserEngine;
126
197
  }
198
+ /** Validate network mocks and turn each into a plain fulfil/abort/delay spec. */
199
+ export declare function resolveRouteMocks(routes: NonNullable<PageStateInput['routes']>): ResolvedRouteMock[];
127
200
  /**
128
201
  * Validate page-state inputs: the storageState file must sit inside the
129
202
  * project root (symlinks resolved) and parse as JSON, every goto URL passes
@@ -133,10 +206,12 @@ export interface ResolvedPageState {
133
206
  export declare function resolvePageState(input: PageStateInput, root: string, entryUrl: string, ctx: ToolContext): Promise<ResolvedPageState>;
134
207
  /** One line describing the page state, for tool output headers. */
135
208
  export declare function describePageState(state: ResolvedPageState, root: string): string | undefined;
209
+ /** One line per network mock with how often it matched; unmatched mocks are flagged. */
210
+ export declare function describeRouteHits(state: ResolvedPageState, hits: number[] | undefined): string[];
136
211
  /** A hint when an unauthenticated page landed on a login screen. */
137
212
  export declare function loginHint(finalUrl: string | undefined, state: ResolvedPageState): string | undefined;
138
213
  export interface BrowserScriptConfig extends Omit<ResolvedPageState, never> {
139
- mode: 'inspect' | 'validate' | 'screenshot' | 'heal' | 'a11y';
214
+ mode: 'inspect' | 'validate' | 'screenshot' | 'heal' | 'a11y' | 'perf';
140
215
  projectRoot: string;
141
216
  /** Page to open; omitted when rendering a snapshot. */
142
217
  url?: string;
@@ -147,7 +222,7 @@ export interface BrowserScriptConfig extends Omit<ResolvedPageState, never> {
147
222
  /** Parsed locator chain for validate / screenshot modes. */
148
223
  selectorChain?: LocatorCall[];
149
224
  /** inspect: 'elements' (DOM inventory) or 'locators' (compact locator map). */
150
- view?: 'elements' | 'locators';
225
+ view?: 'elements' | 'locators' | 'aria';
151
226
  /** heal: words from the broken locator, and its role when it had one. */
152
227
  healTokens?: string[];
153
228
  healRole?: string;
@@ -166,8 +241,21 @@ export interface BrowserScriptConfig extends Omit<ResolvedPageState, never> {
166
241
  axeSourcePath?: string;
167
242
  axeTags?: string[];
168
243
  maxNodes?: number;
244
+ /** perf: CPU slowdown factor and network profile (CDP emulation). */
245
+ cpuThrottle?: number;
246
+ network?: NetworkProfile;
247
+ /** perf: emulate a phone (isMobile, touch, device pixel ratio). */
248
+ mobile?: boolean;
249
+ /** perf: ms to keep observing after load so late shifts and LCP land. */
250
+ settleMs?: number;
169
251
  startedAt: number;
170
252
  }
253
+ /** Network emulation in Chrome DevTools terms (throughput in bytes/s). */
254
+ export interface NetworkProfile {
255
+ latencyMs: number;
256
+ downloadBps: number;
257
+ uploadBps: number;
258
+ }
171
259
  export interface BrowserScriptData {
172
260
  title?: string;
173
261
  finalUrl?: string;
@@ -188,6 +276,53 @@ export interface BrowserScriptData {
188
276
  durationMs?: number;
189
277
  /** mode 'a11y': axe-core violations; node.element indexes `elements`. */
190
278
  a11y?: AccessibilityScan;
279
+ /** mode 'perf': lab Web Vitals and resource stats; element fields index `elements`. */
280
+ perf?: PerformanceScan;
281
+ /** inspect view 'aria': the YAML accessibility snapshot of the scope. */
282
+ ariaSnapshot?: string;
283
+ ariaTruncated?: boolean;
284
+ /** Requests each network mock in `routes` handled, by index. */
285
+ routeHits?: number[];
286
+ }
287
+ export interface PerformanceResource {
288
+ url: string;
289
+ type: string;
290
+ transferBytes: number;
291
+ durationMs: number;
292
+ renderBlocking: boolean;
293
+ }
294
+ export interface PerformanceScan {
295
+ ttfbMs?: number;
296
+ fcpMs?: number;
297
+ lcpMs?: number;
298
+ /** Index into `elements` of the LCP element, -1 when unknown. */
299
+ lcpElement: number;
300
+ lcpTag?: string;
301
+ lcpUrl?: string;
302
+ cls: number;
303
+ /** Elements that moved most in the worst layout-shift window. */
304
+ shiftElements: number[];
305
+ tbtMs: number;
306
+ longTasks: number;
307
+ longestTaskMs: number;
308
+ /** Slowest interaction during `actions` (lab INP), when there were any. */
309
+ inpMs?: number;
310
+ inpTarget?: string;
311
+ domContentLoadedMs?: number;
312
+ loadMs?: number;
313
+ requests: number;
314
+ transferBytes: number;
315
+ byType: Array<{
316
+ type: string;
317
+ requests: number;
318
+ transferBytes: number;
319
+ }>;
320
+ largest: PerformanceResource[];
321
+ renderBlocking: PerformanceResource[];
322
+ thirdPartyBytes: number;
323
+ thirdPartyOrigins: number;
324
+ domNodes: number;
325
+ jsHeapBytes?: number;
191
326
  }
192
327
  export interface AccessibilityNode {
193
328
  target: string;