playwright-e2e-mcp 0.4.2 → 0.5.1

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 (56) hide show
  1. package/README.md +238 -924
  2. package/dist/http.d.ts +2 -1
  3. package/dist/http.js +2 -1
  4. package/dist/http.js.map +1 -1
  5. package/dist/server.d.ts +2 -2
  6. package/dist/server.js +31 -4
  7. package/dist/server.js.map +1 -1
  8. package/dist/tools/analyze-history.d.ts +12 -1
  9. package/dist/tools/analyze-history.js +89 -5
  10. package/dist/tools/analyze-history.js.map +1 -1
  11. package/dist/tools/audit-accessibility.d.ts +85 -0
  12. package/dist/tools/audit-accessibility.js +4 -1
  13. package/dist/tools/audit-accessibility.js.map +1 -1
  14. package/dist/tools/audit-performance.d.ts +333 -0
  15. package/dist/tools/audit-performance.js +377 -0
  16. package/dist/tools/audit-performance.js.map +1 -0
  17. package/dist/tools/compare-visual-state.d.ts +84 -0
  18. package/dist/tools/generate-e2e-test.d.ts +90 -0
  19. package/dist/tools/generate-e2e-test.js +223 -34
  20. package/dist/tools/generate-e2e-test.js.map +1 -1
  21. package/dist/tools/get-failure.js +2 -1
  22. package/dist/tools/get-failure.js.map +1 -1
  23. package/dist/tools/inspect-page.d.ts +94 -0
  24. package/dist/tools/inspect-page.js +42 -5
  25. package/dist/tools/inspect-page.js.map +1 -1
  26. package/dist/tools/load-report.d.ts +48 -0
  27. package/dist/tools/load-report.js +167 -0
  28. package/dist/tools/load-report.js.map +1 -0
  29. package/dist/tools/probe-script.d.ts +7 -0
  30. package/dist/tools/probe-script.js +266 -7
  31. package/dist/tools/probe-script.js.map +1 -1
  32. package/dist/tools/shared.d.ts +137 -2
  33. package/dist/tools/shared.js +176 -9
  34. package/dist/tools/shared.js.map +1 -1
  35. package/dist/tools/suggest-fix.d.ts +35 -1
  36. package/dist/tools/suggest-fix.js +197 -17
  37. package/dist/tools/suggest-fix.js.map +1 -1
  38. package/dist/tools/validate-selector.d.ts +84 -0
  39. package/dist/tools/validate-selector.js +7 -1
  40. package/dist/tools/validate-selector.js.map +1 -1
  41. package/dist/types/index.d.ts +28 -0
  42. package/dist/utils/project-detector.js +2 -2
  43. package/dist/utils/project-detector.js.map +1 -1
  44. package/dist/utils/report-parser.d.ts +28 -3
  45. package/dist/utils/report-parser.js +77 -11
  46. package/dist/utils/report-parser.js.map +1 -1
  47. package/dist/utils/run-history.d.ts +66 -3
  48. package/dist/utils/run-history.js +149 -3
  49. package/dist/utils/run-history.js.map +1 -1
  50. package/dist/utils/serverless-chromium.d.ts +32 -0
  51. package/dist/utils/serverless-chromium.js +64 -0
  52. package/dist/utils/serverless-chromium.js.map +1 -0
  53. package/package.json +3 -2
  54. package/skill/playwright-e2e-mcp/SKILL.md +71 -15
  55. package/skill/playwright-e2e-mcp/references/setup.md +1 -1
  56. 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');
@@ -439,6 +592,12 @@ async function runActions(page, actions, timeout) {
439
592
  try {
440
593
  pw = req('@playwright/test');
441
594
  } catch (errB) {
595
+ // The hosted bridge ships playwright-core next to its bundled Chromium.
596
+ try {
597
+ if (cfg.serverlessChromium) pw = req('playwright-core');
598
+ } catch (errC) {}
599
+ }
600
+ if (!pw) {
442
601
  send({
443
602
  ok: false,
444
603
  kind: 'NO_PLAYWRIGHT',
@@ -450,8 +609,16 @@ async function runActions(page, actions, timeout) {
450
609
  }
451
610
 
452
611
  let browser = null;
612
+ let openContext = null;
453
613
  let actionsDone = [];
614
+ // A recorded HAR is written when its context closes, so close the
615
+ // context before the browser.
616
+ const closeBrowser = async () => {
617
+ if (openContext) await openContext.close().catch(() => undefined);
618
+ await browser.close();
619
+ };
454
620
  try {
621
+ const engine = cfg.browser || 'chromium';
455
622
  const launchOptions = { headless: true };
456
623
  if (cfg.blockPrivate) {
457
624
  // SSRF guard: every browser connection (each redirect hop and
@@ -461,16 +628,77 @@ async function runActions(page, actions, timeout) {
461
628
  launchOptions.proxy = { server: 'http://127.0.0.1:' + proxyPort };
462
629
  // WebRTC STUN/TURN over UDP does not go through the proxy; keep it off
463
630
  // 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'];
631
+ if (engine === 'firefox') {
632
+ launchOptions.firefoxUserPrefs = { 'network.proxy.allow_hijacking_localhost': true, 'media.peerconnection.enabled': false };
633
+ } else {
634
+ launchOptions.args = ['--proxy-bypass-list=<-loopback>', '--force-webrtc-ip-handling-policy=disable_non_proxied_udp'];
635
+ }
465
636
  }
466
- browser = await pw.chromium.launch(launchOptions);
637
+ if (cfg.serverlessChromium) {
638
+ launchOptions.executablePath = cfg.serverlessChromium.executablePath;
639
+ launchOptions.args = cfg.serverlessChromium.args.concat(launchOptions.args || []);
640
+ }
641
+ browser = await pw[engine].launch(launchOptions);
467
642
  const contextOptions = { viewport: cfg.viewport || { width: 1280, height: 720 } };
643
+ if (cfg.mobile) Object.assign(contextOptions, { isMobile: true, hasTouch: true, deviceScaleFactor: 1.75 });
468
644
  if (cfg.storageState) contextOptions.storageState = cfg.storageState;
469
645
  if (cfg.headers) contextOptions.extraHTTPHeaders = cfg.headers;
470
646
  if (cfg.testIdAttribute) pw.selectors.setTestIdAttribute(cfg.testIdAttribute);
647
+ const recording = cfg.har && cfg.har.mode === 'record' && !cfg.snapshotHtmlPath;
648
+ if (recording) contextOptions.recordHar = { path: cfg.har.path, urlFilter: cfg.har.url, mode: 'minimal' };
471
649
  const context = await browser.newContext(contextOptions);
650
+ openContext = context;
651
+ // Replay goes first so the network mocks below, registered later, win.
652
+ if (cfg.har && cfg.har.mode === 'replay' && !cfg.snapshotHtmlPath) {
653
+ await context.routeFromHAR(cfg.har.path, { url: cfg.har.url, notFound: cfg.har.notFound || 'abort' });
654
+ }
655
+
656
+ // Network mocks. Playwright runs the most recently registered handler
657
+ // first, so register in reverse to make the first listed mock win.
658
+ const routeHits = (cfg.routes || []).map(() => 0);
659
+ if (cfg.routes && cfg.routes.length && !cfg.snapshotHtmlPath) {
660
+ for (let i = cfg.routes.length - 1; i >= 0; i -= 1) {
661
+ const r = cfg.routes[i];
662
+ await context.route(r.url, async (route) => {
663
+ if (r.method && route.request().method() !== r.method) return route.fallback();
664
+ routeHits[i] += 1;
665
+ if (r.delayMs) await new Promise((resolve) => setTimeout(resolve, r.delayMs));
666
+ if (r.abort) return route.abort('failed');
667
+ if (r.status !== undefined) {
668
+ return route.fulfill({ status: r.status, body: r.body === undefined ? '' : r.body, contentType: r.contentType, headers: r.headers });
669
+ }
670
+ return route.fallback();
671
+ });
672
+ }
673
+ }
674
+
675
+ if (cfg.mode === 'perf') await context.addInitScript(perfInit);
472
676
  const page = await context.newPage();
473
677
 
678
+ // perf: count bytes per request over CDP (cross-origin sizes included)
679
+ // and apply CPU / network throttling before the first byte.
680
+ const requests = new Map();
681
+ if (cfg.mode === 'perf') {
682
+ const cdp = await context.newCDPSession(page);
683
+ cdp.on('Network.responseReceived', (e) => {
684
+ 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 }));
685
+ });
686
+ cdp.on('Network.loadingFinished', (e) => {
687
+ const r = requests.get(e.requestId);
688
+ if (r) r.bytes = e.encodedDataLength;
689
+ });
690
+ await cdp.send('Network.enable');
691
+ if (cfg.network) {
692
+ await cdp.send('Network.emulateNetworkConditions', {
693
+ offline: false,
694
+ latency: cfg.network.latencyMs,
695
+ downloadThroughput: cfg.network.downloadBps,
696
+ uploadThroughput: cfg.network.uploadBps,
697
+ });
698
+ }
699
+ if (cfg.cpuThrottle && cfg.cpuThrottle > 1) await cdp.send('Emulation.setCPUThrottlingRate', { rate: cfg.cpuThrottle });
700
+ }
701
+
474
702
  const consoleMessages = [];
475
703
  page.on('console', (msg) => {
476
704
  if (consoleMessages.length < 50) consoleMessages.push({ type: msg.type(), text: String(msg.text()).slice(0, 300) });
@@ -487,7 +715,15 @@ async function runActions(page, actions, timeout) {
487
715
  } else {
488
716
  await page.goto(cfg.url, { waitUntil: cfg.waitUntil || 'domcontentloaded', timeout: cfg.gotoTimeout || 15000 });
489
717
  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);
718
+ // perf: let late images, fonts and shifts land before anything else.
719
+ if (cfg.mode === 'perf' && cfg.settleMs) await page.waitForTimeout(cfg.settleMs);
720
+ if (cfg.actions && cfg.actions.length) {
721
+ actionsDone = await runActions(page, cfg.actions, cfg.actionTimeout || 10000);
722
+ if (cfg.mode === 'perf') {
723
+ await page.waitForLoadState('load', { timeout: cfg.waitTimeout || 5000 }).catch(() => undefined);
724
+ await page.waitForTimeout(Math.min(cfg.settleMs || 1000, 2000));
725
+ }
726
+ }
491
727
  }
492
728
 
493
729
  if (cfg.mode === 'screenshot') {
@@ -507,10 +743,11 @@ async function runActions(page, actions, timeout) {
507
743
  matchCount: 0,
508
744
  elements: [],
509
745
  actionsDone: actionsDone,
746
+ routeHits: routeHits,
510
747
  durationMs: Date.now() - cfg.startedAt,
511
748
  },
512
749
  });
513
- await browser.close();
750
+ await closeBrowser();
514
751
  process.exit(0);
515
752
  }
516
753
 
@@ -584,6 +821,13 @@ async function runActions(page, actions, timeout) {
584
821
  };
585
822
  }, { tags: cfg.axeTags || [], selector: cfg.selector, maxNodes: cfg.maxNodes || 3 });
586
823
  await attachLocators(page, data.elements, 40, 2);
824
+ } else if (cfg.mode === 'perf') {
825
+ const collected = await page.evaluate(collectPerf);
826
+ const network = summariseRequests(requests, collected.timings, page.url());
827
+ delete collected.timings;
828
+ Object.assign(collected.perf, network);
829
+ data = Object.assign(collected, { matchCount: 0, elementCount: collected.elements.length });
830
+ await attachLocators(page, data.elements, 12, 2);
587
831
  } else if (cfg.mode === 'heal') {
588
832
  data = await page.evaluate((input) => {
589
833
  const h = window.__pwmcp;
@@ -612,6 +856,20 @@ async function runActions(page, actions, timeout) {
612
856
  return { matchCount: elements.length, elementCount: document.querySelectorAll('*').length, elements: elements, title: document.title };
613
857
  }, { tokens: cfg.healTokens || [], role: cfg.healRole });
614
858
  await attachLocators(page, data.elements, 5, 3);
859
+ } else if (cfg.view === 'aria') {
860
+ const scope = cfg.selector ? page.locator(cfg.selector) : page.locator('body');
861
+ const count = await scope.count();
862
+ data = { matchCount: count, elementCount: count, elements: [], title: await page.title(), finalUrl: page.url() };
863
+ if (count > 0) {
864
+ if (typeof scope.ariaSnapshot !== 'function') {
865
+ throw new Error('ariaSnapshot needs Playwright 1.49 or newer');
866
+ }
867
+ const snapshot = await scope.first().ariaSnapshot({ timeout: cfg.waitTimeout || 10000 });
868
+ const max = cfg.maxHtmlChars || 12000;
869
+ data.ariaSnapshot = snapshot.slice(0, max);
870
+ data.ariaTruncated = snapshot.length > max;
871
+ }
872
+ data.viewport = page.viewportSize() || undefined;
615
873
  } else {
616
874
  data = await page.evaluate((input) => {
617
875
  const h = window.__pwmcp;
@@ -671,16 +929,17 @@ async function runActions(page, actions, timeout) {
671
929
 
672
930
  data.consoleMessages = consoleMessages;
673
931
  data.actionsDone = actionsDone;
932
+ data.routeHits = routeHits;
674
933
  data.finalUrl = data.finalUrl || page.url();
675
934
  data.durationMs = Date.now() - cfg.startedAt;
676
935
  send({ ok: true, data: data });
677
- await browser.close();
936
+ await closeBrowser();
678
937
  process.exit(0);
679
938
  } catch (err) {
680
939
  const message = err && err.message ? err.message : String(err);
681
940
  const classified = classifyError(message);
682
941
  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 */ }
942
+ try { if (browser) await closeBrowser(); } catch (closeErr) { /* ignore */ }
684
943
  process.exit(1);
685
944
  }
686
945
  })().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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAw5BrC,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;