cbrowser 8.10.0 → 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.
- package/README.md +121 -0
- package/dist/analysis/accessibility-empathy.d.ts +32 -0
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -0
- package/dist/analysis/accessibility-empathy.js +1005 -0
- package/dist/analysis/accessibility-empathy.js.map +1 -0
- package/dist/analysis/agent-ready-audit.d.ts +11 -0
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -0
- package/dist/analysis/agent-ready-audit.js +900 -0
- package/dist/analysis/agent-ready-audit.js.map +1 -0
- package/dist/analysis/competitive-benchmark.d.ts +11 -0
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -0
- package/dist/analysis/competitive-benchmark.js +1122 -0
- package/dist/analysis/competitive-benchmark.js.map +1 -0
- package/dist/analysis/index.d.ts +5 -1
- package/dist/analysis/index.d.ts.map +1 -1
- package/dist/analysis/index.js +5 -1
- package/dist/analysis/index.js.map +1 -1
- package/dist/cli.js +191 -2
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/focus-hierarchies.d.ts +113 -0
- package/dist/cognitive/focus-hierarchies.d.ts.map +1 -0
- package/dist/cognitive/focus-hierarchies.js +500 -0
- package/dist/cognitive/focus-hierarchies.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +100 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/personas.d.ts +14 -0
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +393 -0
- package/dist/personas.js.map +1 -1
- package/dist/types.d.ts +429 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/METHODOLOGY.md +245 -0
- 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, '<').replace(/>/g, '>')}</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
|