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.
- package/README.md +292 -232
- package/THIRD_PARTY_LICENSES.md +416 -7
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +2 -0
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +70 -558
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/analysis/natural-language.d.ts.map +1 -1
- package/dist/analysis/natural-language.js +43 -0
- package/dist/analysis/natural-language.js.map +1 -1
- package/dist/analysis/persona-comparison.d.ts +7 -0
- package/dist/analysis/persona-comparison.d.ts.map +1 -1
- package/dist/analysis/persona-comparison.js +8 -1
- package/dist/analysis/persona-comparison.js.map +1 -1
- package/dist/browser-options.d.ts +23 -0
- package/dist/browser-options.d.ts.map +1 -0
- package/dist/browser-options.js +24 -0
- package/dist/browser-options.js.map +1 -0
- package/dist/browser.d.ts +36 -14
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +139 -196
- package/dist/browser.js.map +1 -1
- package/dist/cli-args.d.ts +9 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +35 -7
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-browser-flags.d.ts +33 -0
- package/dist/cli-browser-flags.d.ts.map +1 -0
- package/dist/cli-browser-flags.js +89 -0
- package/dist/cli-browser-flags.js.map +1 -0
- package/dist/cli.js +979 -401
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts +0 -13
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +39 -15
- package/dist/cognitive/index.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -6
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +5 -4
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +87 -29
- package/dist/daemon.js.map +1 -1
- package/dist/devices.d.ts +20 -0
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.js +18 -0
- package/dist/devices.js.map +1 -1
- package/dist/help-client.d.ts +48 -0
- package/dist/help-client.d.ts.map +1 -0
- package/dist/help-client.js +96 -0
- package/dist/help-client.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +165 -2281
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools/base/advanced-interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/advanced-interaction-tools.js +21 -21
- package/dist/mcp-tools/base/advanced-interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.js +6 -3
- package/dist/mcp-tools/base/assertion-tools.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +15 -4
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.js +33 -27
- package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.js +9 -2
- package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/capture-tools.d.ts +60 -4
- package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/capture-tools.js +118 -14
- package/dist/mcp-tools/base/capture-tools.js.map +1 -1
- package/dist/mcp-tools/base/index.d.ts +6 -5
- package/dist/mcp-tools/base/index.d.ts.map +1 -1
- package/dist/mcp-tools/base/index.js +7 -6
- package/dist/mcp-tools/base/index.js.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.js +20 -6
- package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.js +83 -3
- package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.js +11 -23
- package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +13 -3
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/enterprise-stubs.d.ts +2 -2
- package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -1
- package/dist/mcp-tools/enterprise-stubs.js +29 -16
- package/dist/mcp-tools/enterprise-stubs.js.map +1 -1
- package/dist/mcp-tools/index.d.ts +19 -14
- package/dist/mcp-tools/index.d.ts.map +1 -1
- package/dist/mcp-tools/index.js +2 -2
- package/dist/mcp-tools/index.js.map +1 -1
- package/dist/mcp-tools/red-zone-refusal.d.ts +33 -0
- package/dist/mcp-tools/red-zone-refusal.d.ts.map +1 -0
- package/dist/mcp-tools/red-zone-refusal.js +43 -0
- package/dist/mcp-tools/red-zone-refusal.js.map +1 -0
- package/dist/mcp-tools/tier-gate.js +1 -1
- package/dist/mcp-tools/tier-gate.js.map +1 -1
- package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
- package/dist/mcp-tools/tool-categories.js +2 -0
- package/dist/mcp-tools/tool-categories.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +18 -2
- package/dist/output.js.map +1 -1
- package/dist/performance/metrics.d.ts +8 -3
- package/dist/performance/metrics.d.ts.map +1 -1
- package/dist/performance/metrics.js +16 -5
- package/dist/performance/metrics.js.map +1 -1
- package/dist/recording/contact-sheet.js.map +1 -1
- package/dist/security/tool-permissions.d.ts.map +1 -1
- package/dist/security/tool-permissions.js +1 -0
- package/dist/security/tool-permissions.js.map +1 -1
- package/dist/testing/coverage.d.ts +2 -2
- package/dist/testing/coverage.d.ts.map +1 -1
- package/dist/testing/coverage.js +6 -2
- package/dist/testing/coverage.js.map +1 -1
- package/dist/testing/flaky-detection.d.ts +2 -2
- package/dist/testing/flaky-detection.d.ts.map +1 -1
- package/dist/testing/flaky-detection.js +11 -3
- package/dist/testing/flaky-detection.js.map +1 -1
- package/dist/testing/nl-test-suite.d.ts +13 -0
- package/dist/testing/nl-test-suite.d.ts.map +1 -1
- package/dist/testing/nl-test-suite.js +5 -1
- package/dist/testing/nl-test-suite.js.map +1 -1
- package/dist/testing/suite-results.d.ts +27 -0
- package/dist/testing/suite-results.d.ts.map +1 -0
- package/dist/testing/suite-results.js +54 -0
- package/dist/testing/suite-results.js.map +1 -0
- package/dist/testing/test-repair.d.ts +2 -2
- package/dist/testing/test-repair.d.ts.map +1 -1
- package/dist/testing/test-repair.js +6 -2
- package/dist/testing/test-repair.js.map +1 -1
- package/dist/types.d.ts +61 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -4
- package/dist/types.js.map +1 -1
- package/dist/visual/ab-comparison.d.ts.map +1 -1
- package/dist/visual/ab-comparison.js +7 -2
- package/dist/visual/ab-comparison.js.map +1 -1
- package/dist/visual/cross-browser.d.ts +7 -2
- package/dist/visual/cross-browser.d.ts.map +1 -1
- package/dist/visual/cross-browser.js +33 -5
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/distance-metrics.d.ts +7 -0
- package/dist/visual/distance-metrics.d.ts.map +1 -1
- package/dist/visual/distance-metrics.js.map +1 -1
- package/dist/visual/heatmap-overlay.js.map +1 -1
- package/dist/visual/llm-relevance.d.ts.map +1 -1
- package/dist/visual/llm-relevance.js.map +1 -1
- package/dist/visual/regression.d.ts +17 -7
- package/dist/visual/regression.d.ts.map +1 -1
- package/dist/visual/regression.js +77 -38
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.d.ts +22 -0
- package/dist/visual/responsive.d.ts.map +1 -1
- package/dist/visual/responsive.js +44 -7
- package/dist/visual/responsive.js.map +1 -1
- package/dist/visual/visual-overlays.js.map +1 -1
- package/dist/webhooks.d.ts +67 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +128 -0
- package/dist/webhooks.js.map +1 -0
- 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
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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 });
|