cbrowser 8.9.1 → 9.1.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 (35) hide show
  1. package/README.md +121 -0
  2. package/dist/analysis/accessibility-empathy.d.ts +32 -0
  3. package/dist/analysis/accessibility-empathy.d.ts.map +1 -0
  4. package/dist/analysis/accessibility-empathy.js +1005 -0
  5. package/dist/analysis/accessibility-empathy.js.map +1 -0
  6. package/dist/analysis/agent-ready-audit.d.ts +11 -0
  7. package/dist/analysis/agent-ready-audit.d.ts.map +1 -0
  8. package/dist/analysis/agent-ready-audit.js +900 -0
  9. package/dist/analysis/agent-ready-audit.js.map +1 -0
  10. package/dist/analysis/competitive-benchmark.d.ts +11 -0
  11. package/dist/analysis/competitive-benchmark.d.ts.map +1 -0
  12. package/dist/analysis/competitive-benchmark.js +1122 -0
  13. package/dist/analysis/competitive-benchmark.js.map +1 -0
  14. package/dist/analysis/index.d.ts +5 -1
  15. package/dist/analysis/index.d.ts.map +1 -1
  16. package/dist/analysis/index.js +5 -1
  17. package/dist/analysis/index.js.map +1 -1
  18. package/dist/cli.js +191 -2
  19. package/dist/cli.js.map +1 -1
  20. package/dist/cognitive/focus-hierarchies.d.ts +113 -0
  21. package/dist/cognitive/focus-hierarchies.d.ts.map +1 -0
  22. package/dist/cognitive/focus-hierarchies.js +500 -0
  23. package/dist/cognitive/focus-hierarchies.js.map +1 -0
  24. package/dist/mcp-server.d.ts.map +1 -1
  25. package/dist/mcp-server.js +100 -1
  26. package/dist/mcp-server.js.map +1 -1
  27. package/dist/personas.d.ts +14 -0
  28. package/dist/personas.d.ts.map +1 -1
  29. package/dist/personas.js +393 -0
  30. package/dist/personas.js.map +1 -1
  31. package/dist/types.d.ts +429 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/docs/METHODOLOGY.md +245 -0
  35. package/package.json +1 -1
@@ -0,0 +1,900 @@
1
+ /**
2
+ * Agent-Ready Audit Module
3
+ *
4
+ * Analyzes a site and outputs specific recommendations to make it AI-agent-friendly.
5
+ * Flips CBrowser's "workaround" detections into "fix this" recommendations.
6
+ */
7
+ import { chromium } from "playwright";
8
+ // ============================================================================
9
+ // Scoring Methodology & Disclaimers
10
+ // ============================================================================
11
+ /**
12
+ * METHODOLOGY DISCLAIMER:
13
+ *
14
+ * Agent-Ready scores are HEURISTIC estimates based on pattern detection,
15
+ * not precise measurements of AI agent compatibility.
16
+ *
17
+ * Scoring approach:
18
+ * - Each detected issue deducts points based on severity
19
+ * - Severity levels align with Nielsen's usability severity scale (0-4)
20
+ * - Category weights reflect typical agent interaction patterns
21
+ *
22
+ * Research basis:
23
+ * - WCAG 2.1 compliance thresholds (94.8% of sites have failures - WebAIM)
24
+ * - Touch target minimums: 44x44px (WCAG 2.5.5/2.5.8)
25
+ * - Screen reader success rates: ~55% task completion (WebAIM survey)
26
+ *
27
+ * Interpretation:
28
+ * - A/B grades: Site works well with AI agents
29
+ * - C grade: Some issues, agents may need workarounds
30
+ * - D/F grades: Significant barriers to agent automation
31
+ *
32
+ * Use letter grades (not percentage scores) for clearer communication.
33
+ */
34
+ // ============================================================================
35
+ // Scoring Algorithm
36
+ // ============================================================================
37
+ /**
38
+ * Severity penalties calibrated to Nielsen's usability severity scale:
39
+ * - Critical (4): Prevents task completion entirely
40
+ * - High (3): Major problem, difficult workaround
41
+ * - Medium (2): Minor problem, easy workaround
42
+ * - Low (1): Cosmetic issue, minimal impact
43
+ */
44
+ const SEVERITY_PENALTY = {
45
+ critical: 25, // ~Nielsen Level 4 - complete blocker
46
+ high: 15, // ~Nielsen Level 3 - major obstacle
47
+ medium: 8, // ~Nielsen Level 2 - minor problem
48
+ low: 3, // ~Nielsen Level 1 - cosmetic
49
+ };
50
+ /**
51
+ * Category weights based on typical AI agent interaction priorities:
52
+ * - Findability (35%): Can the agent locate elements? Most critical for automation
53
+ * - Stability (30%): Will selectors remain stable across page loads?
54
+ * - Accessibility (20%): ARIA labels provide semantic meaning for agents
55
+ * - Semantics (15%): Proper HTML structure aids understanding
56
+ */
57
+ const CATEGORY_WEIGHTS = {
58
+ findability: 0.35,
59
+ stability: 0.30,
60
+ accessibility: 0.20,
61
+ semantics: 0.15,
62
+ };
63
+ function calculateAgentReadyScore(issues) {
64
+ // Start with perfect scores
65
+ const scores = {
66
+ findability: 100,
67
+ stability: 100,
68
+ accessibility: 100,
69
+ semantics: 100,
70
+ };
71
+ // Deduct based on issues
72
+ for (const issue of issues) {
73
+ const penalty = SEVERITY_PENALTY[issue.severity];
74
+ scores[issue.category] = Math.max(0, scores[issue.category] - penalty);
75
+ }
76
+ // Calculate overall weighted score
77
+ const overall = Math.round(scores.findability * CATEGORY_WEIGHTS.findability +
78
+ scores.stability * CATEGORY_WEIGHTS.stability +
79
+ scores.accessibility * CATEGORY_WEIGHTS.accessibility +
80
+ scores.semantics * CATEGORY_WEIGHTS.semantics);
81
+ return {
82
+ overall,
83
+ findability: Math.round(scores.findability),
84
+ stability: Math.round(scores.stability),
85
+ accessibility: Math.round(scores.accessibility),
86
+ semantics: Math.round(scores.semantics),
87
+ };
88
+ }
89
+ function calculateGrade(score) {
90
+ if (score >= 90)
91
+ return "A";
92
+ if (score >= 80)
93
+ return "B";
94
+ if (score >= 70)
95
+ return "C";
96
+ if (score >= 60)
97
+ return "D";
98
+ return "F";
99
+ }
100
+ /**
101
+ * Detect elements without proper labels (findability)
102
+ */
103
+ async function detectUnlabeledElements(ctx) {
104
+ const { page, issues, summary } = ctx;
105
+ // Find buttons without accessible names
106
+ const unlabeledButtons = await page.$$eval('button:not([aria-label]):not([aria-labelledby]), [role="button"]:not([aria-label]):not([aria-labelledby])', (elements) => elements.map(el => ({
107
+ selector: el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '') + (el.className ? `.${el.className.split(' ')[0]}` : ''),
108
+ text: el.textContent?.trim() || '',
109
+ html: el.outerHTML.slice(0, 200),
110
+ })).filter(el => !el.text) // Only those without visible text
111
+ );
112
+ for (const btn of unlabeledButtons) {
113
+ issues.push({
114
+ category: "findability",
115
+ severity: "high",
116
+ element: btn.selector,
117
+ description: `Button without accessible text or aria-label`,
118
+ detectionMethod: "button-label-check",
119
+ recommendation: "Add aria-label or visible text to the button",
120
+ codeExample: `<button aria-label="Describe action here">...</button>`,
121
+ });
122
+ summary.elementsWithoutText++;
123
+ }
124
+ // Find inputs without labels
125
+ const unlabeledInputs = await page.$$eval('input:not([aria-label]):not([aria-labelledby]):not([type="hidden"]):not([type="submit"]):not([type="button"])', (elements) => elements.map(el => {
126
+ const input = el;
127
+ const id = input.id;
128
+ // Check if there's an associated label
129
+ const hasLabel = id && document.querySelector(`label[for="${id}"]`);
130
+ return {
131
+ selector: `input${id ? `#${id}` : ''}[type="${input.type || 'text'}"]`,
132
+ type: input.type || 'text',
133
+ name: input.name,
134
+ placeholder: input.placeholder,
135
+ hasLabel: !!hasLabel,
136
+ };
137
+ }).filter(el => !el.hasLabel && !el.placeholder));
138
+ for (const input of unlabeledInputs) {
139
+ issues.push({
140
+ category: "accessibility",
141
+ severity: "medium",
142
+ element: input.selector,
143
+ description: `Input field without label or aria-label`,
144
+ detectionMethod: "input-label-check",
145
+ recommendation: "Add <label for=\"id\"> or aria-label attribute",
146
+ codeExample: `<label for="${input.name || 'field'}">Label text</label>\n<input id="${input.name || 'field'}" ... />`,
147
+ });
148
+ summary.missingAriaLabels++;
149
+ }
150
+ }
151
+ /**
152
+ * Detect hidden inputs with custom UI (stability)
153
+ */
154
+ async function detectHiddenInputs(ctx) {
155
+ const { page, issues, summary } = ctx;
156
+ // Find visually hidden selects (custom dropdowns)
157
+ const hiddenSelects = await page.$$eval('select', (elements) => elements.map(el => {
158
+ const rect = el.getBoundingClientRect();
159
+ const styles = window.getComputedStyle(el);
160
+ const isHidden = rect.width === 0 ||
161
+ rect.height === 0 ||
162
+ styles.opacity === '0' ||
163
+ styles.visibility === 'hidden' ||
164
+ styles.position === 'absolute' && rect.width < 2;
165
+ return {
166
+ selector: el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '') + (el.name ? `[name="${el.name}"]` : ''),
167
+ isHidden,
168
+ name: el.name,
169
+ };
170
+ }).filter(el => el.isHidden));
171
+ for (const select of hiddenSelects) {
172
+ issues.push({
173
+ category: "stability",
174
+ severity: "high",
175
+ element: select.selector,
176
+ description: "Hidden select with custom UI - agents may not find options",
177
+ detectionMethod: "hidden-select-check",
178
+ recommendation: "Add aria-expanded, role=\"listbox\" to custom dropdown, or make native select visible",
179
+ codeExample: `<div role="listbox" aria-expanded="false" aria-label="Select option">\n <div role="option" aria-selected="true">Option 1</div>\n</div>`,
180
+ });
181
+ summary.hiddenInputs++;
182
+ summary.customDropdowns++;
183
+ }
184
+ // Find hidden file inputs
185
+ const hiddenFileInputs = await page.$$eval('input[type="file"]', (elements) => elements.map(el => {
186
+ const rect = el.getBoundingClientRect();
187
+ const styles = window.getComputedStyle(el);
188
+ const isHidden = rect.width < 2 ||
189
+ rect.height < 2 ||
190
+ styles.opacity === '0' ||
191
+ styles.visibility === 'hidden';
192
+ return {
193
+ selector: `input[type="file"]${el.id ? `#${el.id}` : ''}`,
194
+ isHidden,
195
+ };
196
+ }).filter(el => el.isHidden));
197
+ for (const input of hiddenFileInputs) {
198
+ issues.push({
199
+ category: "stability",
200
+ severity: "medium",
201
+ element: input.selector,
202
+ description: "Hidden file input - agents must trigger it via label or button",
203
+ detectionMethod: "hidden-file-input-check",
204
+ recommendation: "Ensure the trigger element has for=\"input-id\" or aria-controls",
205
+ codeExample: `<label for="file-upload" tabindex="0" role="button">Upload File</label>\n<input type="file" id="file-upload" />`,
206
+ });
207
+ summary.hiddenInputs++;
208
+ }
209
+ }
210
+ /**
211
+ * Detect sticky/fixed elements that may intercept clicks
212
+ */
213
+ async function detectStickyOverlays(ctx) {
214
+ const { page, issues, summary } = ctx;
215
+ const stickyElements = await page.$$eval('*', (elements) => {
216
+ const results = [];
217
+ for (const el of elements) {
218
+ const styles = window.getComputedStyle(el);
219
+ const position = styles.position;
220
+ if (position === 'fixed' || position === 'sticky') {
221
+ const rect = el.getBoundingClientRect();
222
+ const zIndex = parseInt(styles.zIndex) || 0;
223
+ const tag = el.tagName.toLowerCase();
224
+ const id = el.id ? `#${el.id}` : '';
225
+ const className = el.className && typeof el.className === 'string'
226
+ ? `.${el.className.split(' ')[0]}`
227
+ : '';
228
+ results.push({
229
+ selector: tag + id + className,
230
+ position,
231
+ zIndex,
232
+ height: rect.height,
233
+ isHeader: rect.top < 100,
234
+ isFooter: rect.bottom > window.innerHeight - 100,
235
+ });
236
+ }
237
+ }
238
+ return results.filter(el => el.height > 40 && el.zIndex >= 0);
239
+ });
240
+ for (const sticky of stickyElements) {
241
+ // Only flag non-trivial sticky elements
242
+ if (sticky.height > 60) {
243
+ issues.push({
244
+ category: "stability",
245
+ severity: sticky.zIndex > 100 ? "high" : "medium",
246
+ element: sticky.selector,
247
+ description: `${sticky.position} element may intercept clicks (z-index: ${sticky.zIndex}, height: ${sticky.height}px)`,
248
+ detectionMethod: "sticky-element-check",
249
+ recommendation: "Add scroll-margin-top to target elements, or ensure proper z-index layering",
250
+ codeExample: sticky.isHeader
251
+ ? `/* Add to elements that sticky header might cover */\n.target-element {\n scroll-margin-top: ${sticky.height + 20}px;\n}`
252
+ : `/* Ensure modal/overlay has backdrop to prevent accidental clicks */`,
253
+ });
254
+ summary.stickyOverlays++;
255
+ }
256
+ }
257
+ }
258
+ /**
259
+ * Detect div/span with onclick but no button semantics
260
+ */
261
+ async function detectClickableDivs(ctx) {
262
+ const { page, issues } = ctx;
263
+ const clickableDivs = await page.$$eval('div[onclick], span[onclick], div[data-action], span[data-action], [style*="cursor: pointer"]:not(button):not(a):not([role="button"])', (elements) => elements.map(el => ({
264
+ selector: el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '') + (el.className && typeof el.className === 'string' ? `.${el.className.split(' ')[0]}` : ''),
265
+ hasOnclick: el.hasAttribute('onclick'),
266
+ hasRole: el.hasAttribute('role'),
267
+ text: el.textContent?.trim().slice(0, 50) || '',
268
+ })).filter(el => !el.hasRole));
269
+ for (const div of clickableDivs) {
270
+ issues.push({
271
+ category: "semantics",
272
+ severity: "medium",
273
+ element: div.selector,
274
+ description: `Clickable ${div.selector.split('.')[0]} without button role`,
275
+ detectionMethod: "clickable-div-check",
276
+ recommendation: "Replace with <button> or add role=\"button\" and tabindex=\"0\"",
277
+ codeExample: `<!-- Better: use semantic button -->\n<button onclick="...">${div.text || 'Action'}</button>\n\n<!-- If div is needed: -->\n<div role="button" tabindex="0" onclick="..." onkeydown="if(event.key==='Enter')...">${div.text || 'Action'}</div>`,
278
+ });
279
+ }
280
+ }
281
+ /**
282
+ * Detect images without alt text
283
+ */
284
+ async function detectMissingAltText(ctx) {
285
+ const { page, issues, summary } = ctx;
286
+ const imagesWithoutAlt = await page.$$eval('img:not([alt])', (elements) => elements.map(el => {
287
+ const imgEl = el;
288
+ return {
289
+ selector: `img${imgEl.id ? `#${imgEl.id}` : ''}[src="${imgEl.src.slice(0, 50)}..."]`,
290
+ src: imgEl.src,
291
+ isDecorative: imgEl.width < 20 || imgEl.height < 20,
292
+ };
293
+ }).filter(el => !el.isDecorative));
294
+ for (const img of imagesWithoutAlt) {
295
+ issues.push({
296
+ category: "accessibility",
297
+ severity: "medium",
298
+ element: img.selector,
299
+ description: "Image without alt text",
300
+ detectionMethod: "img-alt-check",
301
+ recommendation: "Add descriptive alt text, or alt=\"\" if decorative",
302
+ codeExample: `<img src="..." alt="Description of the image" />`,
303
+ });
304
+ summary.missingAriaLabels++;
305
+ }
306
+ }
307
+ /**
308
+ * Detect links without href or with javascript: href
309
+ */
310
+ async function detectBadLinks(ctx) {
311
+ const { page, issues } = ctx;
312
+ const badLinks = await page.$$eval('a:not([href]), a[href=""], a[href="#"], a[href^="javascript:"]', (elements) => elements.map(el => ({
313
+ selector: `a${el.id ? `#${el.id}` : ''}`,
314
+ text: el.textContent?.trim().slice(0, 30) || '',
315
+ href: el.getAttribute('href') || '',
316
+ })));
317
+ for (const link of badLinks) {
318
+ issues.push({
319
+ category: "semantics",
320
+ severity: "low",
321
+ element: link.selector,
322
+ description: `Link with ${link.href ? 'javascript:' : 'no'} href acts as button`,
323
+ detectionMethod: "link-href-check",
324
+ recommendation: "Use <button> for actions, <a href> for navigation",
325
+ codeExample: `<!-- For actions, use button: -->\n<button onclick="...">${link.text || 'Action'}</button>\n\n<!-- For navigation, use proper href: -->\n<a href="/path">${link.text || 'Link'}</a>`,
326
+ });
327
+ }
328
+ }
329
+ /**
330
+ * Detect elements only findable by fuzzy/visual match
331
+ */
332
+ async function detectLowFindabilityElements(ctx) {
333
+ const { page, issues, summary } = ctx;
334
+ // Check for buttons/links that lack good selectors
335
+ const poorSelectors = await page.$$eval('button, a, [role="button"]', (elements) => elements.map(el => {
336
+ const hasId = !!el.id;
337
+ const hasTestId = el.hasAttribute('data-testid') || el.hasAttribute('data-test') || el.hasAttribute('data-cy');
338
+ const hasAriaLabel = el.hasAttribute('aria-label');
339
+ const hasName = el.hasAttribute('name');
340
+ const hasGoodClass = el.className && typeof el.className === 'string' &&
341
+ /btn|button|cta|submit|action/i.test(el.className);
342
+ const text = el.textContent?.trim() || '';
343
+ // Score how findable this element is
344
+ const findabilityScore = (hasId ? 3 : 0) +
345
+ (hasTestId ? 3 : 0) +
346
+ (hasAriaLabel ? 2 : 0) +
347
+ (hasName ? 2 : 0) +
348
+ (hasGoodClass ? 1 : 0) +
349
+ (text.length > 0 && text.length < 50 ? 2 : 0);
350
+ return {
351
+ selector: el.tagName.toLowerCase() + (el.id ? `#${el.id}` : ''),
352
+ text: text.slice(0, 30),
353
+ findabilityScore,
354
+ suggestions: {
355
+ needsId: !hasId,
356
+ needsTestId: !hasTestId,
357
+ needsAriaLabel: !hasAriaLabel && !text,
358
+ },
359
+ };
360
+ }).filter(el => el.findabilityScore < 3));
361
+ for (const el of poorSelectors.slice(0, 10)) { // Limit to avoid noise
362
+ issues.push({
363
+ category: "findability",
364
+ severity: "low",
365
+ element: el.selector,
366
+ description: `Element lacks stable selectors (score: ${el.findabilityScore}/10)`,
367
+ detectionMethod: "findability-score-check",
368
+ recommendation: el.suggestions.needsTestId
369
+ ? "Add data-testid for stable automation selectors"
370
+ : el.suggestions.needsAriaLabel
371
+ ? "Add aria-label for accessibility and findability"
372
+ : "Add unique id or data-testid",
373
+ codeExample: `<button data-testid="submit-form" aria-label="Submit form">${el.text || '...'}</button>`,
374
+ });
375
+ }
376
+ summary.totalElements += poorSelectors.length;
377
+ }
378
+ // ============================================================================
379
+ // Report Generation
380
+ // ============================================================================
381
+ function generateRecommendations(issues) {
382
+ // Group issues by category and sort by severity
383
+ const grouped = issues.reduce((acc, issue) => {
384
+ const key = `${issue.category}-${issue.severity}`;
385
+ if (!acc[key])
386
+ acc[key] = [];
387
+ acc[key].push(issue);
388
+ return acc;
389
+ }, {});
390
+ const recommendations = [];
391
+ let priority = 1;
392
+ // Critical issues first
393
+ const severityOrder = ['critical', 'high', 'medium', 'low'];
394
+ for (const severity of severityOrder) {
395
+ for (const category of Object.keys(CATEGORY_WEIGHTS)) {
396
+ const key = `${category}-${severity}`;
397
+ const categoryIssues = grouped[key];
398
+ if (categoryIssues && categoryIssues.length > 0) {
399
+ // Group similar issues
400
+ const issueTypes = new Map();
401
+ for (const issue of categoryIssues) {
402
+ const type = issue.detectionMethod;
403
+ if (!issueTypes.has(type))
404
+ issueTypes.set(type, []);
405
+ issueTypes.get(type).push(issue);
406
+ }
407
+ for (const [type, typeIssues] of issueTypes) {
408
+ const representative = typeIssues[0];
409
+ const count = typeIssues.length;
410
+ recommendations.push({
411
+ priority: priority++,
412
+ category: representative.category,
413
+ issue: count > 1
414
+ ? `${count} ${representative.description}s found`
415
+ : representative.description,
416
+ fix: representative.recommendation,
417
+ effort: severity === 'critical' || severity === 'high' ? 'easy' : 'trivial',
418
+ impact: severity === 'critical' ? 'high' : severity === 'high' ? 'high' : 'medium',
419
+ codeSnippet: representative.codeExample,
420
+ });
421
+ }
422
+ }
423
+ }
424
+ }
425
+ return recommendations;
426
+ }
427
+ export function formatAgentReadyReport(result) {
428
+ const gradeEmoji = {
429
+ A: '🟢',
430
+ B: '🟡',
431
+ C: '🟠',
432
+ D: '🔴',
433
+ F: '⛔',
434
+ };
435
+ let report = `
436
+ ╔══════════════════════════════════════════════════════════════════════════════╗
437
+ ║ AGENT-READY AUDIT REPORT ║
438
+ ╚══════════════════════════════════════════════════════════════════════════════╝
439
+
440
+ URL: ${result.url}
441
+ Timestamp: ${result.timestamp}
442
+ Duration: ${(result.duration / 1000).toFixed(1)}s
443
+
444
+ ⚠️ METHODOLOGY: Letter grades indicate AI agent compatibility level.*
445
+ Grade A/B: Works well with agents | C: May need workarounds | D/F: Significant barriers
446
+ *Based on pattern detection. See documentation for methodology and sources.
447
+
448
+ ┌────────────────────────────────────────────────────────────────────────────┐
449
+ │ GRADE: ${gradeEmoji[result.grade]} ${result.grade} │
450
+ ├────────────────────────────────────────────────────────────────────────────┤
451
+ │ │
452
+ │ Findability ${result.score.findability}/100 ${'█'.repeat(Math.floor(result.score.findability / 10))}${'░'.repeat(10 - Math.floor(result.score.findability / 10))} │
453
+ │ Stability ${result.score.stability}/100 ${'█'.repeat(Math.floor(result.score.stability / 10))}${'░'.repeat(10 - Math.floor(result.score.stability / 10))} │
454
+ │ Accessibility ${result.score.accessibility}/100 ${'█'.repeat(Math.floor(result.score.accessibility / 10))}${'░'.repeat(10 - Math.floor(result.score.accessibility / 10))} │
455
+ │ Semantics ${result.score.semantics}/100 ${'█'.repeat(Math.floor(result.score.semantics / 10))}${'░'.repeat(10 - Math.floor(result.score.semantics / 10))} │
456
+ │ │
457
+ └────────────────────────────────────────────────────────────────────────────┘
458
+
459
+ SUMMARY
460
+ ───────
461
+ Total elements scanned: ${result.summary.totalElements}
462
+ Problematic elements: ${result.summary.problematicElements}
463
+ Missing ARIA labels: ${result.summary.missingAriaLabels}
464
+ Hidden inputs: ${result.summary.hiddenInputs}
465
+ Sticky overlays: ${result.summary.stickyOverlays}
466
+ Custom dropdowns: ${result.summary.customDropdowns}
467
+
468
+ `;
469
+ if (result.recommendations.length > 0) {
470
+ report += `TOP RECOMMENDATIONS
471
+ ───────────────────
472
+ `;
473
+ for (const rec of result.recommendations.slice(0, 10)) {
474
+ const severityIcon = rec.impact === 'high' ? '🔴' : rec.impact === 'medium' ? '🟠' : '🟡';
475
+ report += `
476
+ ${rec.priority}. [${severityIcon} ${rec.impact.toUpperCase()}] ${rec.issue}
477
+ → ${rec.fix}
478
+ ${rec.codeSnippet ? ` \`\`\`\n ${rec.codeSnippet.split('\n').join('\n ')}\n \`\`\`\n` : ''}`;
479
+ }
480
+ }
481
+ report += `
482
+ ISSUES BY CATEGORY
483
+ ──────────────────
484
+ Findability: ${result.issues.filter(i => i.category === 'findability').length} issues
485
+ Stability: ${result.issues.filter(i => i.category === 'stability').length} issues
486
+ Accessibility: ${result.issues.filter(i => i.category === 'accessibility').length} issues
487
+ Semantics: ${result.issues.filter(i => i.category === 'semantics').length} issues
488
+
489
+ ─────────────────────────────────────────────────────────────────────────────
490
+ * Methodology and research sources: docs/METHODOLOGY.md
491
+ Key sources: Nielsen Norman Group (severity scale), WCAG 2.1, WebAIM
492
+
493
+ Generated by CBrowser v8.0.0 - Agent-Ready Audit
494
+ `;
495
+ return report;
496
+ }
497
+ export function generateAgentReadyHtmlReport(result) {
498
+ const gradeColor = {
499
+ A: '#10b981',
500
+ B: '#84cc16',
501
+ C: '#f59e0b',
502
+ D: '#ef4444',
503
+ F: '#7f1d1d',
504
+ };
505
+ const issueRows = result.issues.slice(0, 50).map(issue => `
506
+ <tr class="severity-${issue.severity}">
507
+ <td><span class="badge badge-${issue.category}">${issue.category}</span></td>
508
+ <td><span class="badge badge-${issue.severity}">${issue.severity}</span></td>
509
+ <td><code>${issue.element}</code></td>
510
+ <td>${issue.description}</td>
511
+ <td>${issue.recommendation}</td>
512
+ </tr>
513
+ `).join('');
514
+ const recommendationCards = result.recommendations.slice(0, 10).map(rec => `
515
+ <div class="rec-card impact-${rec.impact}">
516
+ <div class="rec-header">
517
+ <span class="priority">#${rec.priority}</span>
518
+ <span class="badge badge-${rec.impact}">${rec.impact}</span>
519
+ <span class="badge badge-effort-${rec.effort}">${rec.effort}</span>
520
+ </div>
521
+ <h4>${rec.issue}</h4>
522
+ <p>${rec.fix}</p>
523
+ ${rec.codeSnippet ? `<pre><code>${rec.codeSnippet.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre>` : ''}
524
+ </div>
525
+ `).join('');
526
+ return `<!DOCTYPE html>
527
+ <html lang="en">
528
+ <head>
529
+ <meta charset="UTF-8">
530
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
531
+ <title>Agent-Ready Audit - ${result.url}</title>
532
+ <style>
533
+ * { box-sizing: border-box; }
534
+ body {
535
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
536
+ max-width: 1400px;
537
+ margin: 0 auto;
538
+ padding: 2rem;
539
+ background: #0f172a;
540
+ color: #e2e8f0;
541
+ }
542
+ h1 {
543
+ color: #f8fafc;
544
+ border-bottom: 3px solid #3b82f6;
545
+ padding-bottom: 0.5rem;
546
+ }
547
+ h2 { color: #94a3b8; margin-top: 2rem; }
548
+ .meta {
549
+ background: #1e293b;
550
+ padding: 1rem;
551
+ border-radius: 8px;
552
+ margin-bottom: 1rem;
553
+ }
554
+ .meta p { margin: 0.25rem 0; }
555
+ .score-card {
556
+ background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
557
+ border-radius: 16px;
558
+ padding: 2rem;
559
+ text-align: center;
560
+ margin: 2rem 0;
561
+ }
562
+ .score-value {
563
+ font-size: 4rem;
564
+ font-weight: bold;
565
+ color: ${gradeColor[result.grade]};
566
+ }
567
+ .grade {
568
+ font-size: 2rem;
569
+ margin-top: 0.5rem;
570
+ padding: 0.5rem 2rem;
571
+ background: ${gradeColor[result.grade]}33;
572
+ border: 2px solid ${gradeColor[result.grade]};
573
+ border-radius: 8px;
574
+ display: inline-block;
575
+ }
576
+ .score-bars {
577
+ display: grid;
578
+ grid-template-columns: repeat(4, 1fr);
579
+ gap: 1rem;
580
+ margin-top: 2rem;
581
+ }
582
+ .score-bar {
583
+ background: #1e293b;
584
+ padding: 1rem;
585
+ border-radius: 8px;
586
+ text-align: center;
587
+ }
588
+ .score-bar .value {
589
+ font-size: 1.5rem;
590
+ font-weight: bold;
591
+ color: #3b82f6;
592
+ }
593
+ .score-bar .label {
594
+ font-size: 0.875rem;
595
+ color: #94a3b8;
596
+ }
597
+ .progress-bar {
598
+ height: 8px;
599
+ background: #334155;
600
+ border-radius: 4px;
601
+ margin-top: 0.5rem;
602
+ overflow: hidden;
603
+ }
604
+ .progress-fill {
605
+ height: 100%;
606
+ background: #3b82f6;
607
+ border-radius: 4px;
608
+ }
609
+ table {
610
+ width: 100%;
611
+ border-collapse: collapse;
612
+ background: #1e293b;
613
+ border-radius: 8px;
614
+ overflow: hidden;
615
+ margin: 1rem 0;
616
+ }
617
+ th, td {
618
+ padding: 0.75rem 1rem;
619
+ text-align: left;
620
+ border-bottom: 1px solid #334155;
621
+ }
622
+ th { background: #0f172a; color: #94a3b8; }
623
+ code {
624
+ background: #334155;
625
+ padding: 0.125rem 0.375rem;
626
+ border-radius: 4px;
627
+ font-size: 0.875rem;
628
+ }
629
+ pre {
630
+ background: #1e293b;
631
+ padding: 1rem;
632
+ border-radius: 8px;
633
+ overflow-x: auto;
634
+ font-size: 0.875rem;
635
+ }
636
+ pre code {
637
+ background: none;
638
+ padding: 0;
639
+ }
640
+ .badge {
641
+ padding: 0.25rem 0.5rem;
642
+ border-radius: 4px;
643
+ font-size: 0.75rem;
644
+ font-weight: 500;
645
+ }
646
+ .badge-findability { background: #3b82f633; color: #60a5fa; }
647
+ .badge-stability { background: #f59e0b33; color: #fbbf24; }
648
+ .badge-accessibility { background: #10b98133; color: #34d399; }
649
+ .badge-semantics { background: #8b5cf633; color: #a78bfa; }
650
+ .badge-critical { background: #7f1d1d; color: #fca5a5; }
651
+ .badge-high { background: #7f1d1d80; color: #f87171; }
652
+ .badge-medium { background: #78350f; color: #fbbf24; }
653
+ .badge-low { background: #365314; color: #a3e635; }
654
+ .badge-effort-trivial { background: #166534; color: #86efac; }
655
+ .badge-effort-easy { background: #1e3a8a; color: #93c5fd; }
656
+ .badge-effort-medium { background: #78350f; color: #fde047; }
657
+ .badge-effort-hard { background: #7f1d1d; color: #fca5a5; }
658
+ .rec-cards {
659
+ display: grid;
660
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
661
+ gap: 1rem;
662
+ }
663
+ .rec-card {
664
+ background: #1e293b;
665
+ border-radius: 8px;
666
+ padding: 1rem;
667
+ border-left: 4px solid #3b82f6;
668
+ }
669
+ .rec-card.impact-high { border-left-color: #ef4444; }
670
+ .rec-card.impact-medium { border-left-color: #f59e0b; }
671
+ .rec-card.impact-low { border-left-color: #10b981; }
672
+ .rec-header {
673
+ display: flex;
674
+ gap: 0.5rem;
675
+ align-items: center;
676
+ margin-bottom: 0.5rem;
677
+ }
678
+ .rec-header .priority {
679
+ font-weight: bold;
680
+ color: #94a3b8;
681
+ }
682
+ .rec-card h4 {
683
+ margin: 0.5rem 0;
684
+ color: #f8fafc;
685
+ }
686
+ .rec-card p {
687
+ color: #94a3b8;
688
+ margin: 0.5rem 0;
689
+ }
690
+ .summary-grid {
691
+ display: grid;
692
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
693
+ gap: 1rem;
694
+ margin: 1rem 0;
695
+ }
696
+ .summary-stat {
697
+ background: #1e293b;
698
+ padding: 1rem;
699
+ border-radius: 8px;
700
+ text-align: center;
701
+ }
702
+ .summary-stat .value {
703
+ font-size: 1.5rem;
704
+ font-weight: bold;
705
+ color: #3b82f6;
706
+ }
707
+ .summary-stat .label {
708
+ font-size: 0.875rem;
709
+ color: #94a3b8;
710
+ }
711
+ .disclaimer {
712
+ background: #1e3a5f;
713
+ border-left: 4px solid #3b82f6;
714
+ padding: 1rem;
715
+ margin: 1rem 0;
716
+ border-radius: 0 8px 8px 0;
717
+ }
718
+ .disclaimer h4 {
719
+ margin: 0 0 0.5rem 0;
720
+ color: #60a5fa;
721
+ }
722
+ .disclaimer p {
723
+ margin: 0.25rem 0;
724
+ font-size: 0.875rem;
725
+ color: #94a3b8;
726
+ }
727
+ .footnote {
728
+ font-size: 0.75rem;
729
+ color: #64748b;
730
+ margin-top: 1rem;
731
+ padding-top: 1rem;
732
+ border-top: 1px solid #334155;
733
+ }
734
+ </style>
735
+ </head>
736
+ <body>
737
+ <h1>🤖 Agent-Ready Audit Report</h1>
738
+
739
+ <div class="disclaimer">
740
+ <h4>⚠️ Methodology Note</h4>
741
+ <p>Letter grades indicate AI agent compatibility level based on <strong>pattern detection</strong>, not precise measurements.*</p>
742
+ <p><strong>A/B:</strong> Works well with agents | <strong>C:</strong> May need workarounds | <strong>D/F:</strong> Significant barriers</p>
743
+ <p style="font-size: 0.75rem; margin-top: 0.5rem;">*Severity calibrated to Nielsen's usability scale. Touch targets per WCAG 2.5.5/2.5.8 (44x44px min).</p>
744
+ </div>
745
+
746
+ <div class="meta">
747
+ <p><strong>URL:</strong> ${result.url}</p>
748
+ <p><strong>Timestamp:</strong> ${result.timestamp}</p>
749
+ <p><strong>Duration:</strong> ${(result.duration / 1000).toFixed(1)}s</p>
750
+ </div>
751
+
752
+ <div class="score-card">
753
+ <div class="score-value">${result.score.overall}</div>
754
+ <div class="grade">${result.grade}</div>
755
+ <div class="score-bars">
756
+ <div class="score-bar">
757
+ <div class="value">${result.score.findability}</div>
758
+ <div class="label">Findability</div>
759
+ <div class="progress-bar"><div class="progress-fill" style="width: ${result.score.findability}%"></div></div>
760
+ </div>
761
+ <div class="score-bar">
762
+ <div class="value">${result.score.stability}</div>
763
+ <div class="label">Stability</div>
764
+ <div class="progress-bar"><div class="progress-fill" style="width: ${result.score.stability}%"></div></div>
765
+ </div>
766
+ <div class="score-bar">
767
+ <div class="value">${result.score.accessibility}</div>
768
+ <div class="label">Accessibility</div>
769
+ <div class="progress-bar"><div class="progress-fill" style="width: ${result.score.accessibility}%"></div></div>
770
+ </div>
771
+ <div class="score-bar">
772
+ <div class="value">${result.score.semantics}</div>
773
+ <div class="label">Semantics</div>
774
+ <div class="progress-bar"><div class="progress-fill" style="width: ${result.score.semantics}%"></div></div>
775
+ </div>
776
+ </div>
777
+ </div>
778
+
779
+ <h2>Summary</h2>
780
+ <div class="summary-grid">
781
+ <div class="summary-stat">
782
+ <div class="value">${result.summary.totalElements}</div>
783
+ <div class="label">Total Elements</div>
784
+ </div>
785
+ <div class="summary-stat">
786
+ <div class="value">${result.summary.problematicElements}</div>
787
+ <div class="label">With Issues</div>
788
+ </div>
789
+ <div class="summary-stat">
790
+ <div class="value">${result.summary.missingAriaLabels}</div>
791
+ <div class="label">Missing ARIA</div>
792
+ </div>
793
+ <div class="summary-stat">
794
+ <div class="value">${result.summary.hiddenInputs}</div>
795
+ <div class="label">Hidden Inputs</div>
796
+ </div>
797
+ <div class="summary-stat">
798
+ <div class="value">${result.summary.stickyOverlays}</div>
799
+ <div class="label">Sticky Overlays</div>
800
+ </div>
801
+ <div class="summary-stat">
802
+ <div class="value">${result.summary.customDropdowns}</div>
803
+ <div class="label">Custom Dropdowns</div>
804
+ </div>
805
+ </div>
806
+
807
+ <h2>Top Recommendations</h2>
808
+ <div class="rec-cards">
809
+ ${recommendationCards}
810
+ </div>
811
+
812
+ <h2>All Issues (${result.issues.length})</h2>
813
+ <table>
814
+ <thead>
815
+ <tr>
816
+ <th>Category</th>
817
+ <th>Severity</th>
818
+ <th>Element</th>
819
+ <th>Issue</th>
820
+ <th>Fix</th>
821
+ </tr>
822
+ </thead>
823
+ <tbody>
824
+ ${issueRows}
825
+ </tbody>
826
+ </table>
827
+
828
+ <div class="footnote">
829
+ <p>* Methodology and research sources: <a href="docs/METHODOLOGY.md" style="color: #60a5fa;">docs/METHODOLOGY.md</a></p>
830
+ <p>Key sources: Nielsen Norman Group (severity scale), WCAG 2.1, WebAIM Million (2024)</p>
831
+ </div>
832
+
833
+ <p style="color: #64748b; text-align: center; margin-top: 2rem;">
834
+ Generated by CBrowser v8.0.0 - Agent-Ready Audit
835
+ </p>
836
+ </body>
837
+ </html>`;
838
+ }
839
+ // ============================================================================
840
+ // Main Audit Function
841
+ // ============================================================================
842
+ export async function runAgentReadyAudit(url, options = {}) {
843
+ const startTime = Date.now();
844
+ let browser = null;
845
+ try {
846
+ browser = await chromium.launch({ headless: true });
847
+ const context = await browser.newContext({
848
+ viewport: { width: 1920, height: 1080 },
849
+ });
850
+ const page = await context.newPage();
851
+ // Navigate to URL
852
+ await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
853
+ // Wait for dynamic content
854
+ await page.waitForTimeout(2000);
855
+ // Initialize detection context
856
+ const issues = [];
857
+ const summary = {
858
+ totalElements: 0,
859
+ problematicElements: 0,
860
+ missingAriaLabels: 0,
861
+ hiddenInputs: 0,
862
+ stickyOverlays: 0,
863
+ customDropdowns: 0,
864
+ elementsWithoutText: 0,
865
+ };
866
+ const ctx = { page, issues, summary };
867
+ // Run all detection functions
868
+ await detectUnlabeledElements(ctx);
869
+ await detectHiddenInputs(ctx);
870
+ await detectStickyOverlays(ctx);
871
+ await detectClickableDivs(ctx);
872
+ await detectMissingAltText(ctx);
873
+ await detectBadLinks(ctx);
874
+ await detectLowFindabilityElements(ctx);
875
+ // Update summary
876
+ summary.problematicElements = issues.length;
877
+ // Calculate scores
878
+ const score = calculateAgentReadyScore(issues);
879
+ const grade = calculateGrade(score.overall);
880
+ // Generate recommendations
881
+ const recommendations = generateRecommendations(issues);
882
+ const result = {
883
+ url,
884
+ timestamp: new Date().toISOString(),
885
+ score,
886
+ issues,
887
+ recommendations,
888
+ summary,
889
+ grade,
890
+ duration: Date.now() - startTime,
891
+ };
892
+ return result;
893
+ }
894
+ finally {
895
+ if (browser) {
896
+ await browser.close();
897
+ }
898
+ }
899
+ }
900
+ //# sourceMappingURL=agent-ready-audit.js.map