@govtechsg/oobee 0.10.84 → 0.10.86

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 (40) hide show
  1. package/.github/workflows/image.yml +3 -2
  2. package/.github/workflows/publish.yml +10 -0
  3. package/DETAILS.md +29 -0
  4. package/dist/cli.js +7 -6
  5. package/dist/combine.js +1 -1
  6. package/dist/constants/common.js +15 -4
  7. package/dist/constants/constants.js +604 -1
  8. package/dist/crawlers/commonCrawlerFunc.js +3 -2
  9. package/dist/crawlers/crawlSitemap.js +98 -80
  10. package/dist/crawlers/custom/utils.js +218 -71
  11. package/dist/crawlers/guards/urlGuard.js +8 -15
  12. package/dist/crawlers/runCustom.js +24 -15
  13. package/dist/generateOobeeClientScanner.js +570 -0
  14. package/dist/mergeAxeResults.js +49 -29
  15. package/dist/npmIndex.js +10 -2
  16. package/dist/proxyService.js +18 -3
  17. package/dist/services/s3Uploader.js +21 -10
  18. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  19. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  20. package/dist/static/ejs/summary.ejs +10 -5
  21. package/oobee-client-scanner.js +34992 -0
  22. package/package.json +3 -3
  23. package/src/cli.ts +20 -15
  24. package/src/combine.ts +3 -1
  25. package/src/constants/common.ts +22 -10
  26. package/src/constants/constants.ts +602 -1
  27. package/src/crawlers/commonCrawlerFunc.ts +4 -3
  28. package/src/crawlers/crawlSitemap.ts +116 -98
  29. package/src/crawlers/custom/utils.ts +244 -84
  30. package/src/crawlers/guards/urlGuard.ts +24 -31
  31. package/src/crawlers/runCustom.ts +38 -15
  32. package/src/generateOobeeClientScanner.ts +591 -0
  33. package/src/mergeAxeResults.ts +48 -29
  34. package/src/npmIndex.ts +12 -2
  35. package/src/proxyService.ts +25 -4
  36. package/src/services/s3Uploader.ts +23 -11
  37. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  38. package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  39. package/src/static/ejs/summary.ejs +10 -5
  40. package/testStaticJSScanner.html +534 -0
@@ -23,12 +23,17 @@
23
23
  const scanItems = <%- JSON.stringify(
24
24
  {
25
25
  ...items,
26
- passed: {
27
- rules: items.passed.rules.map(r => delete r.pagesAffected),
28
- ...items.passed
29
- },
26
+ ...['mustFix','goodToFix','needsReview','passed'].reduce((acc, cat) => {
27
+ if (items[cat]) {
28
+ acc[cat] = {
29
+ ...items[cat],
30
+ rules: (items[cat].rules || []).map(({ htmlGroups, ...rest }) => rest),
31
+ };
32
+ }
33
+ return acc;
34
+ }, {}),
30
35
  }
31
- ) %>
36
+ ).replace(/<\//g, '<\\/') %>
32
37
  </script>
33
38
  <%- include('partials/scripts/summaryTable') %>
34
39
  <script>
@@ -0,0 +1,534 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ testStaticJSScanner.html
4
+ ========================
5
+ Demo page for oobee-client-scanner.js.
6
+
7
+ Steps to use:
8
+ 1. Build oobee: npm run build
9
+ 2. Generate the bundle: node dist/generateOobeeClientScanner.js
10
+ 3. Open this file in a browser (must be served, not file://, to avoid CORS on axe iframes):
11
+ npx serve .
12
+ Then visit: http://localhost:3000/testStaticJSScanner.html
13
+
14
+ The page contains intentional accessibility violations so the scanner has
15
+ something to find. Open the browser DevTools console to see the full JSON output.
16
+ -->
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="UTF-8" />
20
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
21
+ <title>Oobee Client Scanner — Test Page</title>
22
+ <style>
23
+ *, *::before, *::after { box-sizing: border-box; }
24
+ body {
25
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
+ max-width: 1200px; margin: 2rem auto; padding: 0 1.5rem;
27
+ color: #212529; line-height: 1.5;
28
+ }
29
+ h1 { color: #1a1a2e; margin-bottom: 0.25rem; }
30
+ h2 { color: #343a40; border-bottom: 2px solid #dee2e6; padding-bottom: 0.4rem; margin-top: 2rem; }
31
+ h3 { color: #495057; margin-top: 1.5rem; }
32
+
33
+ /* ── Status bar ────────────────────────────────────────────────────────── */
34
+ .status {
35
+ padding: 0.85rem 1rem; border-radius: 6px; margin: 1rem 0;
36
+ font-weight: 500;
37
+ }
38
+ .status.running { background: #fff3cd; border-left: 4px solid #ffc107; }
39
+ .status.done { background: #d4edda; border-left: 4px solid #28a745; }
40
+ .status.error { background: #f8d7da; border-left: 4px solid #dc3545; }
41
+
42
+ /* ── Category badges ───────────────────────────────────────────────────── */
43
+ .badge {
44
+ display: inline-block; padding: 2px 8px; border-radius: 4px;
45
+ font-size: 0.72rem; font-weight: 700; color: #fff; white-space: nowrap;
46
+ }
47
+ .badge.mustFix { background: #dc3545; }
48
+ .badge.goodToFix { background: #fd7e14; }
49
+ .badge.needsReview { background: #6c757d; }
50
+
51
+ /* ── Summary table ─────────────────────────────────────────────────────── */
52
+ table.summary-table {
53
+ border-collapse: collapse; margin: 0.75rem 0;
54
+ }
55
+ table.summary-table th,
56
+ table.summary-table td {
57
+ border: 1px solid #dee2e6; padding: 0.45rem 0.8rem; text-align: left;
58
+ }
59
+ table.summary-table th { background: #f1f3f5; font-weight: 600; }
60
+
61
+ /* ── Issues table ──────────────────────────────────────────────────────── */
62
+ .table-scroll { overflow-x: auto; margin: 1rem 0; }
63
+ table.issues-table {
64
+ border-collapse: collapse; width: 100%; min-width: 900px;
65
+ font-size: 0.875rem;
66
+ }
67
+ table.issues-table th {
68
+ background: #1a1a2e; color: #fff;
69
+ padding: 0.55rem 0.75rem; text-align: left;
70
+ font-weight: 600; white-space: nowrap;
71
+ }
72
+ table.issues-table td {
73
+ border: 1px solid #dee2e6; padding: 0.5rem 0.75rem;
74
+ vertical-align: top;
75
+ }
76
+ table.issues-table tr:nth-child(even) td { background: #f8f9fa; }
77
+ table.issues-table tr:hover td { background: #e9f4ff; }
78
+
79
+ /* Rule ID cell */
80
+ .rule-id { font-family: monospace; font-size: 0.82rem; color: #0d6efd; font-weight: 600; }
81
+
82
+ /* Conformance tags */
83
+ .conf-tag {
84
+ display: inline-block; margin: 1px 2px;
85
+ padding: 1px 5px; border-radius: 3px; font-size: 0.7rem;
86
+ background: #e9ecef; color: #495057; border: 1px solid #ced4da;
87
+ }
88
+
89
+ /* Occurrences pill */
90
+ .occ-pill {
91
+ display: inline-block; padding: 2px 8px; border-radius: 12px;
92
+ background: #e9ecef; font-weight: 600; font-size: 0.8rem;
93
+ }
94
+
95
+ /* Go-to-element links */
96
+ .goto-link {
97
+ display: inline-block; margin: 2px 0;
98
+ font-size: 0.78rem; color: #0d6efd; text-decoration: none;
99
+ white-space: nowrap;
100
+ }
101
+ .goto-link:hover { text-decoration: underline; }
102
+ .item-message {
103
+ display: block; font-size: 0.75rem; color: #6c757d;
104
+ margin: 1px 0 6px; font-style: italic;
105
+ }
106
+
107
+ /* Long description cell — collapsible */
108
+ details summary {
109
+ cursor: pointer; color: #0d6efd; font-size: 0.82rem;
110
+ user-select: none; list-style: none;
111
+ }
112
+ details summary::before { content: '▶ '; font-size: 0.7rem; }
113
+ details[open] summary::before { content: '▼ '; }
114
+ details p { margin: 0.4rem 0 0; font-size: 0.83rem; color: #495057; }
115
+
116
+ /* Step-by-step guide */
117
+ .steps { margin: 0; padding: 0; list-style: none; font-size: 0.82rem; }
118
+ .steps li { margin-bottom: 0.35rem; }
119
+ .step-label {
120
+ display: inline-block; min-width: 3.5rem;
121
+ font-weight: 700; color: #343a40;
122
+ }
123
+
124
+ /* ── Raw JSON block ────────────────────────────────────────────────────── */
125
+ pre {
126
+ background: #1e1e1e; color: #d4d4d4;
127
+ padding: 1rem; border-radius: 6px;
128
+ overflow: auto; max-height: 500px;
129
+ font-size: 0.78rem; line-height: 1.4;
130
+ }
131
+
132
+ /* ── Check Accessibility button ───────────────────────────────────────── */
133
+ .scan-btn {
134
+ display: inline-flex; align-items: center; gap: 0.5rem;
135
+ padding: 0.65rem 1.4rem; font-size: 1rem; font-weight: 600;
136
+ color: #fff; background: #1a1a2e; border: none; border-radius: 6px;
137
+ cursor: pointer; transition: background 0.15s;
138
+ }
139
+ .scan-btn:hover { background: #2e2e5e; }
140
+ .scan-btn:active { background: #0d0d1a; }
141
+ .scan-btn:disabled { background: #6c757d; cursor: not-allowed; }
142
+
143
+ /* ── Integration instructions ─────────────────────────────────────────── */
144
+ .integration-box {
145
+ background: #f0f4ff; border: 1px solid #c7d4f5;
146
+ border-radius: 8px; padding: 1.25rem 1.5rem; margin: 1.5rem 0;
147
+ }
148
+ .integration-box h2 {
149
+ margin-top: 0; border-bottom: none; font-size: 1rem;
150
+ color: #1a1a2e; padding-bottom: 0;
151
+ }
152
+ .integration-box ol { margin: 0.5rem 0 0 1.25rem; padding: 0; }
153
+ .integration-box li { margin-bottom: 0.5rem; font-size: 0.9rem; }
154
+ .integration-box pre {
155
+ background: #1e1e1e; color: #d4d4d4;
156
+ padding: 0.85rem 1rem; border-radius: 5px;
157
+ overflow-x: auto; max-height: none;
158
+ font-size: 0.78rem; line-height: 1.5; margin: 0.5rem 0 0;
159
+ }
160
+ .integration-box code {
161
+ background: #e8edf8; color: #1a1a2e;
162
+ padding: 1px 5px; border-radius: 3px; font-size: 0.85em;
163
+ }
164
+
165
+ /* ── Violation sandbox ─────────────────────────────────────────────────── */
166
+ .violation-area {
167
+ border: 2px dashed #dc3545; padding: 1rem; margin: 2rem 0;
168
+ border-radius: 6px; background: #fff5f5;
169
+ }
170
+ .violation-area h3 { color: #dc3545; margin-top: 0; }
171
+ </style>
172
+ </head>
173
+ <body>
174
+
175
+ <h1>Oobee Client Scanner — Test Page</h1>
176
+ <p>
177
+ This page intentionally contains several accessibility violations so
178
+ <code>oobee-client-scanner.js</code> has something to detect.
179
+ Open the browser DevTools console for the full JSON output.
180
+ </p>
181
+
182
+ <!-- ── Integration instructions ────────────────────────────────────────── -->
183
+ <div class="integration-box">
184
+ <h2>How to integrate into your own page</h2>
185
+ <ol>
186
+ <li>
187
+ <strong>Add the script</strong> — include the scanner before your closing
188
+ <code>&lt;/body&gt;</code> tag:
189
+ <pre>&lt;script src="https://cdn.jsdelivr.net/gh/GovTechSG/oobee@v0.10.86/oobee-client-scanner.js"&gt;&lt;/script&gt;</pre>
190
+
191
+ <br>This points to the <code>v0.10.86</code> release. Update the version tag as needed for newer releases.
192
+ </li>
193
+ <li>
194
+ <strong>Run the scan</strong> — call <code>window.oobee.scan()</code> in your own script,
195
+ e.g. on a button click or on page load:
196
+ <pre>&lt;script&gt;
197
+ window.oobee.scan({
198
+ userInfo: { email: 'you@example.com', name: 'Your Name' },
199
+ // disableOobee: false, // true → skip oobee custom checks
200
+ // enableWcagAaa: true, // true → also run WCAG AAA rules
201
+ // elementsToScan: [], // [] = full page; or pass CSS selectors / DOM nodes
202
+ }).then(function(results) {
203
+ console.log(JSON.stringify(results, null, 2));
204
+ });
205
+ &lt;/script&gt;</pre>
206
+ </li>
207
+ <li>
208
+ <strong>Navigate to a flagged element</strong> — call
209
+ <code>window.oobee.scrollToElement(item.xpath)</code> with any
210
+ <code>item.xpath</code> value from the scan results to scroll and highlight it.
211
+ </li>
212
+ </ol>
213
+ </div>
214
+
215
+ <button id="scanBtn" class="scan-btn">Check Accessibility</button>
216
+
217
+ <!-- ── Status display ───────────────────────────────────────────────────── -->
218
+ <div id="status" style="display:none" class="status"></div>
219
+
220
+ <!-- ── Results (populated by JS) ───────────────────────────────────────── -->
221
+ <div id="summary" style="display:none">
222
+
223
+ <!-- Summary counts -->
224
+ <h2>Scan Summary</h2>
225
+ <table class="summary-table">
226
+ <thead>
227
+ <tr>
228
+ <th>Category</th>
229
+ <th>Total Occurrences</th>
230
+ <th>Unique Rules</th>
231
+ </tr>
232
+ </thead>
233
+ <tbody id="summaryBody"></tbody>
234
+ </table>
235
+
236
+ <!-- Issues table -->
237
+ <h2>Issues Found</h2>
238
+ <div class="table-scroll">
239
+ <table class="issues-table">
240
+ <thead>
241
+ <tr>
242
+ <th>Category</th>
243
+ <th>Rule ID</th>
244
+ <th>Occurrences</th>
245
+ <th>Elements</th>
246
+ <th>Conformance</th>
247
+ <th>Short Description</th>
248
+ <th>Long Description</th>
249
+ <th>Step-by-Step Guide</th>
250
+ </tr>
251
+ </thead>
252
+ <tbody id="issuesBody"></tbody>
253
+ </table>
254
+ </div>
255
+
256
+ <!-- Raw JSON: full scan results -->
257
+ <h2>Raw JSON — Full Scan Results</h2>
258
+ <p style="font-size:0.875rem; color:#6c757d;">
259
+ Complete output of <code>window.oobee.scan()</code>, including every occurrence per rule.
260
+ Use this to build your own UI on top of the scanner.
261
+ </p>
262
+ <pre id="jsonFullOutput"></pre>
263
+
264
+ <!-- Raw JSON: description maps only -->
265
+ <h2>Raw JSON — Description Maps</h2>
266
+ <p style="font-size:0.875rem; color:#6c757d;">
267
+ Flat maps of <code>shortDescription</code>, <code>longDescription</code>, and
268
+ <code>stepByStepGuide</code> keyed by rule ID — only rules found in this scan are included.
269
+ </p>
270
+ <pre id="jsonDescOutput"></pre>
271
+ </div>
272
+
273
+ <!-- ── Intentional violations ───────────────────────────────────────────── -->
274
+ <div class="violation-area">
275
+ <h3>Intentional Accessibility Violations</h3>
276
+ <p>The elements below contain violations for the scanner to catch.</p>
277
+
278
+ <!-- 1. img with no alt → "image-alt" mustFix -->
279
+ <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" width="50" height="50">
280
+
281
+ <!-- 2. img with confusing alt → "oobee-confusing-alt-text" -->
282
+ <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image" width="50" height="50">
283
+
284
+ <!-- 3. button with no accessible label → "button-name" mustFix -->
285
+ <button style="margin: 4px"><!-- empty --></button>
286
+
287
+ <!-- 4. input with no label → "label" mustFix -->
288
+ <input type="text" placeholder="Enter value" style="margin: 4px">
289
+
290
+ <!-- 5. link with non-descriptive text → may surface as incomplete/needsReview -->
291
+ <a href="#">click here</a>
292
+
293
+ <!-- 6. Low contrast text → "color-contrast" -->
294
+ <p style="color:#ccc; background:#fff; font-size:12px">This text has low contrast.</p>
295
+ </div>
296
+
297
+ <!-- ── oobee-client-scanner.js ──────────────────────────────────────────── -->
298
+ <script src="oobee-client-scanner.js"></script>
299
+
300
+ <!-- ── Run the scan ─────────────────────────────────────────────────────── -->
301
+ <script>
302
+ document.getElementById('scanBtn').addEventListener('click', async function () {
303
+ const btn = document.getElementById('scanBtn');
304
+ const statusEl = document.getElementById('status');
305
+
306
+ // Reset previous results
307
+ document.getElementById('summary').style.display = 'none';
308
+ document.getElementById('summaryBody').innerHTML = '';
309
+ document.getElementById('issuesBody').innerHTML = '';
310
+ document.getElementById('jsonFullOutput').textContent = '';
311
+ document.getElementById('jsonDescOutput').textContent = '';
312
+
313
+ btn.disabled = true;
314
+ btn.textContent = 'Scanning…';
315
+ statusEl.className = 'status running';
316
+ statusEl.textContent = 'Scan running…';
317
+ statusEl.style.display = 'block';
318
+
319
+ try {
320
+ if (!window.oobee) {
321
+ throw new Error(
322
+ 'window.oobee is not defined — make sure oobee-client-scanner.js loaded correctly.'
323
+ );
324
+ }
325
+
326
+ // ── Run the scan ──────────────────────────────────────────────────
327
+ const results = await window.oobee.scan({
328
+ disableOobee: false, // set true to skip oobee custom checks
329
+ enableWcagAaa: true, // set true to also include WCAG AAA rules
330
+ elementsToScan: [], // empty = scan the entire page
331
+ userInfo: {
332
+ email: 'accessibility@tech.gov.sg',
333
+ name: 'A11y Team',
334
+ },
335
+ });
336
+
337
+ // ── Log full JSON to console ──────────────────────────────────────
338
+ console.log('[oobee-client-scanner] Scan complete. Full results:');
339
+ console.log(JSON.stringify(results, null, 2));
340
+
341
+ // ── Update status ─────────────────────────────────────────────────
342
+ statusEl.className = 'status done';
343
+ statusEl.textContent = 'Scan complete. Summary and raw JSON output below.';
344
+
345
+ // ── Categories to render ──────────────────────────────────────────
346
+ const categories = [
347
+ { key: 'mustFix', label: 'Must Fix' },
348
+ { key: 'goodToFix', label: 'Good to Fix' },
349
+ { key: 'needsReview', label: 'Needs Review' },
350
+ ];
351
+
352
+ // ── Summary table ─────────────────────────────────────────────────
353
+ const summaryBody = document.getElementById('summaryBody');
354
+ categories.forEach(function ({ key, label }) {
355
+ const cat = results[key];
356
+ if (!cat) return;
357
+ const tr = document.createElement('tr');
358
+ tr.innerHTML =
359
+ '<td><span class="badge ' + key + '">' + label + '</span></td>' +
360
+ '<td>' + cat.totalItems + '</td>' +
361
+ '<td>' + Object.keys(cat.rules).length + '</td>';
362
+ summaryBody.appendChild(tr);
363
+ });
364
+
365
+ // ── Issues table ──────────────────────────────────────────────────
366
+ const issuesBody = document.getElementById('issuesBody');
367
+
368
+ categories.forEach(function ({ key, label }) {
369
+ const cat = results[key];
370
+ if (!cat || !cat.totalItems) return;
371
+
372
+ Object.entries(cat.rules).forEach(function ([ruleId, rule]) {
373
+ const tr = document.createElement('tr');
374
+
375
+ // Category
376
+ const tdCat = document.createElement('td');
377
+ tdCat.innerHTML = '<span class="badge ' + key + '">' + label + '</span>';
378
+
379
+ // Rule ID
380
+ const tdRule = document.createElement('td');
381
+ tdRule.innerHTML = '<span class="rule-id">' + escapeHtml(ruleId) + '</span>';
382
+
383
+ // Occurrences
384
+ const tdOcc = document.createElement('td');
385
+ tdOcc.innerHTML = '<span class="occ-pill">' + rule.totalItems + '</span>';
386
+
387
+ // Elements — one "Go to element" link + message per item with a non-empty xpath
388
+ const tdEl = document.createElement('td');
389
+ if (rule.items && rule.items.length) {
390
+ rule.items.forEach(function (item, idx) {
391
+ if (!item.xpath) return;
392
+ const a = document.createElement('a');
393
+ a.href = '#';
394
+ a.className = 'goto-link';
395
+ a.textContent = '↗ Element ' + (idx + 1);
396
+ a.title = item.xpath;
397
+ a.addEventListener('click', function (e) {
398
+ e.preventDefault();
399
+ window.oobee.scrollToElement(item.xpath);
400
+ });
401
+ tdEl.appendChild(a);
402
+ if (item.message) {
403
+ const lines = item.message.split('\n').map(function(l) { return l.trim(); }).filter(Boolean);
404
+ const wrap = document.createElement('span');
405
+ wrap.className = 'item-message';
406
+ if (lines.length <= 1) {
407
+ wrap.textContent = item.message;
408
+ } else {
409
+ // First line is the header (e.g. "Fix any of the following:")
410
+ const header = document.createElement('span');
411
+ header.textContent = lines[0];
412
+ wrap.appendChild(header);
413
+ const ul = document.createElement('ul');
414
+ ul.style.cssText = 'margin:2px 0 0 1rem; padding:0;';
415
+ lines.slice(1).forEach(function(line) {
416
+ const li = document.createElement('li');
417
+ li.textContent = line;
418
+ ul.appendChild(li);
419
+ });
420
+ wrap.appendChild(ul);
421
+ }
422
+ tdEl.appendChild(wrap);
423
+ } else {
424
+ tdEl.appendChild(document.createElement('br'));
425
+ }
426
+ });
427
+ }
428
+ if (!tdEl.hasChildNodes()) tdEl.textContent = '—';
429
+
430
+ // Conformance tags — formatted via window.oobee.formatConformance
431
+ const tdConf = document.createElement('td');
432
+ if (rule.conformance && rule.conformance.length) {
433
+ const fmt = window.oobee.formatConformance(rule.conformance);
434
+ const parts = fmt.criteria.slice();
435
+ if (fmt.level) parts.push('Level ' + fmt.level);
436
+ tdConf.innerHTML = parts.length
437
+ ? parts.map(function (c) { return '<span class="conf-tag">' + escapeHtml(c) + '</span>'; }).join(' ')
438
+ : '—';
439
+ } else {
440
+ tdConf.textContent = '—';
441
+ }
442
+
443
+ // Short description
444
+ const tdShort = document.createElement('td');
445
+ tdShort.textContent = rule.shortDescription || rule.description || '—';
446
+
447
+ // Long description — collapsible
448
+ const tdLong = document.createElement('td');
449
+ if (rule.longDescription) {
450
+ tdLong.innerHTML =
451
+ '<details><summary>Show</summary><p>' +
452
+ escapeHtml(rule.longDescription) +
453
+ '</p></details>';
454
+ } else {
455
+ tdLong.textContent = '—';
456
+ }
457
+
458
+ // Step-by-step guide
459
+ const tdSteps = document.createElement('td');
460
+ const guide = rule.stepByStepGuide;
461
+ if (guide) {
462
+ const steps = [
463
+ { n: '1', label: 'Check', text: guide.check },
464
+ { n: '2', label: 'Fix', text: guide.fix },
465
+ { n: '3', label: 'Review', text: guide.review },
466
+ { n: '4', label: 'Learn', text: guide.learn },
467
+ ];
468
+ tdSteps.innerHTML =
469
+ '<ul class="steps">' +
470
+ steps.map(function (s) {
471
+ return '<li>' +
472
+ '<span class="step-label">' + s.n + '. ' + s.label + ':</span> ' +
473
+ escapeHtml(s.text || '—') +
474
+ '</li>';
475
+ }).join('') +
476
+ '</ul>';
477
+ } else {
478
+ tdSteps.textContent = '—';
479
+ }
480
+
481
+ tr.append(tdCat, tdRule, tdOcc, tdEl, tdConf, tdShort, tdLong, tdSteps);
482
+ issuesBody.appendChild(tr);
483
+ });
484
+ });
485
+
486
+ // ── Raw JSON: description maps for found rules only ────────────────
487
+ const shortDescMap = {};
488
+ const longDescMap = {};
489
+ const stepByStepMap = {};
490
+
491
+ categories.forEach(function ({ key }) {
492
+ const cat = results[key];
493
+ if (!cat || !cat.rules) return;
494
+ Object.entries(cat.rules).forEach(function ([ruleId, rule]) {
495
+ if (rule.shortDescription !== undefined) shortDescMap[ruleId] = rule.shortDescription;
496
+ if (rule.longDescription !== undefined) longDescMap[ruleId] = rule.longDescription;
497
+ if (rule.stepByStepGuide !== undefined) stepByStepMap[ruleId] = rule.stepByStepGuide;
498
+ });
499
+ });
500
+
501
+ // ── Full scan results JSON ────────────────────────────────────────
502
+ document.getElementById('jsonFullOutput').textContent =
503
+ JSON.stringify(results, null, 2);
504
+
505
+ // ── Description maps JSON (found rules only) ──────────────────────
506
+ document.getElementById('jsonDescOutput').textContent = JSON.stringify(
507
+ { shortDescriptionMap: shortDescMap, longDescriptionMap: longDescMap, stepByStepGuideMap: stepByStepMap },
508
+ null, 2
509
+ );
510
+
511
+ document.getElementById('summary').style.display = 'block';
512
+
513
+ } catch (err) {
514
+ statusEl.className = 'status error';
515
+ statusEl.textContent = 'Scan error: ' + (err && err.message ? err.message : String(err));
516
+ statusEl.style.display = 'block';
517
+ console.error('[oobee-client-scanner] Error:', err);
518
+ } finally {
519
+ btn.disabled = false;
520
+ btn.textContent = 'Check Accessibility';
521
+ }
522
+ });
523
+
524
+ // ── Helper ──────────────────────────────────────────────────────────────
525
+ function escapeHtml(str) {
526
+ return String(str)
527
+ .replace(/&/g, '&amp;')
528
+ .replace(/</g, '&lt;')
529
+ .replace(/>/g, '&gt;')
530
+ .replace(/"/g, '&quot;');
531
+ }
532
+ </script>
533
+ </body>
534
+ </html>