cbrowser 19.6.0 → 20.0.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 (170) 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 +979 -401
  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.map +1 -1
  39. package/dist/config.js +16 -6
  40. package/dist/config.js.map +1 -1
  41. package/dist/daemon.d.ts +5 -4
  42. package/dist/daemon.d.ts.map +1 -1
  43. package/dist/daemon.js +87 -29
  44. package/dist/daemon.js.map +1 -1
  45. package/dist/devices.d.ts +20 -0
  46. package/dist/devices.d.ts.map +1 -1
  47. package/dist/devices.js +18 -0
  48. package/dist/devices.js.map +1 -1
  49. package/dist/help-client.d.ts +48 -0
  50. package/dist/help-client.d.ts.map +1 -0
  51. package/dist/help-client.js +96 -0
  52. package/dist/help-client.js.map +1 -0
  53. package/dist/mcp-server.d.ts.map +1 -1
  54. package/dist/mcp-server.js +165 -2281
  55. package/dist/mcp-server.js.map +1 -1
  56. package/dist/mcp-tools/base/advanced-interaction-tools.d.ts.map +1 -1
  57. package/dist/mcp-tools/base/advanced-interaction-tools.js +21 -21
  58. package/dist/mcp-tools/base/advanced-interaction-tools.js.map +1 -1
  59. package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -1
  60. package/dist/mcp-tools/base/assertion-tools.js +6 -3
  61. package/dist/mcp-tools/base/assertion-tools.js.map +1 -1
  62. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  63. package/dist/mcp-tools/base/audit-tools.js +15 -4
  64. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  65. package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
  66. package/dist/mcp-tools/base/browser-state-tools.js +33 -27
  67. package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
  68. package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
  69. package/dist/mcp-tools/base/bug-analysis-tools.js +9 -2
  70. package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
  71. package/dist/mcp-tools/base/capture-tools.d.ts +60 -4
  72. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  73. package/dist/mcp-tools/base/capture-tools.js +118 -14
  74. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  75. package/dist/mcp-tools/base/index.d.ts +6 -5
  76. package/dist/mcp-tools/base/index.d.ts.map +1 -1
  77. package/dist/mcp-tools/base/index.js +7 -6
  78. package/dist/mcp-tools/base/index.js.map +1 -1
  79. package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
  80. package/dist/mcp-tools/base/interaction-tools.js +20 -6
  81. package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
  82. package/dist/mcp-tools/base/navigation-tools.d.ts +1 -1
  83. package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
  84. package/dist/mcp-tools/base/navigation-tools.js +83 -3
  85. package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
  86. package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
  87. package/dist/mcp-tools/base/site-knowledge-tools.js +11 -23
  88. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  89. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  90. package/dist/mcp-tools/base/visual-testing-tools.js +13 -3
  91. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  92. package/dist/mcp-tools/enterprise-stubs.d.ts +2 -2
  93. package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -1
  94. package/dist/mcp-tools/enterprise-stubs.js +29 -16
  95. package/dist/mcp-tools/enterprise-stubs.js.map +1 -1
  96. package/dist/mcp-tools/index.d.ts +19 -14
  97. package/dist/mcp-tools/index.d.ts.map +1 -1
  98. package/dist/mcp-tools/index.js +2 -2
  99. package/dist/mcp-tools/index.js.map +1 -1
  100. package/dist/mcp-tools/red-zone-refusal.d.ts +33 -0
  101. package/dist/mcp-tools/red-zone-refusal.d.ts.map +1 -0
  102. package/dist/mcp-tools/red-zone-refusal.js +43 -0
  103. package/dist/mcp-tools/red-zone-refusal.js.map +1 -0
  104. package/dist/mcp-tools/tier-gate.js +1 -1
  105. package/dist/mcp-tools/tier-gate.js.map +1 -1
  106. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  107. package/dist/mcp-tools/tool-categories.js +2 -0
  108. package/dist/mcp-tools/tool-categories.js.map +1 -1
  109. package/dist/output.d.ts.map +1 -1
  110. package/dist/output.js +18 -2
  111. package/dist/output.js.map +1 -1
  112. package/dist/performance/metrics.d.ts +8 -3
  113. package/dist/performance/metrics.d.ts.map +1 -1
  114. package/dist/performance/metrics.js +16 -5
  115. package/dist/performance/metrics.js.map +1 -1
  116. package/dist/recording/contact-sheet.js.map +1 -1
  117. package/dist/security/tool-permissions.d.ts.map +1 -1
  118. package/dist/security/tool-permissions.js +1 -0
  119. package/dist/security/tool-permissions.js.map +1 -1
  120. package/dist/testing/coverage.d.ts +2 -2
  121. package/dist/testing/coverage.d.ts.map +1 -1
  122. package/dist/testing/coverage.js +6 -2
  123. package/dist/testing/coverage.js.map +1 -1
  124. package/dist/testing/flaky-detection.d.ts +2 -2
  125. package/dist/testing/flaky-detection.d.ts.map +1 -1
  126. package/dist/testing/flaky-detection.js +11 -3
  127. package/dist/testing/flaky-detection.js.map +1 -1
  128. package/dist/testing/nl-test-suite.d.ts +13 -0
  129. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  130. package/dist/testing/nl-test-suite.js +5 -1
  131. package/dist/testing/nl-test-suite.js.map +1 -1
  132. package/dist/testing/suite-results.d.ts +27 -0
  133. package/dist/testing/suite-results.d.ts.map +1 -0
  134. package/dist/testing/suite-results.js +54 -0
  135. package/dist/testing/suite-results.js.map +1 -0
  136. package/dist/testing/test-repair.d.ts +2 -2
  137. package/dist/testing/test-repair.d.ts.map +1 -1
  138. package/dist/testing/test-repair.js +6 -2
  139. package/dist/testing/test-repair.js.map +1 -1
  140. package/dist/types.d.ts +61 -7
  141. package/dist/types.d.ts.map +1 -1
  142. package/dist/types.js +17 -4
  143. package/dist/types.js.map +1 -1
  144. package/dist/visual/ab-comparison.d.ts.map +1 -1
  145. package/dist/visual/ab-comparison.js +7 -2
  146. package/dist/visual/ab-comparison.js.map +1 -1
  147. package/dist/visual/cross-browser.d.ts +7 -2
  148. package/dist/visual/cross-browser.d.ts.map +1 -1
  149. package/dist/visual/cross-browser.js +33 -5
  150. package/dist/visual/cross-browser.js.map +1 -1
  151. package/dist/visual/distance-metrics.d.ts +7 -0
  152. package/dist/visual/distance-metrics.d.ts.map +1 -1
  153. package/dist/visual/distance-metrics.js.map +1 -1
  154. package/dist/visual/heatmap-overlay.js.map +1 -1
  155. package/dist/visual/llm-relevance.d.ts.map +1 -1
  156. package/dist/visual/llm-relevance.js.map +1 -1
  157. package/dist/visual/regression.d.ts +17 -7
  158. package/dist/visual/regression.d.ts.map +1 -1
  159. package/dist/visual/regression.js +77 -38
  160. package/dist/visual/regression.js.map +1 -1
  161. package/dist/visual/responsive.d.ts +22 -0
  162. package/dist/visual/responsive.d.ts.map +1 -1
  163. package/dist/visual/responsive.js +44 -7
  164. package/dist/visual/responsive.js.map +1 -1
  165. package/dist/visual/visual-overlays.js.map +1 -1
  166. package/dist/webhooks.d.ts +67 -0
  167. package/dist/webhooks.d.ts.map +1 -0
  168. package/dist/webhooks.js +128 -0
  169. package/dist/webhooks.js.map +1 -0
  170. package/package.json +23 -24
@@ -3,11 +3,14 @@
3
3
  * Copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com
4
4
  * Learn more at https://cbrowser.ai - MIT License
5
5
  */
6
+ /**
7
+ * Competitive UX Benchmark Module
8
+ *
9
+ * Run identical cognitive journeys across multiple sites simultaneously.
10
+ * Output: head-to-head comparison with friction analysis.
11
+ */
6
12
  import { VERSION } from "../version.js";
7
- import { CBrowser } from "../browser.js";
8
- import { navigationError } from "../navigation-errors.js";
9
13
  import { getPersonaOrRefuse } from "../personas.js";
10
- import { getFocusHierarchy, inferTaskTypeFromGoal, calculateFocusPriority, getDistractionIgnoreRate, } from "../cognitive/focus-hierarchies.js";
11
14
  import { runCognitiveJourney, isApiKeyConfigured, } from "../cognitive/index.js";
12
15
  // ============================================================================
13
16
  // Scoring Methodology & Disclaimers
@@ -45,120 +48,6 @@ const ABANDONMENT_RISK_LABELS = [
45
48
  { max: 90, score: 9, label: "Very High" },
46
49
  { max: 100, score: 10, label: "Very High" },
47
50
  ];
48
- /**
49
- * Simulate a user journey toward a goal
50
- */
51
- async function simulateJourney(page, url, goal, persona, maxSteps, maxTime) {
52
- const startTime = Date.now();
53
- const state = {
54
- patience: 100,
55
- frustration: 0,
56
- confusion: 0,
57
- steps: 0,
58
- frictionPoints: [],
59
- startTime,
60
- };
61
- let goalAchieved = false;
62
- let abandonmentReason;
63
- const screenshots = {
64
- start: '',
65
- end: '',
66
- };
67
- try {
68
- // Navigate to starting URL (a failed load in plain words, not the raw Playwright error)
69
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 })
70
- .catch((e) => { throw navigationError(e, url); });
71
- await page.waitForTimeout(1000);
72
- // Take start screenshot
73
- const startBuffer = await page.screenshot();
74
- screenshots.start = startBuffer.toString("base64");
75
- // Check initial page state
76
- const initialAnalysis = await analyzePageForGoal(page, goal);
77
- if (initialAnalysis.goalReached) {
78
- goalAchieved = true;
79
- }
80
- else {
81
- // Simulate exploration toward goal
82
- for (let step = 0; step < maxSteps && !goalAchieved; step++) {
83
- // Check time limit
84
- if (Date.now() - startTime > maxTime * 1000) {
85
- abandonmentReason = "Time limit exceeded";
86
- break;
87
- }
88
- state.steps++;
89
- // Analyze current page for actions
90
- const pageAnalysis = await analyzePageForGoal(page, goal);
91
- if (pageAnalysis.goalReached) {
92
- goalAchieved = true;
93
- break;
94
- }
95
- // Check for friction points
96
- if (pageAnalysis.frictionPoints.length > 0) {
97
- state.frictionPoints.push(...pageAnalysis.frictionPoints);
98
- state.patience -= pageAnalysis.frictionPoints.length * 5;
99
- state.frustration += pageAnalysis.frictionPoints.length * 3;
100
- }
101
- // Try to take an action toward the goal
102
- const action = await findBestAction(page, goal, pageAnalysis);
103
- if (!action) {
104
- state.confusion += 10;
105
- state.patience -= 5;
106
- if (state.patience <= 20 || state.confusion >= 70) {
107
- abandonmentReason = "User became confused and lost";
108
- break;
109
- }
110
- // Try scrolling to find more options
111
- await page.evaluate(() => window.scrollBy(0, 300));
112
- await page.waitForTimeout(200);
113
- continue;
114
- }
115
- // Execute the action
116
- try {
117
- await executeAction(page, action);
118
- await page.waitForTimeout(300);
119
- // Patience recovery on success
120
- state.patience = Math.min(100, state.patience + 2);
121
- }
122
- catch (e) {
123
- state.frictionPoints.push(`Failed to ${action.type}: ${action.target}`);
124
- state.frustration += 5;
125
- state.patience -= 10;
126
- if (state.patience <= 10) {
127
- abandonmentReason = "Too many failed interactions";
128
- break;
129
- }
130
- }
131
- // Check for patience exhaustion
132
- if (state.patience <= 0) {
133
- abandonmentReason = "User ran out of patience";
134
- break;
135
- }
136
- }
137
- }
138
- // Take end screenshot
139
- const endBuffer = await page.screenshot();
140
- screenshots.end = endBuffer.toString("base64");
141
- }
142
- catch (e) {
143
- abandonmentReason = `Error: ${e.message}`;
144
- }
145
- // Use full URL (without protocol) as site name to distinguish subpaths
146
- const parsed = new URL(url);
147
- const siteName = parsed.hostname.replace(/^www\d*\./, '') + (parsed.pathname !== '/' ? parsed.pathname.replace(/\/$/, '') : '');
148
- return {
149
- url,
150
- siteName,
151
- goalAchieved,
152
- abandonmentReason,
153
- totalTime: Date.now() - startTime,
154
- stepCount: state.steps,
155
- frictionPoints: [...new Set(state.frictionPoints)], // Dedupe
156
- confusionLevel: Math.round(state.confusion),
157
- frustrationLevel: Math.round(state.frustration),
158
- abandonmentRisk: calculateAbandonmentRisk(state, goalAchieved),
159
- screenshots,
160
- };
161
- }
162
51
  /**
163
52
  * Calculate abandonment risk as a letter grade.
164
53
  *
@@ -195,387 +84,6 @@ function getAbandonmentRiskRating(risk) {
195
84
  }
196
85
  return { score: 10, label: "Very High" };
197
86
  }
198
- async function analyzePageForGoal(page, goal) {
199
- const goalLower = goal.toLowerCase();
200
- const frictionPoints = [];
201
- // Extract page content for analysis with position data for focus hierarchy
202
- const pageData = await page.evaluate(() => {
203
- const viewportHeight = window.innerHeight;
204
- const viewportWidth = window.innerWidth;
205
- // Helper: detect page area from element position and context
206
- function detectArea(el) {
207
- const rect = el.getBoundingClientRect();
208
- const classes = el.className?.toString?.().toLowerCase() || '';
209
- const id = el.id?.toLowerCase() || '';
210
- const tagName = el.tagName.toLowerCase();
211
- // Semantic detection first
212
- if (el.closest('nav') || classes.includes('nav') || id.includes('nav') || el.closest('[role="navigation"]')) {
213
- return 'navigation';
214
- }
215
- if (el.closest('form') || classes.includes('form')) {
216
- return 'forms';
217
- }
218
- if (el.closest('footer') || classes.includes('footer') || id.includes('footer')) {
219
- return 'footer';
220
- }
221
- if (el.closest('aside') || classes.includes('sidebar') || classes.includes('aside')) {
222
- return 'sidebar';
223
- }
224
- if (tagName.match(/^h[1-6]$/) || el.closest('h1, h2, h3, h4, h5, h6')) {
225
- return 'headings';
226
- }
227
- if (classes.includes('hero') || classes.includes('banner') || classes.includes('jumbotron')) {
228
- return 'hero';
229
- }
230
- if (classes.includes('cta') || classes.includes('call-to-action') ||
231
- (tagName === 'button' && rect.width > 100) ||
232
- el.getAttribute?.('role') === 'button') {
233
- return 'cta';
234
- }
235
- if (tagName === 'img' || el.closest('figure, picture')) {
236
- return 'images';
237
- }
238
- if (classes.includes('search') || id.includes('search') || el.type === 'search') {
239
- return 'search';
240
- }
241
- // Position-based detection
242
- if (rect.top < 100) {
243
- // Top of page - likely navigation or hero
244
- return rect.height > 200 ? 'hero' : 'navigation';
245
- }
246
- if (rect.top > viewportHeight * 0.8) {
247
- return 'footer';
248
- }
249
- if (rect.left < viewportWidth * 0.2 || rect.right > viewportWidth * 0.8) {
250
- return 'sidebar';
251
- }
252
- // Default to content
253
- return 'content';
254
- }
255
- const data = {
256
- title: document.title,
257
- url: window.location.href,
258
- text: document.body.innerText.slice(0, 5000),
259
- buttons: Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).map(el => ({
260
- text: el.textContent?.trim() || el.value || '',
261
- selector: el.id ? `#${el.id}` : el.className ? `.${el.className.toString().split(' ')[0]}` : el.tagName.toLowerCase(),
262
- visible: el.getBoundingClientRect().width > 0,
263
- area: detectArea(el),
264
- })),
265
- links: Array.from(document.querySelectorAll('a[href]')).map(el => ({
266
- text: el.textContent?.trim() || '',
267
- href: el.href,
268
- selector: el.id ? `#${el.id}` : `a[href="${el.getAttribute('href')}"]`,
269
- visible: el.getBoundingClientRect().width > 0,
270
- area: detectArea(el),
271
- })).slice(0, 50),
272
- inputs: Array.from(document.querySelectorAll('input:not([type="hidden"]), textarea, select')).map(el => {
273
- const input = el;
274
- return {
275
- type: input.type || 'text',
276
- name: input.name,
277
- placeholder: input.placeholder,
278
- label: document.querySelector(`label[for="${input.id}"]`)?.textContent?.trim() || '',
279
- selector: input.id ? `#${input.id}` : input.name ? `[name="${input.name}"]` : input.placeholder ? `[placeholder="${input.placeholder}"]` : 'input',
280
- visible: el.getBoundingClientRect().width > 0,
281
- area: detectArea(el),
282
- };
283
- }),
284
- // Check for common friction indicators
285
- hasPopup: !!document.querySelector('[class*="modal"], [class*="popup"], [role="dialog"]'),
286
- hasError: !!document.querySelector('[class*="error"], .alert-danger, [role="alert"]'),
287
- hasCaptcha: !!document.querySelector('[class*="captcha"], [class*="recaptcha"], iframe[src*="captcha"]'),
288
- formCount: document.querySelectorAll('form').length,
289
- };
290
- return data;
291
- });
292
- // Detect friction points
293
- if (pageData.hasPopup) {
294
- frictionPoints.push("Popup/modal blocking content");
295
- }
296
- if (pageData.hasError) {
297
- frictionPoints.push("Error message displayed");
298
- }
299
- if (pageData.hasCaptcha) {
300
- frictionPoints.push("CAPTCHA blocking progress");
301
- }
302
- // Check if goal is reached based on semantic analysis
303
- const goalKeywords = extractGoalKeywords(goal);
304
- const pageText = pageData.text.toLowerCase();
305
- const pageUrl = pageData.url.toLowerCase();
306
- let goalReached = false;
307
- // Action-oriented goal patterns (require completion confirmation)
308
- if (goalLower.includes("sign up") || goalLower.includes("register")) {
309
- goalReached = pageText.includes("welcome") ||
310
- pageText.includes("account created") ||
311
- pageText.includes("verify your email") ||
312
- pageUrl.includes("dashboard") ||
313
- pageUrl.includes("welcome");
314
- }
315
- else if (goalLower.includes("login") || goalLower.includes("sign in")) {
316
- goalReached = pageUrl.includes("dashboard") ||
317
- pageUrl.includes("account") ||
318
- pageText.includes("welcome back");
319
- }
320
- else if (goalLower.includes("checkout") || goalLower.includes("purchase")) {
321
- goalReached = pageText.includes("order confirmed") ||
322
- pageText.includes("thank you for your") ||
323
- pageUrl.includes("confirmation");
324
- }
325
- else {
326
- // Information-seeking goals: "find X", "search for X", "locate X", or generic
327
- // Extract the subject being sought (remove action verbs)
328
- const actionVerbs = ["find", "search", "locate", "look", "get", "see", "view", "check", "learn", "discover"];
329
- const subjectKeywords = goalKeywords.filter(kw => !actionVerbs.includes(kw));
330
- // Expand subject keywords with synonyms (e.g., "requirements" -> also check "eligibility", "criteria")
331
- const expandedKeywords = expandKeywordsWithSynonyms(subjectKeywords);
332
- // If we have subject keywords, check for their presence and density
333
- if (subjectKeywords.length > 0) {
334
- // Count how many expanded keywords (including synonyms) appear in the page
335
- const _keywordsFound = expandedKeywords.filter(kw => pageText.includes(kw));
336
- // For ratio, compare against original subject count (finding ANY synonym counts)
337
- // If we find synonyms for a concept, that concept is "found"
338
- const conceptsFound = new Set();
339
- for (const kw of subjectKeywords) {
340
- // Check if original keyword or any of its synonyms appear
341
- const synonyms = expandKeywordsWithSynonyms([kw]);
342
- if (synonyms.some(syn => pageText.includes(syn))) {
343
- conceptsFound.add(kw);
344
- }
345
- }
346
- const keywordRatio = conceptsFound.size / subjectKeywords.length;
347
- // Also check keyword density (multiple mentions = more relevant)
348
- let totalMentions = 0;
349
- for (const kw of expandedKeywords) {
350
- const regex = new RegExp(kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
351
- const matches = pageText.match(regex);
352
- totalMentions += matches ? matches.length : 0;
353
- }
354
- // Goal reached if:
355
- // - At least 50% of subject CONCEPTS are present (via original or synonym), AND
356
- // - Keywords/synonyms are mentioned at least 3x total (indicates actual content)
357
- goalReached = keywordRatio >= 0.5 && totalMentions >= 3;
358
- // Also check if URL contains subject keywords (strong signal)
359
- const urlHasSubject = expandedKeywords.some(kw => pageUrl.includes(kw));
360
- if (urlHasSubject && keywordRatio >= 0.4) {
361
- goalReached = true;
362
- }
363
- }
364
- else {
365
- // Fallback: all keywords are action verbs, just check if we're on a relevant page
366
- goalReached = goalKeywords.some(kw => pageUrl.includes(kw));
367
- }
368
- }
369
- // Build action candidates
370
- const availableActions = [];
371
- // Score buttons by relevance to goal (with area detection)
372
- for (const btn of pageData.buttons.filter(b => b.visible)) {
373
- const textLower = btn.text.toLowerCase();
374
- let relevance = 0;
375
- for (const keyword of goalKeywords) {
376
- if (textLower.includes(keyword))
377
- relevance += 0.3;
378
- }
379
- // Common action words
380
- if (textLower.match(/submit|continue|next|proceed|sign|login|create|register|add|buy|checkout/)) {
381
- relevance += 0.2;
382
- }
383
- if (relevance > 0 || btn.text.length > 0) {
384
- availableActions.push({
385
- type: "click",
386
- target: btn.text || "button",
387
- selector: btn.selector,
388
- relevance: Math.min(1, relevance),
389
- area: btn.area,
390
- text: btn.text,
391
- });
392
- }
393
- }
394
- // Score links by relevance (with area detection)
395
- for (const link of pageData.links.filter(l => l.visible)) {
396
- const textLower = link.text.toLowerCase();
397
- let relevance = 0;
398
- for (const keyword of goalKeywords) {
399
- if (textLower.includes(keyword) || link.href.toLowerCase().includes(keyword)) {
400
- relevance += 0.3;
401
- }
402
- }
403
- if (relevance > 0) {
404
- availableActions.push({
405
- type: "click",
406
- target: link.text || link.href,
407
- selector: link.selector,
408
- relevance: Math.min(1, relevance),
409
- area: link.area,
410
- text: link.text,
411
- });
412
- }
413
- }
414
- // Score inputs (with area detection)
415
- for (const input of pageData.inputs.filter(i => i.visible)) {
416
- const labelLower = (input.label + input.placeholder + input.name).toLowerCase();
417
- let relevance = 0;
418
- for (const keyword of goalKeywords) {
419
- if (labelLower.includes(keyword))
420
- relevance += 0.2;
421
- }
422
- // Common form fields
423
- if (labelLower.match(/email|password|name|username|search/)) {
424
- relevance += 0.3;
425
- }
426
- if (relevance > 0) {
427
- availableActions.push({
428
- type: "fill",
429
- target: input.label || input.placeholder || input.name || input.type,
430
- selector: input.selector,
431
- relevance: Math.min(1, relevance),
432
- area: input.area,
433
- text: input.label || input.placeholder,
434
- });
435
- }
436
- }
437
- // Sort by relevance (focus hierarchy applied later in findBestAction)
438
- availableActions.sort((a, b) => b.relevance - a.relevance);
439
- return {
440
- goalReached,
441
- frictionPoints,
442
- availableActions,
443
- pageContent: pageData.text.slice(0, 1000),
444
- };
445
- }
446
- function extractGoalKeywords(goal) {
447
- const stopWords = ['a', 'an', 'the', 'to', 'for', 'on', 'in', 'at', 'and', 'or', 'of', 'with'];
448
- return goal.toLowerCase()
449
- .split(/\s+/)
450
- .filter(word => word.length > 2 && !stopWords.includes(word));
451
- }
452
- // Synonym map for common goal terms - expands keywords to include related terms
453
- const KEYWORD_SYNONYMS = {
454
- requirements: ['requirements', 'eligibility', 'criteria', 'prerequisites', 'qualifications', 'what you need', 'how to apply', 'checklist'],
455
- application: ['application', 'apply', 'applying', 'submit', 'enrollment', 'enroll'],
456
- admission: ['admission', 'admissions', 'accepted', 'acceptance', 'enrolled'],
457
- cost: ['cost', 'tuition', 'fees', 'price', 'pricing', 'financial', 'payment'],
458
- deadline: ['deadline', 'due date', 'dates', 'when to apply', 'timeline'],
459
- contact: ['contact', 'email', 'phone', 'reach', 'support', 'help'],
460
- international: ['international', 'global', 'foreign', 'overseas', 'visa', 'i-20'],
461
- };
462
- function expandKeywordsWithSynonyms(keywords) {
463
- const expanded = new Set();
464
- for (const kw of keywords) {
465
- expanded.add(kw);
466
- // Check if this keyword has synonyms
467
- if (KEYWORD_SYNONYMS[kw]) {
468
- for (const syn of KEYWORD_SYNONYMS[kw]) {
469
- expanded.add(syn);
470
- }
471
- }
472
- // Also check if keyword is IN a synonym list (reverse lookup)
473
- for (const [key, syns] of Object.entries(KEYWORD_SYNONYMS)) {
474
- if (syns.includes(kw)) {
475
- expanded.add(key);
476
- for (const syn of syns) {
477
- expanded.add(syn);
478
- }
479
- }
480
- }
481
- }
482
- return Array.from(expanded);
483
- }
484
- /**
485
- * Find the best action using focus hierarchy weighting.
486
- *
487
- * This applies probabilistic focus patterns to simulate how real users
488
- * prioritize different page areas based on their task type.
489
- *
490
- * Key behaviors:
491
- * - Information-seeking tasks prioritize headings/navigation
492
- * - Action-completion tasks prioritize forms/CTAs
493
- * - Common distractions (newsletter popups, etc.) are filtered
494
- */
495
- async function findBestAction(page, goal, analysis) {
496
- if (analysis.availableActions.length === 0)
497
- return null;
498
- // Infer task type from goal and get appropriate focus hierarchy
499
- const taskType = inferTaskTypeFromGoal(goal);
500
- const focusHierarchy = getFocusHierarchy(taskType);
501
- // Apply focus hierarchy weighting to each action
502
- const weightedActions = analysis.availableActions.map(action => {
503
- // Calculate focus priority based on area and distraction filtering
504
- const focusPriority = calculateFocusPriority({
505
- area: action.area,
506
- text: action.text,
507
- selector: action.selector,
508
- isRelevantToGoal: action.relevance > 0.3,
509
- }, focusHierarchy);
510
- // Check if this action should be filtered as a distraction
511
- const distractionRate = getDistractionIgnoreRate({ text: action.text, selector: action.selector }, focusHierarchy.distractionFilters);
512
- // Deterministic filtering: skip high-probability distractions
513
- // (Previously random, now uses threshold for consistent behavior)
514
- const skipAsDistraction = distractionRate >= 0.7;
515
- // Combined score: relevance * focus priority, with distraction penalty
516
- const combinedScore = skipAsDistraction
517
- ? 0 // Filtered out as distraction
518
- : action.relevance * focusPriority;
519
- return {
520
- action,
521
- focusPriority,
522
- distractionRate,
523
- combinedScore,
524
- skipped: skipAsDistraction,
525
- };
526
- });
527
- // Filter out skipped distractions
528
- const viableActions = weightedActions.filter(w => !w.skipped);
529
- if (viableActions.length === 0) {
530
- // All actions were filtered as distractions - fall back to nav-first
531
- // This happens when page is full of popups/banners but has some nav
532
- const navActions = weightedActions.filter(w => w.action.area === 'navigation' || w.action.area === 'headings');
533
- if (navActions.length > 0) {
534
- // Force through navigation even if it was a distraction
535
- navActions.sort((a, b) => b.action.relevance - a.action.relevance);
536
- return navActions[0].action;
537
- }
538
- // Absolute fallback: return highest relevance regardless of focus
539
- return analysis.availableActions[0];
540
- }
541
- // Sort by combined score (focus-weighted relevance)
542
- viableActions.sort((a, b) => b.combinedScore - a.combinedScore);
543
- // Apply attention capacity limit (humans don't consider all options)
544
- const consideredActions = viableActions.slice(0, focusHierarchy.attentionCapacity);
545
- // Select best action deterministically (no random selection)
546
- // When using cognitive journeys, Claude provides intelligent decision making
547
- // For heuristic mode, we use the highest-scored action consistently
548
- return consideredActions[0]?.action ?? null;
549
- }
550
- async function executeAction(page, action) {
551
- switch (action.type) {
552
- case "click":
553
- await page.click(action.selector, { timeout: 5000 });
554
- break;
555
- case "fill":
556
- // Generate test data based on field type
557
- const testValue = generateTestValue(action.target);
558
- await page.fill(action.selector, testValue, { timeout: 5000 });
559
- break;
560
- case "scroll":
561
- await page.evaluate(() => window.scrollBy(0, 500));
562
- break;
563
- }
564
- }
565
- function generateTestValue(fieldHint) {
566
- const hint = fieldHint.toLowerCase();
567
- if (hint.includes("email"))
568
- return "test@example.com";
569
- if (hint.includes("password"))
570
- return "TestPassword123!";
571
- if (hint.includes("name"))
572
- return "Test User";
573
- if (hint.includes("phone"))
574
- return "555-123-4567";
575
- if (hint.includes("search"))
576
- return "test search";
577
- return "test value";
578
- }
579
87
  // ============================================================================
580
88
  // Comparison and Ranking
581
89
  // ============================================================================
@@ -1084,74 +592,77 @@ export function generateCompetitiveBenchmarkHtmlReport(result) {
1084
592
  // Main Benchmark Function
1085
593
  // ============================================================================
1086
594
  export async function runCompetitiveBenchmark(options) {
1087
- const { sites, goal, persona = "first-timer", maxSteps = 30, maxTime = 180, headless = true, maxConcurrency = 3, } = options;
595
+ const { sites, goal, persona = "first-timer", maxSteps = 30, maxTime = 180, headless = true, maxConcurrency = 3, device, viewport, } = options;
1088
596
  const startTime = Date.now();
1089
- const personaConfig = getPersonaOrRefuse(persona);
1090
- const useCognitiveJourneys = isApiKeyConfigured();
597
+ getPersonaOrRefuse(persona);
598
+ // 20.0: no keyless mode. The heuristic simulation called a sign-up goal
599
+ // achieved in 0 steps on a page with no sign-up, because "welcome" was on it
600
+ // (measured 2026-10-21), and the rankings were built on that verdict.
601
+ if (!isApiKeyConfigured()) {
602
+ throw new Error("competitive-benchmark needs an Anthropic API key (cbrowser config set-api-key <key>). The keyless heuristic mode was removed in 20.0.0 because its goal verdicts were not accurate.");
603
+ }
1091
604
  // Run journeys in parallel (limited concurrency)
1092
605
  const results = [];
1093
606
  // Process sites in batches
1094
607
  for (let i = 0; i < sites.length; i += maxConcurrency) {
1095
608
  const batch = sites.slice(i, i + maxConcurrency);
1096
609
  const batchResults = await Promise.all(batch.map(async (site) => {
1097
- // Use cognitive journeys when API key is available (realistic simulation)
1098
- if (useCognitiveJourneys) {
1099
- try {
1100
- const journey = await runCognitiveJourney({
1101
- persona,
1102
- startUrl: site.url,
1103
- goal,
1104
- maxSteps,
1105
- maxTime,
1106
- headless,
1107
- vision: false,
1108
- verbose: false,
1109
- });
1110
- const _u = new URL(site.url);
1111
- const siteName = site.name || _u.hostname.replace(/^www\d*\./, '') + (_u.pathname !== '/' ? _u.pathname.replace(/\/$/, '') : '');
1112
- return {
1113
- url: site.url,
1114
- siteName,
1115
- goalAchieved: journey.goalAchieved,
1116
- abandonmentReason: journey.abandonmentReason,
1117
- totalTime: journey.totalTime * 1000, // Convert to ms
1118
- stepCount: journey.stepCount,
1119
- frictionPoints: journey.frictionPoints.map(fp => `${fp.type}: ${fp.monologue.substring(0, 80)}`),
1120
- confusionLevel: Math.round(journey.finalState.confusionLevel * 100),
1121
- frustrationLevel: Math.round(journey.finalState.frustrationLevel * 100),
1122
- abandonmentRisk: calculateAbandonmentRisk({
1123
- patience: journey.finalState.patienceRemaining * 100,
1124
- frustration: journey.finalState.frustrationLevel * 100,
1125
- confusion: journey.finalState.confusionLevel * 100,
1126
- steps: journey.stepCount,
1127
- frictionPoints: [],
1128
- startTime: Date.now(),
1129
- }, journey.goalAchieved),
1130
- screenshots: { start: '', end: '' },
1131
- };
1132
- }
1133
- catch (e) {
1134
- // Fall back to heuristic mode on error
1135
- console.warn(`Cognitive journey failed for ${site.url}, falling back to heuristic: ${e.message}`);
1136
- }
1137
- }
1138
- // Fallback: Heuristic simulation (when no API key or cognitive journey failed)
1139
- let browser = null;
1140
610
  try {
1141
- browser = new CBrowser({ headless, persistent: false });
1142
- await browser.launch();
1143
- const page = await browser.getPage();
1144
- const result = await simulateJourney(page, site.url, goal, personaConfig, maxSteps, maxTime);
1145
- // Override site name if provided
1146
- if (site.name) {
1147
- result.siteName = site.name;
1148
- }
1149
- return result;
611
+ const journey = await runCognitiveJourney({
612
+ persona,
613
+ startUrl: site.url,
614
+ goal,
615
+ maxSteps,
616
+ maxTime,
617
+ headless,
618
+ // Concurrent journeys need their own profiles (cognitive/index.ts).
619
+ persistent: false,
620
+ vision: false,
621
+ verbose: false,
622
+ ...(device ? { device } : {}),
623
+ ...(viewport ? { viewport } : {}),
624
+ });
625
+ const _u = new URL(site.url);
626
+ const siteName = site.name || _u.hostname.replace(/^www\d*\./, '') + (_u.pathname !== '/' ? _u.pathname.replace(/\/$/, '') : '');
627
+ return {
628
+ url: site.url,
629
+ siteName,
630
+ goalAchieved: journey.goalAchieved,
631
+ abandonmentReason: journey.abandonmentReason,
632
+ totalTime: journey.totalTime * 1000, // Convert to ms
633
+ stepCount: journey.stepCount,
634
+ frictionPoints: journey.frictionPoints.map(fp => `${fp.type}: ${fp.monologue.substring(0, 80)}`),
635
+ confusionLevel: Math.round(journey.finalState.confusionLevel * 100),
636
+ frustrationLevel: Math.round(journey.finalState.frustrationLevel * 100),
637
+ abandonmentRisk: calculateAbandonmentRisk({
638
+ patience: journey.finalState.patienceRemaining * 100,
639
+ frustration: journey.finalState.frustrationLevel * 100,
640
+ confusion: journey.finalState.confusionLevel * 100,
641
+ steps: journey.stepCount,
642
+ frictionPoints: [],
643
+ startTime: Date.now(),
644
+ }, journey.goalAchieved),
645
+ screenshots: { start: '', end: '' },
646
+ };
1150
647
  }
1151
- finally {
1152
- if (browser) {
1153
- await browser.close();
1154
- }
648
+ catch (e) {
649
+ // One site's failure is that site's result, not a fake success and
650
+ // not the end of the whole benchmark.
651
+ const message = e.message;
652
+ console.warn(`Cognitive journey failed for ${site.url}: ${message}`);
653
+ return {
654
+ url: site.url,
655
+ siteName: site.name || new URL(site.url).hostname,
656
+ goalAchieved: false,
657
+ abandonmentReason: `Journey failed: ${message}`,
658
+ totalTime: 0,
659
+ stepCount: 0,
660
+ frictionPoints: [],
661
+ confusionLevel: 0,
662
+ frustrationLevel: 0,
663
+ abandonmentRisk: 100,
664
+ screenshots: { start: '', end: '' },
665
+ };
1155
666
  }
1156
667
  }));
1157
668
  results.push(...batchResults);
@@ -1279,7 +790,7 @@ async function withRetry(fn, options = {}) {
1279
790
  * @since 17.0.0
1280
791
  */
1281
792
  export async function runAIReadinessBenchmark(options) {
1282
- const { urls, headless = true, maxConcurrency = 3, } = options;
793
+ const { urls, headless = true, maxConcurrency = 3, device, } = options;
1283
794
  const startTime = Date.now();
1284
795
  const results = [];
1285
796
  // Process URLs in batches
@@ -1293,6 +804,7 @@ export async function runAIReadinessBenchmark(options) {
1293
804
  // v18.22.0: Enable SPA mode and user agent rotation for bot detection hardening
1294
805
  const retryResult = await withRetry(() => runAgentReadyAudit(url, {
1295
806
  headless,
807
+ ...(device ? { device } : {}),
1296
808
  spaMode: true,
1297
809
  useRandomUserAgent: true,
1298
810
  }), { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 10000 });