cbrowser 19.6.0 → 20.0.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 (171) hide show
  1. package/README.md +292 -232
  2. package/THIRD_PARTY_LICENSES.md +416 -7
  3. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  4. package/dist/analysis/accessibility-empathy.js +2 -0
  5. package/dist/analysis/accessibility-empathy.js.map +1 -1
  6. package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
  7. package/dist/analysis/competitive-benchmark.js +70 -558
  8. package/dist/analysis/competitive-benchmark.js.map +1 -1
  9. package/dist/analysis/natural-language.d.ts.map +1 -1
  10. package/dist/analysis/natural-language.js +43 -0
  11. package/dist/analysis/natural-language.js.map +1 -1
  12. package/dist/analysis/persona-comparison.d.ts +7 -0
  13. package/dist/analysis/persona-comparison.d.ts.map +1 -1
  14. package/dist/analysis/persona-comparison.js +8 -1
  15. package/dist/analysis/persona-comparison.js.map +1 -1
  16. package/dist/browser-options.d.ts +23 -0
  17. package/dist/browser-options.d.ts.map +1 -0
  18. package/dist/browser-options.js +24 -0
  19. package/dist/browser-options.js.map +1 -0
  20. package/dist/browser.d.ts +36 -14
  21. package/dist/browser.d.ts.map +1 -1
  22. package/dist/browser.js +139 -196
  23. package/dist/browser.js.map +1 -1
  24. package/dist/cli-args.d.ts +9 -1
  25. package/dist/cli-args.d.ts.map +1 -1
  26. package/dist/cli-args.js +35 -7
  27. package/dist/cli-args.js.map +1 -1
  28. package/dist/cli-browser-flags.d.ts +33 -0
  29. package/dist/cli-browser-flags.d.ts.map +1 -0
  30. package/dist/cli-browser-flags.js +89 -0
  31. package/dist/cli-browser-flags.js.map +1 -0
  32. package/dist/cli.js +990 -412
  33. package/dist/cli.js.map +1 -1
  34. package/dist/cognitive/index.d.ts +0 -13
  35. package/dist/cognitive/index.d.ts.map +1 -1
  36. package/dist/cognitive/index.js +39 -15
  37. package/dist/cognitive/index.js.map +1 -1
  38. package/dist/config.d.ts +2 -2
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +16 -6
  41. package/dist/config.js.map +1 -1
  42. package/dist/daemon.d.ts +5 -4
  43. package/dist/daemon.d.ts.map +1 -1
  44. package/dist/daemon.js +87 -29
  45. package/dist/daemon.js.map +1 -1
  46. package/dist/devices.d.ts +20 -0
  47. package/dist/devices.d.ts.map +1 -1
  48. package/dist/devices.js +18 -0
  49. package/dist/devices.js.map +1 -1
  50. package/dist/help-client.d.ts +48 -0
  51. package/dist/help-client.d.ts.map +1 -0
  52. package/dist/help-client.js +96 -0
  53. package/dist/help-client.js.map +1 -0
  54. package/dist/mcp-server.d.ts.map +1 -1
  55. package/dist/mcp-server.js +165 -2281
  56. package/dist/mcp-server.js.map +1 -1
  57. package/dist/mcp-tools/base/advanced-interaction-tools.d.ts.map +1 -1
  58. package/dist/mcp-tools/base/advanced-interaction-tools.js +21 -21
  59. package/dist/mcp-tools/base/advanced-interaction-tools.js.map +1 -1
  60. package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -1
  61. package/dist/mcp-tools/base/assertion-tools.js +6 -3
  62. package/dist/mcp-tools/base/assertion-tools.js.map +1 -1
  63. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  64. package/dist/mcp-tools/base/audit-tools.js +15 -4
  65. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  66. package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
  67. package/dist/mcp-tools/base/browser-state-tools.js +33 -27
  68. package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
  69. package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
  70. package/dist/mcp-tools/base/bug-analysis-tools.js +9 -2
  71. package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
  72. package/dist/mcp-tools/base/capture-tools.d.ts +60 -4
  73. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  74. package/dist/mcp-tools/base/capture-tools.js +118 -14
  75. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  76. package/dist/mcp-tools/base/index.d.ts +6 -5
  77. package/dist/mcp-tools/base/index.d.ts.map +1 -1
  78. package/dist/mcp-tools/base/index.js +7 -6
  79. package/dist/mcp-tools/base/index.js.map +1 -1
  80. package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
  81. package/dist/mcp-tools/base/interaction-tools.js +20 -6
  82. package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
  83. package/dist/mcp-tools/base/navigation-tools.d.ts +1 -1
  84. package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
  85. package/dist/mcp-tools/base/navigation-tools.js +83 -3
  86. package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
  87. package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
  88. package/dist/mcp-tools/base/site-knowledge-tools.js +11 -23
  89. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  90. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  91. package/dist/mcp-tools/base/visual-testing-tools.js +13 -3
  92. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  93. package/dist/mcp-tools/enterprise-stubs.d.ts +2 -2
  94. package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -1
  95. package/dist/mcp-tools/enterprise-stubs.js +29 -16
  96. package/dist/mcp-tools/enterprise-stubs.js.map +1 -1
  97. package/dist/mcp-tools/index.d.ts +19 -14
  98. package/dist/mcp-tools/index.d.ts.map +1 -1
  99. package/dist/mcp-tools/index.js +2 -2
  100. package/dist/mcp-tools/index.js.map +1 -1
  101. package/dist/mcp-tools/red-zone-refusal.d.ts +33 -0
  102. package/dist/mcp-tools/red-zone-refusal.d.ts.map +1 -0
  103. package/dist/mcp-tools/red-zone-refusal.js +43 -0
  104. package/dist/mcp-tools/red-zone-refusal.js.map +1 -0
  105. package/dist/mcp-tools/tier-gate.js +1 -1
  106. package/dist/mcp-tools/tier-gate.js.map +1 -1
  107. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  108. package/dist/mcp-tools/tool-categories.js +2 -0
  109. package/dist/mcp-tools/tool-categories.js.map +1 -1
  110. package/dist/output.d.ts.map +1 -1
  111. package/dist/output.js +18 -2
  112. package/dist/output.js.map +1 -1
  113. package/dist/performance/metrics.d.ts +8 -3
  114. package/dist/performance/metrics.d.ts.map +1 -1
  115. package/dist/performance/metrics.js +16 -5
  116. package/dist/performance/metrics.js.map +1 -1
  117. package/dist/recording/contact-sheet.js.map +1 -1
  118. package/dist/security/tool-permissions.d.ts.map +1 -1
  119. package/dist/security/tool-permissions.js +1 -0
  120. package/dist/security/tool-permissions.js.map +1 -1
  121. package/dist/testing/coverage.d.ts +2 -2
  122. package/dist/testing/coverage.d.ts.map +1 -1
  123. package/dist/testing/coverage.js +6 -2
  124. package/dist/testing/coverage.js.map +1 -1
  125. package/dist/testing/flaky-detection.d.ts +2 -2
  126. package/dist/testing/flaky-detection.d.ts.map +1 -1
  127. package/dist/testing/flaky-detection.js +11 -3
  128. package/dist/testing/flaky-detection.js.map +1 -1
  129. package/dist/testing/nl-test-suite.d.ts +13 -0
  130. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  131. package/dist/testing/nl-test-suite.js +5 -1
  132. package/dist/testing/nl-test-suite.js.map +1 -1
  133. package/dist/testing/suite-results.d.ts +27 -0
  134. package/dist/testing/suite-results.d.ts.map +1 -0
  135. package/dist/testing/suite-results.js +54 -0
  136. package/dist/testing/suite-results.js.map +1 -0
  137. package/dist/testing/test-repair.d.ts +2 -2
  138. package/dist/testing/test-repair.d.ts.map +1 -1
  139. package/dist/testing/test-repair.js +6 -2
  140. package/dist/testing/test-repair.js.map +1 -1
  141. package/dist/types.d.ts +61 -7
  142. package/dist/types.d.ts.map +1 -1
  143. package/dist/types.js +45 -32
  144. package/dist/types.js.map +1 -1
  145. package/dist/visual/ab-comparison.d.ts.map +1 -1
  146. package/dist/visual/ab-comparison.js +7 -2
  147. package/dist/visual/ab-comparison.js.map +1 -1
  148. package/dist/visual/cross-browser.d.ts +7 -2
  149. package/dist/visual/cross-browser.d.ts.map +1 -1
  150. package/dist/visual/cross-browser.js +33 -5
  151. package/dist/visual/cross-browser.js.map +1 -1
  152. package/dist/visual/distance-metrics.d.ts +7 -0
  153. package/dist/visual/distance-metrics.d.ts.map +1 -1
  154. package/dist/visual/distance-metrics.js.map +1 -1
  155. package/dist/visual/heatmap-overlay.js.map +1 -1
  156. package/dist/visual/llm-relevance.d.ts.map +1 -1
  157. package/dist/visual/llm-relevance.js.map +1 -1
  158. package/dist/visual/regression.d.ts +17 -7
  159. package/dist/visual/regression.d.ts.map +1 -1
  160. package/dist/visual/regression.js +77 -38
  161. package/dist/visual/regression.js.map +1 -1
  162. package/dist/visual/responsive.d.ts +22 -0
  163. package/dist/visual/responsive.d.ts.map +1 -1
  164. package/dist/visual/responsive.js +44 -7
  165. package/dist/visual/responsive.js.map +1 -1
  166. package/dist/visual/visual-overlays.js.map +1 -1
  167. package/dist/webhooks.d.ts +67 -0
  168. package/dist/webhooks.d.ts.map +1 -0
  169. package/dist/webhooks.js +128 -0
  170. package/dist/webhooks.js.map +1 -0
  171. package/package.json +23 -24
package/dist/browser.js CHANGED
@@ -259,6 +259,8 @@ export class CBrowser {
259
259
  // =========================================================================
260
260
  // Session State Persistence
261
261
  // =========================================================================
262
+ /** True when this run was given --device or --viewport (see launch). */
263
+ explicitPageShape = false;
262
264
  get sessionStateFile() {
263
265
  return join(this.paths.dataDir, "browser-state", "last-session.json");
264
266
  }
@@ -318,11 +320,22 @@ export class CBrowser {
318
320
  // Don't save about:blank or empty URLs
319
321
  if (!url || url === "about:blank" || url === "")
320
322
  return;
323
+ // Device and location persist only when set on purpose (`device set`,
324
+ // `geo set`), so carry those over from the file. Saving this run's
325
+ // values made a one-off --device or --geo stick to every later command
326
+ // for an hour (audit 2026-10-21).
327
+ let previous = null;
328
+ try {
329
+ if (existsSync(this.sessionStateFile))
330
+ previous = JSON.parse(readFileSync(this.sessionStateFile, "utf-8"));
331
+ }
332
+ catch { /* unreadable: start fresh */ }
321
333
  const state = {
322
334
  url,
323
335
  timestamp: Date.now(),
324
336
  viewport,
325
- device: device || this.config.device,
337
+ device: device ?? previous?.device,
338
+ ...(previous?.geolocation ? { geolocation: previous.geolocation } : {}),
326
339
  };
327
340
  const stateDir = join(this.paths.dataDir, "browser-state");
328
341
  if (!existsSync(stateDir)) {
@@ -363,6 +376,30 @@ export class CBrowser {
363
376
  console.error(`Failed to save device setting: ${e.message}`);
364
377
  }
365
378
  }
379
+ /**
380
+ * Save a geolocation to session state, so later commands launch with it
381
+ * (`cbrowser geo set`). Returns false when the state could not be written.
382
+ * Before 2026-10-21, geo set changed a browser that was never launched and
383
+ * printed success, so nothing changed.
384
+ */
385
+ saveGeolocationSetting(geo) {
386
+ try {
387
+ const stateDir = join(this.paths.dataDir, "browser-state");
388
+ if (!existsSync(stateDir))
389
+ mkdirSync(stateDir, { recursive: true });
390
+ const state = existsSync(this.sessionStateFile)
391
+ ? JSON.parse(readFileSync(this.sessionStateFile, "utf-8"))
392
+ : { url: "", timestamp: Date.now() };
393
+ state.geolocation = geo;
394
+ state.timestamp = Date.now();
395
+ writeFileSync(this.sessionStateFile, JSON.stringify(state, null, 2));
396
+ return true;
397
+ }
398
+ catch (e) {
399
+ console.error(`Failed to save geolocation: ${e.message}`);
400
+ return false;
401
+ }
402
+ }
366
403
  loadSessionState() {
367
404
  try {
368
405
  if (!existsSync(this.sessionStateFile))
@@ -477,6 +514,15 @@ For more help: https://playwright.dev/docs/browsers
477
514
  }
478
515
  this.config.device = resolved.key;
479
516
  }
517
+ // Was the page shape asked for on this run? Then the saved session's
518
+ // viewport must not overwrite it after launch.
519
+ this.explicitPageShape = !!this.config.device || !!this.config.viewportOverride;
520
+ // Restore a location saved by `cbrowser geo set` unless one was given.
521
+ if (this.config.persistent && !this.config.geolocation) {
522
+ const saved = this.loadSessionState();
523
+ if (saved?.geolocation)
524
+ this.config.geolocation = saved.geolocation;
525
+ }
480
526
  // Load saved session state to restore device setting if not explicitly set
481
527
  if (this.config.persistent && !this.config.device) {
482
528
  const savedSession = this.loadSessionState();
@@ -746,7 +792,8 @@ For more help: https://playwright.dev/docs/browsers
746
792
  await this.page.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => { });
747
793
  await this.page.waitForTimeout(1000);
748
794
  // Apply saved viewport if available
749
- if (savedSession.viewport) {
795
+ // An explicit --device or --viewport wins over last run's size.
796
+ if (savedSession.viewport && !this.explicitPageShape) {
750
797
  await this.page.setViewportSize(savedSession.viewport);
751
798
  }
752
799
  }
@@ -881,7 +928,7 @@ For more help: https://playwright.dev/docs/browsers
881
928
  // Wait for network idle and JS hydration
882
929
  await this.page.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => { });
883
930
  await this.page.waitForTimeout(1000);
884
- if (savedSession.viewport) {
931
+ if (savedSession.viewport && !this.explicitPageShape) {
885
932
  await this.page.setViewportSize(savedSession.viewport);
886
933
  }
887
934
  }
@@ -1685,7 +1732,8 @@ For more help: https://playwright.dev/docs/browsers
1685
1732
  const filtered = cookies.filter((c) => {
1686
1733
  if (c.name !== name)
1687
1734
  return true;
1688
- if (domain && c.domain !== domain)
1735
+ // ".example.com" and "example.com" name the same cookie domain.
1736
+ if (domain && c.domain.replace(/^\./, "") !== domain.replace(/^\./, ""))
1689
1737
  return true;
1690
1738
  return false;
1691
1739
  });
@@ -1733,6 +1781,37 @@ For more help: https://playwright.dev/docs/browsers
1733
1781
  this.config.device = resolved.key;
1734
1782
  return true;
1735
1783
  }
1784
+ /**
1785
+ * Emulate a device preset and/or set the page size, relaunching if the
1786
+ * browser is already up (context options are fixed at launch). An unknown
1787
+ * device throws, as at launch. Returns the device key applied.
1788
+ */
1789
+ async emulate(opts) {
1790
+ let key;
1791
+ if (opts.device) {
1792
+ const resolved = resolveDevice(opts.device);
1793
+ if ("error" in resolved) {
1794
+ const closest = resolved.suggestions.length > 0 ? ` Closest presets: ${resolved.suggestions.join(", ")}.` : "";
1795
+ throw new CBrowserError(CBrowserErrorCode.CONFIG_INVALID, `${resolved.error}.${closest}`, {
1796
+ device: opts.device,
1797
+ suggestions: resolved.suggestions,
1798
+ });
1799
+ }
1800
+ key = resolved.key;
1801
+ }
1802
+ if (this.browser || this.context)
1803
+ await this.close();
1804
+ if (key)
1805
+ this.config.device = key;
1806
+ if (opts.viewport) {
1807
+ this.config.viewportOverride = { ...opts.viewport };
1808
+ if (!key) {
1809
+ this.config.viewportWidth = opts.viewport.width;
1810
+ this.config.viewportHeight = opts.viewport.height;
1811
+ }
1812
+ }
1813
+ return { device: this.config.device ?? null };
1814
+ }
1736
1815
  /**
1737
1816
  * List available device presets.
1738
1817
  */
@@ -5534,202 +5613,57 @@ For more help: https://playwright.dev/docs/browsers
5534
5613
  // Journeys
5535
5614
  // =========================================================================
5536
5615
  /**
5537
- * Run an autonomous journey.
5616
+ * Run an autonomous journey: a model-driven cognitive journey for one persona.
5538
5617
  *
5539
- * When API key is configured, uses AI-driven cognitive journeys (Claude API)
5540
- * for realistic user simulation with cognitive state tracking.
5618
+ * Needs an Anthropic API key. There is no keyless fallback any more (20.0.0):
5619
+ * the old heuristic explorer clicked the highest keyword match among the first
5620
+ * twenty links and called the goal reached when any goal word, "the"
5621
+ * included, appeared anywhere on the page. Measured 2026-10-21: "buy the blue
5622
+ * widget" on a page with no widget reported success for every persona, with
5623
+ * identical click paths. A result that is always yes is not a measurement.
5541
5624
  *
5542
- * Without API key, falls back to heuristic exploration (simplified).
5543
- *
5544
- * @requires ANTHROPIC_API_KEY for cognitive mode (set via `npx cbrowser config set-api-key`)
5625
+ * @requires ANTHROPIC_API_KEY (set via `npx cbrowser config set-api-key`)
5545
5626
  */
5546
5627
  async journey(options) {
5547
5628
  const { persona: personaName, startUrl, goal, maxSteps = 20, maxTime = 120 } = options;
5548
- // Check if cognitive journeys are available (API key configured)
5549
- if (isApiKeyConfigured()) {
5550
- try {
5551
- const cognitiveResult = await runCognitiveJourney({
5552
- persona: personaName,
5553
- startUrl,
5554
- goal,
5555
- maxSteps,
5556
- maxTime,
5557
- headless: this.config.headless,
5558
- vision: false,
5559
- verbose: false,
5560
- });
5561
- // Map cognitive journey result to JourneyResult format
5562
- return {
5563
- persona: personaName,
5564
- goal,
5565
- steps: cognitiveResult.frictionPoints.map((fp, _i) => ({
5566
- action: fp.type,
5567
- target: fp.element || 'page',
5568
- result: fp.monologue.substring(0, 100),
5569
- screenshot: fp.screenshot,
5570
- timestamp: new Date().toISOString(),
5571
- })),
5572
- success: cognitiveResult.goalAchieved,
5573
- frictionPoints: cognitiveResult.frictionPoints.map(fp => `${fp.type}: ${fp.monologue.substring(0, 80)}`),
5574
- totalTime: cognitiveResult.totalTime * 1000, // Convert to ms
5575
- consoleLogs: [],
5576
- cognitive: {
5577
- patienceRemaining: cognitiveResult.finalState.patienceRemaining,
5578
- frustrationLevel: cognitiveResult.finalState.frustrationLevel,
5579
- confusionLevel: cognitiveResult.finalState.confusionLevel,
5580
- abandonmentReason: cognitiveResult.abandonmentReason,
5581
- backtrackCount: cognitiveResult.summary.backtrackCount,
5582
- monologue: cognitiveResult.fullMonologue,
5583
- },
5584
- };
5585
- }
5586
- catch (e) {
5587
- console.warn(`Cognitive journey failed, falling back to heuristic: ${e.message}`);
5588
- // Fall through to heuristic mode
5589
- }
5590
- }
5591
- // Heuristic mode (no API key or cognitive journey failed)
5592
- // NOTE: This is simplified exploration. For realistic user simulation,
5593
- // configure an API key: npx cbrowser config set-api-key YOUR_KEY
5594
- const persona = getPersonaOrRefuse(personaName);
5595
- this.currentPersona = persona;
5596
- // Set viewport based on persona
5597
- if (persona.context?.viewport) {
5598
- this.config.viewportWidth = persona.context.viewport[0];
5599
- this.config.viewportHeight = persona.context.viewport[1];
5629
+ if (!isApiKeyConfigured()) {
5630
+ throw new Error("A journey needs an Anthropic API key (cbrowser config set-api-key <key>). The keyless heuristic explorer was removed in 20.0.0 because its results were not accurate.");
5600
5631
  }
5601
- const page = await this.getPage();
5602
- const consoleLogs = [];
5603
- const frictionPoints = [];
5604
- const startTime = Date.now();
5605
- // Capture console logs. Removed in the finally below (v8 B27, 2026-10-10):
5606
- // it used to stay on the tab, which is the shared page in a hosted session,
5607
- // so every heuristic journey left one more listener and log array behind
5608
- // (measured 4 -> 7 console listeners over three runs). Not read from the
5609
- // tab's console buffer instead: that keeps 500 entries and omits an empty
5610
- // location, so a noisy page or an inline script would change consoleLogs.
5611
- const onConsole = (msg) => {
5612
- const entry = {
5613
- type: msg.type(),
5614
- text: msg.text().substring(0, 500),
5615
- timestamp: new Date().toISOString(),
5616
- url: msg.location()?.url,
5617
- lineNumber: msg.location()?.lineNumber,
5618
- };
5619
- consoleLogs.push(entry);
5620
- if (msg.type() === "error") {
5621
- frictionPoints.push(`Console error: ${msg.text().substring(0, 100)}`);
5622
- }
5623
- };
5624
- page.on("console", onConsole);
5625
- try {
5626
- return await this.heuristicJourney(page, persona, options, consoleLogs, frictionPoints, startTime);
5627
- }
5628
- finally {
5629
- page.off("console", onConsole);
5630
- }
5631
- }
5632
- /** journey()'s heuristic exploration; journey's listener fills `consoleLogs`. */
5633
- async heuristicJourney(page, persona, options, consoleLogs, frictionPoints, startTime) {
5634
- const { persona: personaName, startUrl, goal, maxSteps = 20 } = options;
5635
- const steps = [];
5636
- // Navigate to start
5637
- await this.navigate(startUrl);
5638
- steps.push({
5639
- action: "navigate",
5640
- target: startUrl,
5641
- result: `Loaded: ${await page.title()}`,
5642
- screenshot: await this.screenshot(),
5643
- timestamp: new Date().toISOString(),
5644
- });
5645
- // Heuristic exploration (simplified - no API key available)
5646
- // For realistic AI-driven exploration, set API key: npx cbrowser config set-api-key
5647
- let stepCount = 0;
5648
- let success = false;
5649
- while (stepCount < maxSteps && !success) {
5650
- stepCount++;
5651
- // Wait with human-like timing
5652
- const delay = this.getHumanDelay(persona);
5653
- await new Promise((r) => setTimeout(r, delay));
5654
- // Try to find clickable elements
5655
- const clickable = await page.evaluate((goalText) => {
5656
- const elements = Array.from(document.querySelectorAll('a, button, [role="button"], input[type="submit"]'));
5657
- // Score elements by relevance to goal (deterministic, not random)
5658
- const scored = elements.slice(0, 20).map((el) => {
5659
- const text = el.textContent?.trim().substring(0, 50) || '';
5660
- const href = el.href || '';
5661
- const textLower = text.toLowerCase();
5662
- const goalWords = goalText.toLowerCase().split(' ').filter(w => w.length > 2);
5663
- // Calculate relevance score
5664
- let score = 0;
5665
- for (const word of goalWords) {
5666
- if (textLower.includes(word))
5667
- score += 3;
5668
- if (href.toLowerCase().includes(word))
5669
- score += 2;
5670
- }
5671
- // Boost common action words
5672
- if (textLower.match(/submit|continue|next|proceed|sign|login|register/))
5673
- score += 1;
5674
- return { tag: el.tagName, text, href, score };
5675
- });
5676
- // Sort by score (highest first) and return top 5
5677
- return scored.sort((a, b) => b.score - a.score).slice(0, 5);
5678
- }, goal);
5679
- if (clickable.length === 0) {
5680
- frictionPoints.push("No clickable elements found");
5681
- break;
5682
- }
5683
- // Select highest-scored element (deterministic)
5684
- const target = clickable[0];
5685
- const result = await this.click(target.text || target.tag);
5686
- steps.push({
5687
- action: "click",
5688
- target: target.text || target.tag,
5689
- result: result.message,
5690
- screenshot: result.screenshot,
5691
- timestamp: new Date().toISOString(),
5692
- });
5693
- // Check if goal might be achieved
5694
- const pageText = await page.evaluate(() => document.body.innerText.toLowerCase());
5695
- if (goal.toLowerCase().split(" ").some((word) => pageText.includes(word))) {
5696
- success = true;
5697
- }
5698
- }
5699
- // Save final screenshot
5700
- steps.push({
5701
- action: "final",
5702
- result: `Final page: ${await page.title()}`,
5703
- screenshot: await this.screenshot(),
5704
- timestamp: new Date().toISOString(),
5632
+ // Refuse an unknown persona before spending tokens.
5633
+ this.currentPersona = getPersonaOrRefuse(personaName);
5634
+ const cognitiveResult = await runCognitiveJourney({
5635
+ persona: personaName,
5636
+ startUrl,
5637
+ goal,
5638
+ maxSteps,
5639
+ maxTime,
5640
+ headless: this.config.headless,
5641
+ vision: false,
5642
+ verbose: false,
5705
5643
  });
5706
- const result = {
5644
+ return {
5707
5645
  persona: personaName,
5708
5646
  goal,
5709
- steps,
5710
- success,
5711
- frictionPoints,
5712
- totalTime: Date.now() - startTime,
5713
- consoleLogs,
5647
+ steps: cognitiveResult.frictionPoints.map((fp) => ({
5648
+ action: fp.type,
5649
+ target: fp.element || 'page',
5650
+ result: fp.monologue.substring(0, 100),
5651
+ screenshot: fp.screenshot,
5652
+ timestamp: new Date().toISOString(),
5653
+ })),
5654
+ success: cognitiveResult.goalAchieved,
5655
+ frictionPoints: cognitiveResult.frictionPoints.map(fp => `${fp.type}: ${fp.monologue.substring(0, 80)}`),
5656
+ totalTime: cognitiveResult.totalTime * 1000, // Convert to ms
5657
+ consoleLogs: [],
5658
+ cognitive: {
5659
+ patienceRemaining: cognitiveResult.finalState.patienceRemaining,
5660
+ frustrationLevel: cognitiveResult.finalState.frustrationLevel,
5661
+ confusionLevel: cognitiveResult.finalState.confusionLevel,
5662
+ abandonmentReason: cognitiveResult.abandonmentReason,
5663
+ backtrackCount: cognitiveResult.summary.backtrackCount,
5664
+ monologue: cognitiveResult.fullMonologue,
5665
+ },
5714
5666
  };
5715
- // Save journey results
5716
- const journeyFile = join(this.paths.dataDir, `journey-${Date.now()}.json`);
5717
- writeFileSync(journeyFile, JSON.stringify(result, null, 2));
5718
- return result;
5719
- }
5720
- /**
5721
- * Get human-like delay based on persona.
5722
- * Uses deterministic average of min/max (no random).
5723
- * For realistic variable timing, use cognitive journeys with API key.
5724
- */
5725
- getHumanDelay(persona) {
5726
- const timing = persona.humanBehavior?.timing;
5727
- if (!timing)
5728
- return 500;
5729
- const min = timing.reactionTime.min;
5730
- const max = timing.reactionTime.max;
5731
- // Use midpoint for deterministic behavior (no random)
5732
- return Math.floor((min + max) / 2);
5733
5667
  }
5734
5668
  // =========================================================================
5735
5669
  // Constitutional Safety
@@ -6852,12 +6786,18 @@ For more help: https://playwright.dev/docs/browsers
6852
6786
  */
6853
6787
  exportJUnit(suite, outputPath) {
6854
6788
  const filename = outputPath || join(this.paths.dataDir, `junit-${Date.now()}.xml`);
6789
+ // Names and errors are page and test-file text: escaped, or a test named
6790
+ // "Search & filter" wrote XML no CI parser accepts. A failed test is a
6791
+ // failure with or without an error message; it used to pass without one.
6792
+ const esc = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6793
+ const failures = suite.tests.filter((t) => t.status === "failed").length;
6855
6794
  let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`;
6856
- xml += `<testsuite name="${suite.name}" tests="${suite.tests.length}">\n`;
6795
+ xml += `<testsuite name="${esc(suite.name)}" tests="${suite.tests.length}" failures="${failures}">\n`;
6857
6796
  for (const test of suite.tests) {
6858
- xml += ` <testcase name="${test.name}" time="${(test.duration / 1000).toFixed(3)}">\n`;
6859
- if (test.status === "failed" && test.error) {
6860
- xml += ` <failure message="${test.error.replace(/"/g, "&quot;")}">${test.error}</failure>\n`;
6797
+ xml += ` <testcase name="${esc(test.name)}" time="${(test.duration / 1000).toFixed(3)}">\n`;
6798
+ if (test.status === "failed") {
6799
+ const message = test.error || "failed";
6800
+ xml += ` <failure message="${esc(message)}">${esc(message)}</failure>\n`;
6861
6801
  }
6862
6802
  xml += ` </testcase>\n`;
6863
6803
  }
@@ -6874,7 +6814,8 @@ For more help: https://playwright.dev/docs/browsers
6874
6814
  tap += `1..${suite.tests.length}\n`;
6875
6815
  suite.tests.forEach((test, i) => {
6876
6816
  const status = test.status === "passed" ? "ok" : "not ok";
6877
- tap += `${status} ${i + 1} ${test.name}\n`;
6817
+ // One line per test, and a "#" in a name is not read as a directive.
6818
+ tap += `${status} ${i + 1} ${test.name.replace(/[\r\n]+/g, " ").replace(/#/g, "\\#")}\n`;
6878
6819
  });
6879
6820
  writeFileSync(filename, tap);
6880
6821
  return filename;
@@ -6921,9 +6862,11 @@ For more help: https://playwright.dev/docs/browsers
6921
6862
  * Run the same task across multiple device configurations in parallel.
6922
6863
  */
6923
6864
  static async parallelDevices(devices, run, options = {}) {
6865
+ // `config` is shared by every device (headless, engine); each task's own
6866
+ // device wins over one in it.
6924
6867
  const tasks = devices.map(device => ({
6925
6868
  name: device,
6926
- config: { device },
6869
+ config: { ...options.config, device },
6927
6870
  run: (browser) => run(browser, device),
6928
6871
  }));
6929
6872
  const results = await CBrowser.parallel(tasks, options);