@govtechsg/oobee 0.10.85 → 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.
- package/.github/workflows/image.yml +3 -2
- package/.github/workflows/publish.yml +10 -0
- package/DETAILS.md +29 -0
- package/dist/combine.js +1 -1
- package/dist/constants/common.js +15 -4
- package/dist/constants/constants.js +604 -1
- package/dist/crawlers/commonCrawlerFunc.js +3 -2
- package/dist/crawlers/crawlSitemap.js +98 -80
- package/dist/crawlers/custom/utils.js +137 -31
- package/dist/crawlers/guards/urlGuard.js +8 -15
- package/dist/crawlers/runCustom.js +18 -11
- package/dist/generateOobeeClientScanner.js +570 -0
- package/dist/mergeAxeResults.js +5 -4
- package/dist/npmIndex.js +10 -2
- package/dist/proxyService.js +18 -3
- package/dist/services/s3Uploader.js +21 -10
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
- package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
- package/dist/static/ejs/summary.ejs +10 -5
- package/oobee-client-scanner.js +34992 -0
- package/package.json +2 -2
- package/src/combine.ts +3 -1
- package/src/constants/common.ts +22 -10
- package/src/constants/constants.ts +602 -1
- package/src/crawlers/commonCrawlerFunc.ts +4 -3
- package/src/crawlers/crawlSitemap.ts +116 -98
- package/src/crawlers/custom/utils.ts +143 -38
- package/src/crawlers/guards/urlGuard.ts +24 -31
- package/src/crawlers/runCustom.ts +29 -11
- package/src/generateOobeeClientScanner.ts +591 -0
- package/src/mergeAxeResults.ts +5 -3
- package/src/npmIndex.ts +12 -2
- package/src/proxyService.ts +25 -4
- package/src/services/s3Uploader.ts +23 -11
- package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
- package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
- package/src/static/ejs/summary.ejs +10 -5
- package/testStaticJSScanner.html +534 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generateOobeeClientScanner.ts
|
|
3
|
+
*
|
|
4
|
+
* Standalone script that generates oobee-client-scanner.js — a self-contained
|
|
5
|
+
* browser bundle that runs axe-core + oobee custom checks, returns results in
|
|
6
|
+
* the same JSON format as npmIndex's processAndSubmitResults, and reports
|
|
7
|
+
* telemetry to Sentry using the official Sentry JavaScript browser SDK.
|
|
8
|
+
*
|
|
9
|
+
* Usage (after `npm run build`):
|
|
10
|
+
* node dist/generateOobeeClientScanner.js [output-path]
|
|
11
|
+
*
|
|
12
|
+
* Default output: ./oobee-client-scanner.js (relative to cwd)
|
|
13
|
+
*
|
|
14
|
+
* Environment variables read at generation time:
|
|
15
|
+
* OOBEE_SENTRY_DSN — Sentry DSN to embed in the bundle (falls back to the
|
|
16
|
+
* default DSN in constants.ts if not set)
|
|
17
|
+
*
|
|
18
|
+
* Then in your HTML:
|
|
19
|
+
* <script src="oobee-client-scanner.js"></script>
|
|
20
|
+
* <script>
|
|
21
|
+
* window.oobee.scan({
|
|
22
|
+
* userInfo: { email: 'you@example.com', name: 'Your Name' },
|
|
23
|
+
* // scanMode: [string] choices: "default" | "disable-oobee" | "enable-wcag-aaa" | "disable-oobee,enable-wcag-aaa"
|
|
24
|
+
* disableOobee: false, // true → skip oobee custom checks
|
|
25
|
+
* enableWcagAaa: true, // true → also run WCAG AAA rules
|
|
26
|
+
* elementsToScan: [], // [] = full page; or CSS selectors / DOM nodes
|
|
27
|
+
* }).then(results => console.log(results));
|
|
28
|
+
*
|
|
29
|
+
* // Scroll to an element by CSS selector (item.xpath from scan results):
|
|
30
|
+
* window.oobee.scrollToElement(item.xpath);
|
|
31
|
+
* </script>
|
|
32
|
+
*/
|
|
33
|
+
import { writeFileSync } from 'fs';
|
|
34
|
+
import path from 'path';
|
|
35
|
+
import { createRequire } from 'module';
|
|
36
|
+
import { fileURLToPath } from 'url';
|
|
37
|
+
import axe from 'axe-core';
|
|
38
|
+
import { a11yRuleShortDescriptionMap, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide, sentryConfig, wcagCriteriaLabels, formatWcagId, } from './constants/constants.js';
|
|
39
|
+
import { getOobeeFunctionsScript } from './npmIndex.js';
|
|
40
|
+
import { getVersion } from './utils.js';
|
|
41
|
+
const _require = createRequire(import.meta.url);
|
|
42
|
+
const _filename = fileURLToPath(import.meta.url);
|
|
43
|
+
const _dirname = path.dirname(_filename);
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Sentry config — DSN is read from process.env at generation time
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
const SENTRY_DSN = sentryConfig.dsn; // already resolves OOBEE_SENTRY_DSN || default
|
|
48
|
+
const APP_VERSION = getVersion();
|
|
49
|
+
const SENTRY_NODE_VERSION = (() => {
|
|
50
|
+
try {
|
|
51
|
+
return _require('@sentry/node/package.json').version;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return '9.47.1'; // safe fallback matching currently installed version
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// WCAG conformance helpers — formatWcagId and wcagCriteriaLabels are exported
|
|
59
|
+
// from constants.ts; embedded here so the browser bundle has the same logic.
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
const wcagConformanceScript = `
|
|
62
|
+
// Format a numeric WCAG criterion tag (mirrors formatWcagId in constants.ts).
|
|
63
|
+
// e.g. wcag143 → "WCAG 1.4.3", wcag1412 → "WCAG 1.4.12"
|
|
64
|
+
var _oobeeFormatWcagId = ${formatWcagId.toString()};
|
|
65
|
+
|
|
66
|
+
// Criteria → level map (mirrors wcagCriteriaLabels in constants.ts).
|
|
67
|
+
var _oobeeWcagCriteriaLabels = ${JSON.stringify(wcagCriteriaLabels, null, 2)};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Given an axe-core conformance array (e.g. ["wcag2a","wcag111","wcag143"]),
|
|
71
|
+
* returns the formatted criteria labels and the resolved level string —
|
|
72
|
+
* mirrors the logic used in ruleOffcanvas.ejs / AllIssues.ejs.
|
|
73
|
+
*
|
|
74
|
+
* Returns: { criteria: string[], level: string|null }
|
|
75
|
+
* criteria — e.g. ["WCAG 1.1.1", "WCAG 1.4.3"]
|
|
76
|
+
* level — e.g. "A", "AA", "AAA", or null if none found
|
|
77
|
+
*/
|
|
78
|
+
function _oobeeFormatConformance(conformance) {
|
|
79
|
+
var wcagTags = (conformance || []).filter(function(c) { return c.startsWith('wcag'); });
|
|
80
|
+
var criteria = [];
|
|
81
|
+
var level = null;
|
|
82
|
+
wcagTags.forEach(function(tag) {
|
|
83
|
+
var formatted = _oobeeFormatWcagId(tag);
|
|
84
|
+
if (_oobeeWcagCriteriaLabels[formatted]) {
|
|
85
|
+
criteria.push(formatted);
|
|
86
|
+
if (!level) level = _oobeeWcagCriteriaLabels[formatted];
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return { criteria: criteria, level: level };
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// filterAxeResults — browser-compatible (mirrors commonCrawlerFunc.ts)
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
const filterAxeResultsScript = `
|
|
96
|
+
function _oobeeTruncateHtml(html, maxBytes, suffix) {
|
|
97
|
+
maxBytes = maxBytes !== undefined ? maxBytes : 1024;
|
|
98
|
+
suffix = suffix !== undefined ? suffix : '\\u2026'; // '…'
|
|
99
|
+
var encoder = new TextEncoder();
|
|
100
|
+
if (encoder.encode(html).length <= maxBytes) return html;
|
|
101
|
+
var left = 0, right = html.length, result = '';
|
|
102
|
+
while (left <= right) {
|
|
103
|
+
var mid = Math.floor((left + right) / 2);
|
|
104
|
+
var truncated = html.slice(0, mid) + suffix;
|
|
105
|
+
var bytes = encoder.encode(truncated).length;
|
|
106
|
+
if (bytes <= maxBytes) { result = truncated; left = mid + 1; }
|
|
107
|
+
else { right = mid - 1; }
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _oobeeFilterAxeResults(axeResults, pageTitle) {
|
|
113
|
+
var violations = axeResults.violations || [];
|
|
114
|
+
var passes = axeResults.passes || [];
|
|
115
|
+
var incomplete = axeResults.incomplete || [];
|
|
116
|
+
var url = axeResults.url || (typeof window !== 'undefined' ? window.location.href : '');
|
|
117
|
+
|
|
118
|
+
var totalItems = 0;
|
|
119
|
+
var mustFix = { totalItems: 0, rules: {} };
|
|
120
|
+
var goodToFix = { totalItems: 0, rules: {} };
|
|
121
|
+
var needsReview = { totalItems: 0, rules: {} };
|
|
122
|
+
var passed = { totalItems: 0, rules: {} };
|
|
123
|
+
|
|
124
|
+
var wcagLevelRegex = /^wcag\\d+a+$/;
|
|
125
|
+
|
|
126
|
+
function processItem(item, displayNeedsReview) {
|
|
127
|
+
var rule = item.id;
|
|
128
|
+
var description = item.help;
|
|
129
|
+
var helpUrl = item.helpUrl;
|
|
130
|
+
var tags = item.tags || [];
|
|
131
|
+
var nodes = item.nodes || [];
|
|
132
|
+
|
|
133
|
+
if (rule === 'frame-tested') return;
|
|
134
|
+
|
|
135
|
+
var conformance = tags.filter(function(t) {
|
|
136
|
+
return t.startsWith('wcag') || t === 'best-practice';
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (conformance[0] !== 'best-practice' && !wcagLevelRegex.test(conformance[0])) {
|
|
140
|
+
conformance.sort(function(a, b) {
|
|
141
|
+
if (wcagLevelRegex.test(a) && !wcagLevelRegex.test(b)) return -1;
|
|
142
|
+
if (!wcagLevelRegex.test(a) && wcagLevelRegex.test(b)) return 1;
|
|
143
|
+
return 0;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
var hasWcagA = conformance.some(function(t) { return /^wcag\\d*a$/.test(t); });
|
|
148
|
+
var hasWcagAA = conformance.some(function(t) { return /^wcag\\d*aa$/.test(t); });
|
|
149
|
+
|
|
150
|
+
var category = displayNeedsReview ? needsReview
|
|
151
|
+
: (hasWcagA || hasWcagAA) ? mustFix
|
|
152
|
+
: goodToFix;
|
|
153
|
+
|
|
154
|
+
nodes.forEach(function(node) {
|
|
155
|
+
var html = node.html || '';
|
|
156
|
+
var failureSummary = node.failureSummary || '';
|
|
157
|
+
var target = node.target || [];
|
|
158
|
+
var axeImpact = node.impact;
|
|
159
|
+
|
|
160
|
+
if (!(rule in category.rules)) {
|
|
161
|
+
category.rules[rule] = {
|
|
162
|
+
rule: rule, description: description, axeImpact: axeImpact,
|
|
163
|
+
helpUrl: helpUrl, conformance: conformance, totalItems: 0, items: [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
var message = displayNeedsReview
|
|
168
|
+
? failureSummary.slice(failureSummary.indexOf('\\n') + 1).trim()
|
|
169
|
+
: failureSummary;
|
|
170
|
+
|
|
171
|
+
var finalHtml = html;
|
|
172
|
+
if (html.includes('<\\/script>')) {
|
|
173
|
+
finalHtml = html.replaceAll('<\\/script>', '</script>');
|
|
174
|
+
}
|
|
175
|
+
finalHtml = _oobeeTruncateHtml(finalHtml);
|
|
176
|
+
|
|
177
|
+
var xpath = (target.length === 1 && typeof target[0] === 'string') ? target[0] : undefined;
|
|
178
|
+
|
|
179
|
+
category.rules[rule].items.push({
|
|
180
|
+
html: finalHtml, message: message, xpath: xpath,
|
|
181
|
+
displayNeedsReview: displayNeedsReview || undefined,
|
|
182
|
+
});
|
|
183
|
+
category.rules[rule].totalItems += 1;
|
|
184
|
+
category.totalItems += 1;
|
|
185
|
+
totalItems += 1;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
violations.forEach(function(item) { processItem(item, false); });
|
|
190
|
+
incomplete.forEach(function(item) { processItem(item, true); });
|
|
191
|
+
|
|
192
|
+
passes.forEach(function(item) {
|
|
193
|
+
var rule = item.id;
|
|
194
|
+
var description = item.help;
|
|
195
|
+
var axeImpact = item.impact;
|
|
196
|
+
var helpUrl = item.helpUrl;
|
|
197
|
+
var tags = item.tags || [];
|
|
198
|
+
var nodes = item.nodes || [];
|
|
199
|
+
|
|
200
|
+
if (rule === 'frame-tested') return;
|
|
201
|
+
|
|
202
|
+
var conformance = tags.filter(function(t) {
|
|
203
|
+
return t.startsWith('wcag') || t === 'best-practice';
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
nodes.forEach(function(node) {
|
|
207
|
+
if (!(rule in passed.rules)) {
|
|
208
|
+
passed.rules[rule] = {
|
|
209
|
+
rule: rule, description: description, axeImpact: axeImpact,
|
|
210
|
+
helpUrl: helpUrl, conformance: conformance, totalItems: 0, items: [],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
var passedXpath = (node.target && node.target.length === 1 && typeof node.target[0] === 'string')
|
|
214
|
+
? node.target[0] : undefined;
|
|
215
|
+
passed.rules[rule].items.push({
|
|
216
|
+
html: _oobeeTruncateHtml(node.html || ''), screenshotPath: '',
|
|
217
|
+
message: '', xpath: passedXpath,
|
|
218
|
+
});
|
|
219
|
+
passed.totalItems += 1;
|
|
220
|
+
passed.rules[rule].totalItems += 1;
|
|
221
|
+
totalItems += 1;
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
url: url, pageTitle: pageTitle, totalItems: totalItems,
|
|
227
|
+
mustFix: mustFix, goodToFix: goodToFix, needsReview: needsReview, passed: passed,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
`;
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Sentry telemetry — uses the official Sentry JavaScript browser SDK loaded
|
|
233
|
+
// from CDN (same major version as the installed @sentry/node build).
|
|
234
|
+
// DSN and app version are baked in at generation time.
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
const sentryTelemetryScript = (dsn, appVersion, sentryVersion) => `
|
|
237
|
+
var _oobeeSentryDsn = ${JSON.stringify(dsn)};
|
|
238
|
+
var _oobeeAppVersion = ${JSON.stringify(appVersion)};
|
|
239
|
+
var _oobeeSentryVersion = ${JSON.stringify(sentryVersion)};
|
|
240
|
+
var _oobeeSentryInitialized = false;
|
|
241
|
+
var _oobeeSentryLoadPromise = null;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Lazily load the Sentry JavaScript browser SDK from CDN and return the
|
|
245
|
+
* global Sentry object. Subsequent calls reuse the same promise.
|
|
246
|
+
*/
|
|
247
|
+
function _oobeeLoadSentry() {
|
|
248
|
+
if (_oobeeSentryLoadPromise) return _oobeeSentryLoadPromise;
|
|
249
|
+
|
|
250
|
+
_oobeeSentryLoadPromise = new Promise(function(resolve, reject) {
|
|
251
|
+
// Already present (e.g. host page loaded Sentry itself)
|
|
252
|
+
if (window.Sentry && typeof window.Sentry.init === 'function') {
|
|
253
|
+
resolve(window.Sentry);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
var script = document.createElement('script');
|
|
257
|
+
script.src = 'https://browser.sentry-cdn.com/' + _oobeeSentryVersion + '/bundle.min.js';
|
|
258
|
+
script.crossOrigin = 'anonymous';
|
|
259
|
+
script.onload = function() {
|
|
260
|
+
if (window.Sentry && typeof window.Sentry.init === 'function') {
|
|
261
|
+
resolve(window.Sentry);
|
|
262
|
+
} else {
|
|
263
|
+
reject(new Error('[oobee] Sentry SDK loaded but window.Sentry not found'));
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
script.onerror = function() {
|
|
267
|
+
reject(new Error('[oobee] Failed to load Sentry browser SDK from CDN'));
|
|
268
|
+
};
|
|
269
|
+
document.head.appendChild(script);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return _oobeeSentryLoadPromise;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Build WCAG occurrence map and per-criterion level map from scan results.
|
|
277
|
+
* Mirrors the logic in npmIndex.ts processAndSubmitResults.
|
|
278
|
+
*/
|
|
279
|
+
function _oobeeBuildWcagData(results) {
|
|
280
|
+
var wcagOccurrencesMap = {}; // { wcag111: 3, wcag412: 1, ... }
|
|
281
|
+
var criterionLevel = {}; // { wcag111: 'a', wcag143: 'aa', ... }
|
|
282
|
+
var criterionRegex = /^wcag[0-9]{3,4}$/;
|
|
283
|
+
|
|
284
|
+
['mustFix', 'goodToFix', 'needsReview'].forEach(function(cat) {
|
|
285
|
+
var catData = results[cat];
|
|
286
|
+
if (!catData || !catData.rules) return;
|
|
287
|
+
|
|
288
|
+
Object.values(catData.rules).forEach(function(rule) {
|
|
289
|
+
if (!rule.conformance) return;
|
|
290
|
+
|
|
291
|
+
// Derive level from conformance level-tags (wcag2a / wcag2aa / wcag2aaa)
|
|
292
|
+
var level = '';
|
|
293
|
+
var criteria = [];
|
|
294
|
+
rule.conformance.forEach(function(c) {
|
|
295
|
+
if (/^wcag\\d+aaa$/.test(c)) { if (!level) level = 'aaa'; }
|
|
296
|
+
else if (/^wcag\\d+aa$/.test(c)) { if (!level) level = 'aa'; }
|
|
297
|
+
else if (/^wcag\\d+a$/.test(c)) { if (!level) level = 'a'; }
|
|
298
|
+
else if (criterionRegex.test(c)) { criteria.push(c); }
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
criteria.forEach(function(c) {
|
|
302
|
+
// Keep the most severe level seen for a criterion
|
|
303
|
+
var existing = criterionLevel[c];
|
|
304
|
+
if (!existing || level === 'aaa' ||
|
|
305
|
+
(level === 'aa' && existing === 'a') ||
|
|
306
|
+
(level === 'a' && !existing)) {
|
|
307
|
+
criterionLevel[c] = level;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Only count violations for the occurrence map (mustFix + goodToFix)
|
|
312
|
+
if (cat === 'mustFix' || cat === 'goodToFix') {
|
|
313
|
+
criteria.forEach(function(c) {
|
|
314
|
+
wcagOccurrencesMap[c] = (wcagOccurrencesMap[c] || 0) + rule.totalItems;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return { wcagOccurrencesMap: wcagOccurrencesMap, criterionLevel: criterionLevel };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Send an "Accessibility Scan Page" event to Sentry using the official
|
|
325
|
+
* Sentry JavaScript browser SDK API:
|
|
326
|
+
* Sentry.init / Sentry.setUser / Sentry.captureEvent / Sentry.flush
|
|
327
|
+
*
|
|
328
|
+
* @param {object} results - Full oobee scan result from window.oobee.scan()
|
|
329
|
+
* @param {object} userInfo - { email, name } provided by the implementer
|
|
330
|
+
*/
|
|
331
|
+
async function _oobeeSendSentryTelemetry(results, userInfo) {
|
|
332
|
+
if (!_oobeeSentryDsn) return;
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
var Sentry = await _oobeeLoadSentry();
|
|
336
|
+
|
|
337
|
+
// Initialise once per page load
|
|
338
|
+
if (!_oobeeSentryInitialized) {
|
|
339
|
+
Sentry.init({
|
|
340
|
+
dsn: _oobeeSentryDsn,
|
|
341
|
+
tracesSampleRate: 1.0,
|
|
342
|
+
});
|
|
343
|
+
_oobeeSentryInitialized = true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── User context ────────────────────────────────────────────────────
|
|
347
|
+
Sentry.setUser({
|
|
348
|
+
email: (userInfo && userInfo.email) || undefined,
|
|
349
|
+
username: (userInfo && userInfo.name) || undefined,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ── WCAG breakdown tags ─────────────────────────────────────────────
|
|
353
|
+
var wcagData = _oobeeBuildWcagData(results);
|
|
354
|
+
var wcagOccurrencesMap = wcagData.wcagOccurrencesMap;
|
|
355
|
+
var criterionLevel = wcagData.criterionLevel;
|
|
356
|
+
|
|
357
|
+
var tags = {};
|
|
358
|
+
var wcagCriteriaBreakdown = {};
|
|
359
|
+
|
|
360
|
+
// Format: wcag111a_Occurrences (mirrors sentryTelemetry.ts formatWcagTag)
|
|
361
|
+
Object.keys(wcagOccurrencesMap).forEach(function(wcagId) {
|
|
362
|
+
var level = criterionLevel[wcagId] || '';
|
|
363
|
+
var formattedTag = wcagId + level + '_Occurrences';
|
|
364
|
+
tags[formattedTag] = String(wcagOccurrencesMap[wcagId]);
|
|
365
|
+
wcagCriteriaBreakdown[formattedTag] = { count: wcagOccurrencesMap[wcagId] };
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ── Category counts & occurrences ───────────────────────────────────
|
|
369
|
+
var mustFixRules = results.mustFix ? Object.keys(results.mustFix.rules) : [];
|
|
370
|
+
var goodToFixRules = results.goodToFix ? Object.keys(results.goodToFix.rules) : [];
|
|
371
|
+
var needsReviewRules = results.needsReview ? Object.keys(results.needsReview.rules) : [];
|
|
372
|
+
|
|
373
|
+
tags['version'] = _oobeeAppVersion;
|
|
374
|
+
tags['WCAG-MustFix-Count'] = String(mustFixRules.length);
|
|
375
|
+
tags['WCAG-GoodToFix-Count'] = String(goodToFixRules.length);
|
|
376
|
+
tags['WCAG-NeedsReview-Count'] = String(needsReviewRules.length);
|
|
377
|
+
tags['WCAG-MustFix-Occurrences'] = String(results.mustFix ? results.mustFix.totalItems : 0);
|
|
378
|
+
tags['WCAG-GoodToFix-Occurrences'] = String(results.goodToFix ? results.goodToFix.totalItems : 0);
|
|
379
|
+
tags['WCAG-NeedsReview-Occurrences'] = String(results.needsReview ? results.needsReview.totalItems : 0);
|
|
380
|
+
tags['Pages-Scanned-Count'] = '1';
|
|
381
|
+
|
|
382
|
+
// ── Capture event ───────────────────────────────────────────────────
|
|
383
|
+
Sentry.captureEvent({
|
|
384
|
+
message: 'Accessibility Scan Page',
|
|
385
|
+
level: 'info',
|
|
386
|
+
tags: Object.assign({}, tags, {
|
|
387
|
+
event_type: 'accessibility_scan',
|
|
388
|
+
scanType: 'browser',
|
|
389
|
+
browser: 'browser',
|
|
390
|
+
entryUrl: window.location.href,
|
|
391
|
+
}),
|
|
392
|
+
extra: {
|
|
393
|
+
wcagBreakdown: wcagCriteriaBreakdown,
|
|
394
|
+
reportCounts: {
|
|
395
|
+
mustFix: { issues: mustFixRules.length, occurrences: results.mustFix ? results.mustFix.totalItems : 0 },
|
|
396
|
+
goodToFix: { issues: goodToFixRules.length, occurrences: results.goodToFix ? results.goodToFix.totalItems : 0 },
|
|
397
|
+
needsReview: { issues: needsReviewRules.length, occurrences: results.needsReview ? results.needsReview.totalItems : 0 },
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await Sentry.flush(2000);
|
|
403
|
+
|
|
404
|
+
} catch (err) {
|
|
405
|
+
// Telemetry failures must never break the caller
|
|
406
|
+
console.error('[oobee-client-scanner] Sentry telemetry error:', err);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
`;
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// scan API — enriches results and fires Sentry telemetry
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
const scanApiScript = (shortDescMap, longDescMap, stepByStepMap) => `
|
|
414
|
+
var _oobeeShortDescMap = ${JSON.stringify(shortDescMap)};
|
|
415
|
+
var _oobeeLongDescMap = ${JSON.stringify(longDescMap)};
|
|
416
|
+
var _oobeeStepByStepGuide = ${JSON.stringify(stepByStepMap)};
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* window.oobee.scan(options?) — scan the current page for accessibility issues.
|
|
420
|
+
*
|
|
421
|
+
* @param {object} [options]
|
|
422
|
+
* @param {boolean} [options.disableOobee=false] Disable oobee custom checks.
|
|
423
|
+
* @param {boolean} [options.enableWcagAaa=false] Include WCAG 2 AAA rules.
|
|
424
|
+
* @param {Array} [options.elementsToScan=[]] CSS selectors / DOM nodes to
|
|
425
|
+
* scope the scan; [] = full page.
|
|
426
|
+
* @param {object} [options.userInfo] Implementer-supplied identity.
|
|
427
|
+
* @param {string} [options.userInfo.email] User e-mail for Sentry telemetry.
|
|
428
|
+
* @param {string} [options.userInfo.name] User name for Sentry telemetry.
|
|
429
|
+
*
|
|
430
|
+
* @returns {Promise<object>} Oobee scan result (same shape as npmIndex JSON output).
|
|
431
|
+
*/
|
|
432
|
+
window.oobee = {
|
|
433
|
+
scan: async function(options) {
|
|
434
|
+
var opts = options || {};
|
|
435
|
+
var disableOobee = opts.disableOobee !== undefined ? !!opts.disableOobee : false;
|
|
436
|
+
var enableWcagAaa = opts.enableWcagAaa !== undefined ? !!opts.enableWcagAaa : false;
|
|
437
|
+
var elementsToScan = opts.elementsToScan || [];
|
|
438
|
+
var userInfo = opts.userInfo || {};
|
|
439
|
+
|
|
440
|
+
// Update window globals read by runA11yScan
|
|
441
|
+
window.disableOobee = disableOobee;
|
|
442
|
+
window.enableWcagAaa = enableWcagAaa;
|
|
443
|
+
|
|
444
|
+
// Run axe-core + oobee custom checks
|
|
445
|
+
var scanResult = await window.runA11yScan(elementsToScan, '');
|
|
446
|
+
|
|
447
|
+
// Convert raw axe results into oobee category structure
|
|
448
|
+
var filtered = _oobeeFilterAxeResults(scanResult.axeScanResults, scanResult.pageTitle);
|
|
449
|
+
|
|
450
|
+
// Enrich rules with oobee knowledge-base descriptions
|
|
451
|
+
['mustFix', 'goodToFix', 'needsReview'].forEach(function(category) {
|
|
452
|
+
var cat = filtered[category];
|
|
453
|
+
if (!cat || !cat.rules) return;
|
|
454
|
+
Object.keys(cat.rules).forEach(function(ruleId) {
|
|
455
|
+
var rule = cat.rules[ruleId];
|
|
456
|
+
rule.shortDescription = _oobeeShortDescMap[ruleId];
|
|
457
|
+
rule.longDescription = _oobeeLongDescMap[ruleId];
|
|
458
|
+
rule.stepByStepGuide = _oobeeStepByStepGuide[ruleId];
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Fire-and-forget Sentry telemetry (errors are caught internally)
|
|
463
|
+
_oobeeSendSentryTelemetry(filtered, userInfo);
|
|
464
|
+
|
|
465
|
+
return filtered;
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Format a raw conformance tag array into criteria labels + level.
|
|
470
|
+
* Mirrors ruleOffcanvas.ejs / AllIssues.ejs logic (single source of truth
|
|
471
|
+
* via formatWcagId + wcagCriteriaLabels exported from constants.ts).
|
|
472
|
+
*
|
|
473
|
+
* @param {string[]} conformance e.g. ["wcag2a","wcag111","wcag143"]
|
|
474
|
+
* @returns {{ criteria: string[], level: string|null }}
|
|
475
|
+
* e.g. { criteria: ["WCAG 1.1.1","WCAG 1.4.3"], level: "A" }
|
|
476
|
+
*/
|
|
477
|
+
formatConformance: _oobeeFormatConformance,
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Scroll the element matching the given CSS selector into view and briefly
|
|
481
|
+
* highlight it with an outline flash. The selector comes from item.xpath
|
|
482
|
+
* in the scan results (axe-core stores CSS selectors there).
|
|
483
|
+
*
|
|
484
|
+
* @param {string} selector CSS selector, e.g. "button:nth-child(2)"
|
|
485
|
+
*/
|
|
486
|
+
scrollToElement: function(selector) {
|
|
487
|
+
if (!selector) return;
|
|
488
|
+
var el;
|
|
489
|
+
try { el = document.querySelector(selector); } catch (e) { return; }
|
|
490
|
+
if (!el) return;
|
|
491
|
+
|
|
492
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
493
|
+
|
|
494
|
+
// Brief outline flash so the element is easy to spot
|
|
495
|
+
var prev = el.style.outline;
|
|
496
|
+
el.style.outline = '3px solid #fd7e14';
|
|
497
|
+
setTimeout(function() { el.style.outline = prev; }, 1800);
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
console.log(
|
|
502
|
+
'[oobee-client-scanner] Ready. Call window.oobee.scan() to scan this page.'
|
|
503
|
+
);
|
|
504
|
+
`;
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Assemble the full client bundle
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
function generateClientBundle() {
|
|
509
|
+
const axeSource = axe.source;
|
|
510
|
+
const oobeeFunctions = getOobeeFunctionsScript(false, false);
|
|
511
|
+
return `/**
|
|
512
|
+
* oobee-client-scanner.js — auto-generated by generateOobeeClientScanner.ts
|
|
513
|
+
* DO NOT EDIT MANUALLY. Re-generate with: node dist/generateOobeeClientScanner.js
|
|
514
|
+
*
|
|
515
|
+
* Embedded at generation time:
|
|
516
|
+
* App version : ${APP_VERSION}
|
|
517
|
+
* Sentry DSN : (from OOBEE_SENTRY_DSN env var or constants.ts default)
|
|
518
|
+
* Sentry SDK : @sentry/browser ${SENTRY_NODE_VERSION} (loaded from CDN at runtime)
|
|
519
|
+
*
|
|
520
|
+
* Usage:
|
|
521
|
+
* <script src="oobee-client-scanner.js"></script>
|
|
522
|
+
* <script>
|
|
523
|
+
* window.oobee.scan({
|
|
524
|
+
* userInfo: { email: 'you@example.com', name: 'Your Name' },
|
|
525
|
+
* // scanMode: [string] choices: "default" | "disable-oobee" | "enable-wcag-aaa" | "disable-oobee,enable-wcag-aaa"
|
|
526
|
+
* // "default" — axe-core + oobee custom checks, WCAG A/AA only
|
|
527
|
+
* // "disable-oobee" — axe-core only, no oobee custom checks
|
|
528
|
+
* // "enable-wcag-aaa" — axe-core + oobee + WCAG AAA rules
|
|
529
|
+
* // "disable-oobee,enable-wcag-aaa" — axe-core + WCAG AAA, no oobee checks
|
|
530
|
+
* disableOobee: false, // true → same as "disable-oobee"
|
|
531
|
+
* enableWcagAaa: true, // true → same as "enable-wcag-aaa"
|
|
532
|
+
* elementsToScan: [], // [] = full page; or pass CSS selectors / DOM nodes
|
|
533
|
+
* }).then(results => console.log(JSON.stringify(results, null, 2)));
|
|
534
|
+
* </script>
|
|
535
|
+
*/
|
|
536
|
+
(function () {
|
|
537
|
+
'use strict';
|
|
538
|
+
|
|
539
|
+
// ── axe-core ──────────────────────────────────────────────────────────────
|
|
540
|
+
${axeSource}
|
|
541
|
+
|
|
542
|
+
// ── Oobee helper functions + getAxeConfiguration + runA11yScan ───────────
|
|
543
|
+
${oobeeFunctions}
|
|
544
|
+
|
|
545
|
+
// ── filterAxeResults (browser-compatible) ─────────────────────────────────
|
|
546
|
+
${filterAxeResultsScript}
|
|
547
|
+
|
|
548
|
+
// ── WCAG conformance helpers (formatWcagId + wcagCriteriaLabels from constants.ts) ──
|
|
549
|
+
${wcagConformanceScript}
|
|
550
|
+
|
|
551
|
+
// ── Sentry browser telemetry (Sentry JS SDK, loaded from CDN) ────────────
|
|
552
|
+
${sentryTelemetryScript(SENTRY_DSN, APP_VERSION, SENTRY_NODE_VERSION)}
|
|
553
|
+
|
|
554
|
+
// ── Description maps + window.oobee API ───────────────────────────────────
|
|
555
|
+
${scanApiScript(a11yRuleShortDescriptionMap, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide)}
|
|
556
|
+
})();
|
|
557
|
+
`;
|
|
558
|
+
}
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// Write output file
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
const outputArg = process.argv[2];
|
|
563
|
+
const outputPath = outputArg
|
|
564
|
+
? path.resolve(outputArg)
|
|
565
|
+
: path.resolve(process.cwd(), 'oobee-client-scanner.js');
|
|
566
|
+
writeFileSync(outputPath, generateClientBundle(), 'utf-8');
|
|
567
|
+
console.log(`Generated: ${outputPath}`);
|
|
568
|
+
console.log(` App version : ${APP_VERSION}`);
|
|
569
|
+
console.log(` Sentry DSN : ${SENTRY_DSN.slice(0, 40)}…`);
|
|
570
|
+
console.log(` Sentry SDK : @sentry/browser ${SENTRY_NODE_VERSION} (CDN)`);
|
package/dist/mergeAxeResults.js
CHANGED
|
@@ -5,7 +5,7 @@ import printMessage from 'print-message';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import ejs from 'ejs';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import constants, { BrowserTypes, ScannerTypes, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, } from './constants/constants.js';
|
|
8
|
+
import constants, { BrowserTypes, ScannerTypes, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide, } from './constants/constants.js';
|
|
9
9
|
import { getBrowserToRun, getPlaywrightLaunchOptions } from './constants/common.js';
|
|
10
10
|
import { createScreenshotsFolder, getStoragePath, getVersion, getWcagPassPercentage, getProgressPercentage, retryFunction, zipResults, getIssuesPercentage, register, } from './utils.js';
|
|
11
11
|
import { consoleLogger } from './logs.js';
|
|
@@ -302,9 +302,9 @@ const wcagOccurrencesMap = new Map();
|
|
|
302
302
|
const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
303
303
|
const { url, pageTitle, filePath } = pageResults;
|
|
304
304
|
const totalIssuesInPage = new Set();
|
|
305
|
-
Object.keys(pageResults.mustFix
|
|
306
|
-
Object.keys(pageResults.goodToFix
|
|
307
|
-
Object.keys(pageResults.needsReview
|
|
305
|
+
Object.keys(pageResults.mustFix?.rules ?? {}).forEach(k => totalIssuesInPage.add(k));
|
|
306
|
+
Object.keys(pageResults.goodToFix?.rules ?? {}).forEach(k => totalIssuesInPage.add(k));
|
|
307
|
+
Object.keys(pageResults.needsReview?.rules ?? {}).forEach(k => totalIssuesInPage.add(k));
|
|
308
308
|
allIssues.topFiveMostIssues.push({
|
|
309
309
|
url,
|
|
310
310
|
pageTitle,
|
|
@@ -592,6 +592,7 @@ generateJsonFiles = false) => {
|
|
|
592
592
|
a11yRuleShortDescriptionMap,
|
|
593
593
|
disabilityBadgesMap,
|
|
594
594
|
a11yRuleLongDescriptionMap,
|
|
595
|
+
a11yRuleStepByStepGuide,
|
|
595
596
|
wcagCriteriaLabels: constants.wcagCriteriaLabels,
|
|
596
597
|
scanPagesDetail: {
|
|
597
598
|
pagesAffected: [],
|
package/dist/npmIndex.js
CHANGED
|
@@ -4,7 +4,7 @@ import axe from 'axe-core';
|
|
|
4
4
|
import { JSDOM } from 'jsdom';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { EnqueueStrategy } from 'crawlee';
|
|
7
|
-
import constants, { BrowserTypes, RuleFlags, ScannerTypes } from './constants/constants.js';
|
|
7
|
+
import constants, { BrowserTypes, RuleFlags, ScannerTypes, a11yRuleShortDescriptionMap, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide } from './constants/constants.js';
|
|
8
8
|
import { deleteClonedProfiles, getBrowserToRun, getPlaywrightLaunchOptions, submitForm, } from './constants/common.js';
|
|
9
9
|
import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
|
10
10
|
import { createAndUpdateResultsFolders, getVersion } from './utils.js';
|
|
@@ -489,6 +489,10 @@ const processAndSubmitResults = async (scanData, name, email, metadata) => {
|
|
|
489
489
|
if (constants.a11yRuleShortDescriptionMap[ruleId]) {
|
|
490
490
|
mergedResults[category].rules[ruleId].description = constants.a11yRuleShortDescriptionMap[ruleId];
|
|
491
491
|
}
|
|
492
|
+
// Add short description, long description and step-by-step guide
|
|
493
|
+
mergedResults[category].rules[ruleId].shortDescription = a11yRuleShortDescriptionMap[ruleId];
|
|
494
|
+
mergedResults[category].rules[ruleId].longDescription = a11yRuleLongDescriptionMap[ruleId];
|
|
495
|
+
mergedResults[category].rules[ruleId].stepByStepGuide = a11yRuleStepByStepGuide[ruleId];
|
|
492
496
|
// Add url to items
|
|
493
497
|
mergedResults[category].rules[ruleId].items.forEach((item) => {
|
|
494
498
|
item.url = result.url;
|
|
@@ -554,6 +558,10 @@ const processAndSubmitResults = async (scanData, name, email, metadata) => {
|
|
|
554
558
|
if (constants.a11yRuleShortDescriptionMap[rule.rule]) {
|
|
555
559
|
rule.description = constants.a11yRuleShortDescriptionMap[rule.rule];
|
|
556
560
|
}
|
|
561
|
+
// Add short description, long description and step-by-step guide
|
|
562
|
+
rule.shortDescription = a11yRuleShortDescriptionMap[rule.rule];
|
|
563
|
+
rule.longDescription = a11yRuleLongDescriptionMap[rule.rule];
|
|
564
|
+
rule.stepByStepGuide = a11yRuleStepByStepGuide[rule.rule];
|
|
557
565
|
if (rule.items) {
|
|
558
566
|
rule.items.forEach((item) => {
|
|
559
567
|
// Ensure item URL matches the result URL
|
|
@@ -637,4 +645,4 @@ export const scanPage = async (pages, config) => {
|
|
|
637
645
|
}
|
|
638
646
|
return processAndSubmitResults(scanData, name, email, metadata);
|
|
639
647
|
};
|
|
640
|
-
export { RuleFlags };
|
|
648
|
+
export { RuleFlags, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide, getOobeeFunctionsScript };
|