@govtechsg/oobee 0.10.77 → 0.10.78-alpha1
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/INTEGRATION.md +43 -0
- package/dist/mergeAxeResults.js +39 -22
- package/dist/npmIndex.js +272 -61
- package/examples/oobee-scan-html-demo.js +51 -0
- package/examples/oobee-scan-page-demo.js +40 -0
- package/package.json +1 -1
- package/src/mergeAxeResults.ts +43 -22
- package/src/npmIndex.ts +458 -107
package/INTEGRATION.md
CHANGED
|
@@ -426,3 +426,46 @@ You will see Oobee results generated in <code>results</code> folder.
|
|
|
426
426
|
runScript();
|
|
427
427
|
|
|
428
428
|
</details>
|
|
429
|
+
|
|
430
|
+
#### Integration with any NodeJS workflow (Beta)
|
|
431
|
+
|
|
432
|
+
You can also use Oobee in any NodeJS script without needing a specific framework integration pattern. This is useful for custom workflows, CI/CD pipelines, or simple scripts where you manage the browser automation yourself or want to scan static HTML.
|
|
433
|
+
|
|
434
|
+
Refer to the examples in `examples/oobee-scan-page-demo.js` and `examples/oobee-scan-html-demo.js`.
|
|
435
|
+
|
|
436
|
+
##### `async scanPage(pages, config)`
|
|
437
|
+
|
|
438
|
+
Scans one or more Playwright Page objects. This injects the Oobee engine into the page context to perform the audit.
|
|
439
|
+
|
|
440
|
+
**Parameters:**
|
|
441
|
+
|
|
442
|
+
- `pages`: A Playwright `Page` object or an array of `Page` objects.
|
|
443
|
+
- `config`: Configuration object
|
|
444
|
+
- `name`: Name for results (required)
|
|
445
|
+
- `email`: Email for results (required)
|
|
446
|
+
- `pageTitle`: Optional override for page title (only applied if scanning a single page)
|
|
447
|
+
- `metadata`: Optional metadata string
|
|
448
|
+
- `ruleset`: Optional array of `RuleFlags` (e.g. `['enable-wcag-aaa', 'disable-oobee']`)
|
|
449
|
+
|
|
450
|
+
**Returns:**
|
|
451
|
+
|
|
452
|
+
- Scan results object containing categorized violations and pass counts.
|
|
453
|
+
|
|
454
|
+
##### `async scanHTML(htmlContent, config)`
|
|
455
|
+
|
|
456
|
+
Scans raw HTML string(s). Note that this runs in a JSDOM environment (NodeJS) using axe-core and may not catch accessibility issues that require a full browser layout or JavaScript execution to render.
|
|
457
|
+
|
|
458
|
+
**Parameters:**
|
|
459
|
+
|
|
460
|
+
- `htmlContent`: HTML string or array of HTML strings.
|
|
461
|
+
- `config`: Configuration object
|
|
462
|
+
- `name`: Name for results (required)
|
|
463
|
+
- `email`: Email for results (required)
|
|
464
|
+
- `pageUrl`: Optional URL identifier for the report (defaults to 'raw-html')
|
|
465
|
+
- `pageTitle`: Optional title identifier (defaults to 'HTML Content')
|
|
466
|
+
- `metadata`: Optional metadata string
|
|
467
|
+
- `ruleset`: Optional array of `RuleFlags`
|
|
468
|
+
|
|
469
|
+
**Returns:**
|
|
470
|
+
|
|
471
|
+
- Scan results object containing categorized violations and pass counts.
|
package/dist/mergeAxeResults.js
CHANGED
|
@@ -8,7 +8,6 @@ import { AsyncParser } from '@json2csv/node';
|
|
|
8
8
|
import zlib from 'zlib';
|
|
9
9
|
import { Base64Encode } from 'base64-stream';
|
|
10
10
|
import { pipeline } from 'stream/promises';
|
|
11
|
-
// @ts-ignore
|
|
12
11
|
import * as Sentry from '@sentry/node';
|
|
13
12
|
import constants, { BrowserTypes, ScannerTypes, sentryConfig, setSentryUser, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, } from './constants/constants.js';
|
|
14
13
|
import { getBrowserToRun, getPlaywrightLaunchOptions } from './constants/common.js';
|
|
@@ -904,31 +903,49 @@ function updateIssuesWithOccurrences(issuesList, urlOccurrencesMap) {
|
|
|
904
903
|
issue.totalOccurrences = urlOccurrencesMap.get(issue.url) || 0;
|
|
905
904
|
});
|
|
906
905
|
}
|
|
907
|
-
const
|
|
906
|
+
const extractRuleAiData = (ruleId, totalItems, items, callback) => {
|
|
907
|
+
let snippets = [];
|
|
908
|
+
if (oobeeAiRules.includes(ruleId)) {
|
|
909
|
+
const snippetsSet = new Set();
|
|
910
|
+
if (items) {
|
|
911
|
+
items.forEach(item => {
|
|
912
|
+
snippetsSet.add(oobeeAiHtmlETL(item.html));
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
snippets = [...snippetsSet];
|
|
916
|
+
if (callback)
|
|
917
|
+
callback();
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
snippets,
|
|
921
|
+
occurrences: totalItems,
|
|
922
|
+
};
|
|
923
|
+
};
|
|
924
|
+
// This is for telemetry purposes called within mergeAxeResults.ts
|
|
925
|
+
export const createRuleIdJson = allIssues => {
|
|
908
926
|
const compiledRuleJson = {};
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
page.items.forEach(htmlItem => {
|
|
916
|
-
snippetsSet.add(oobeeAiHtmlETL(htmlItem.html));
|
|
927
|
+
['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
|
|
928
|
+
allIssues.items[category].rules.forEach(rule => {
|
|
929
|
+
const allItems = rule.pagesAffected.flatMap(page => page.items || []);
|
|
930
|
+
compiledRuleJson[rule.rule] = extractRuleAiData(rule.rule, rule.totalItems, allItems, () => {
|
|
931
|
+
rule.pagesAffected.forEach(p => {
|
|
932
|
+
delete p.items;
|
|
917
933
|
});
|
|
918
934
|
});
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
return compiledRuleJson;
|
|
938
|
+
};
|
|
939
|
+
// This is for telemetry purposes called from npmIndex (scanPage and scanHTML) where report is not generated
|
|
940
|
+
export const createBasicFormHTMLSnippet = filteredResults => {
|
|
941
|
+
const compiledRuleJson = {};
|
|
942
|
+
['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
|
|
943
|
+
if (filteredResults[category] && filteredResults[category].rules) {
|
|
944
|
+
Object.entries(filteredResults[category].rules).forEach(([ruleId, ruleVal]) => {
|
|
945
|
+
compiledRuleJson[ruleId] = extractRuleAiData(ruleId, ruleVal.totalItems, ruleVal.items);
|
|
922
946
|
});
|
|
923
947
|
}
|
|
924
|
-
|
|
925
|
-
snippets,
|
|
926
|
-
occurrences: rule.totalItems,
|
|
927
|
-
};
|
|
928
|
-
};
|
|
929
|
-
allIssues.items.mustFix.rules.forEach(ruleIterator);
|
|
930
|
-
allIssues.items.goodToFix.rules.forEach(ruleIterator);
|
|
931
|
-
allIssues.items.needsReview.rules.forEach(ruleIterator);
|
|
948
|
+
});
|
|
932
949
|
return compiledRuleJson;
|
|
933
950
|
};
|
|
934
951
|
const moveElemScreenshots = (randomToken, storagePath) => {
|
|
@@ -1143,7 +1160,7 @@ function populateScanPagesDetail(allIssues) {
|
|
|
1143
1160
|
};
|
|
1144
1161
|
}
|
|
1145
1162
|
// Send WCAG criteria breakdown to Sentry
|
|
1146
|
-
const sendWcagBreakdownToSentry = async (appVersion, wcagBreakdown, ruleIdJson, scanInfo, allIssues, pagesScannedCount = 0) => {
|
|
1163
|
+
export const sendWcagBreakdownToSentry = async (appVersion, wcagBreakdown, ruleIdJson, scanInfo, allIssues, pagesScannedCount = 0) => {
|
|
1147
1164
|
try {
|
|
1148
1165
|
// Initialize Sentry
|
|
1149
1166
|
Sentry.init(sentryConfig);
|
package/dist/npmIndex.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import printMessage from 'print-message';
|
|
3
|
+
import axe from 'axe-core';
|
|
4
|
+
import { JSDOM } from 'jsdom';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
5
6
|
import { EnqueueStrategy } from 'crawlee';
|
|
6
7
|
import constants, { BrowserTypes, RuleFlags, ScannerTypes } from './constants/constants.js';
|
|
7
8
|
import { deleteClonedProfiles, getBrowserToRun, getPlaywrightLaunchOptions, submitForm, } from './constants/common.js';
|
|
8
9
|
import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
|
9
|
-
import { createAndUpdateResultsFolders } from './utils.js';
|
|
10
|
-
import generateArtifacts from './mergeAxeResults.js';
|
|
10
|
+
import { createAndUpdateResultsFolders, getVersion } from './utils.js';
|
|
11
|
+
import generateArtifacts, { createBasicFormHTMLSnippet, sendWcagBreakdownToSentry } from './mergeAxeResults.js';
|
|
11
12
|
import { takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
|
|
12
13
|
import { consoleLogger } from './logs.js';
|
|
13
14
|
import { alertMessageOptions } from './constants/cliFunctions.js';
|
|
@@ -21,50 +22,11 @@ import { extractText } from './crawlers/custom/extractText.js';
|
|
|
21
22
|
import { gradeReadability } from './crawlers/custom/gradeReadability.js';
|
|
22
23
|
const filename = fileURLToPath(import.meta.url);
|
|
23
24
|
const dirname = path.dirname(filename);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
|
|
30
|
-
const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
|
|
31
|
-
const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
|
|
32
|
-
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
33
|
-
// max numbers of mustFix/goodToFix occurrences before test returns a fail
|
|
34
|
-
const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
|
|
35
|
-
process.env.CRAWLEE_STORAGE_DIR = randomToken;
|
|
36
|
-
const scanDetails = {
|
|
37
|
-
startTime: new Date(),
|
|
38
|
-
endTime: new Date(),
|
|
39
|
-
deviceChosen,
|
|
40
|
-
crawlType: ScannerTypes.CUSTOM,
|
|
41
|
-
requestUrl: entryUrl,
|
|
42
|
-
urlsCrawled: { ...constants.urlsCrawledObj },
|
|
43
|
-
isIncludeScreenshots: includeScreenshots,
|
|
44
|
-
isAllowSubdomains: strategy,
|
|
45
|
-
isEnableCustomChecks: ruleset,
|
|
46
|
-
isEnableWcagAaa: ruleset,
|
|
47
|
-
isSlowScanMode: specifiedMaxConcurrency,
|
|
48
|
-
isAdhereRobots: followRobots,
|
|
49
|
-
};
|
|
50
|
-
const urlsCrawled = { ...constants.urlsCrawledObj };
|
|
51
|
-
const { dataset } = await createCrawleeSubFolders(randomToken);
|
|
52
|
-
let mustFixIssues = 0;
|
|
53
|
-
let goodToFixIssues = 0;
|
|
54
|
-
let isInstanceTerminated = false;
|
|
55
|
-
const throwErrorIfTerminated = () => {
|
|
56
|
-
if (isInstanceTerminated) {
|
|
57
|
-
throw new Error('This instance of Oobee was terminated. Please start a new instance.');
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
const getAxeScript = () => {
|
|
61
|
-
throwErrorIfTerminated();
|
|
62
|
-
const axeScript = fs.readFileSync(path.join(dirname, '../../../axe-core/axe.min.js'), 'utf-8');
|
|
63
|
-
return axeScript;
|
|
64
|
-
};
|
|
65
|
-
const getOobeeFunctions = () => {
|
|
66
|
-
throwErrorIfTerminated();
|
|
67
|
-
return `
|
|
25
|
+
const getAxeScriptContent = () => {
|
|
26
|
+
return axe.source;
|
|
27
|
+
};
|
|
28
|
+
const getOobeeFunctionsScript = (disableOobee, enableWcagAaa) => {
|
|
29
|
+
return `
|
|
68
30
|
// Fix for missing __name function used by bundler
|
|
69
31
|
if (typeof __name === 'undefined') {
|
|
70
32
|
window.__name = function(fn, name) {
|
|
@@ -122,7 +84,7 @@ thresholds = { mustFix: undefined, goodToFix: undefined }, scanAboutMetadata = u
|
|
|
122
84
|
return !node.dataset.flagged; // fail any element with a data-flagged attribute set to true
|
|
123
85
|
},
|
|
124
86
|
},
|
|
125
|
-
...(enableWcagAaa
|
|
87
|
+
...((enableWcagAaa && !disableOobee)
|
|
126
88
|
? [
|
|
127
89
|
{
|
|
128
90
|
id: 'oobee-grading-text-contents',
|
|
@@ -170,19 +132,23 @@ thresholds = { mustFix: undefined, goodToFix: undefined }, scanAboutMetadata = u
|
|
|
170
132
|
helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
|
|
171
133
|
},
|
|
172
134
|
},
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
135
|
+
...((enableWcagAaa && !disableOobee)
|
|
136
|
+
? [
|
|
137
|
+
{
|
|
138
|
+
id: 'oobee-grading-text-contents',
|
|
139
|
+
selector: 'html',
|
|
140
|
+
enabled: true,
|
|
141
|
+
any: ['oobee-grading-text-contents'],
|
|
142
|
+
tags: ['wcag2aaa', 'wcag315'],
|
|
143
|
+
metadata: {
|
|
144
|
+
description:
|
|
145
|
+
'Text content should be easy to understand for individuals with education levels up to university graduates. If the text content is difficult to understand, provide supplemental content or a version that is easy to understand.',
|
|
146
|
+
help: 'Text content should be clear and plain to ensure that it is easily understood.',
|
|
147
|
+
helpUrl: 'https://www.wcag.com/uncategorized/3-1-5-reading-level/',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
]
|
|
151
|
+
: []),
|
|
186
152
|
]
|
|
187
153
|
.filter(rule => (disableOobee ? !rule.id.startsWith('oobee') : true))
|
|
188
154
|
.concat(
|
|
@@ -235,6 +201,31 @@ thresholds = { mustFix: undefined, goodToFix: undefined }, scanAboutMetadata = u
|
|
|
235
201
|
const axeScanResults = await (window).axe.run(elementsToScan, {
|
|
236
202
|
resultTypes: ['violations', 'passes', 'incomplete'],
|
|
237
203
|
});
|
|
204
|
+
|
|
205
|
+
if (axeScanResults) {
|
|
206
|
+
['violations', 'incomplete'].forEach(type => {
|
|
207
|
+
if (axeScanResults[type]) {
|
|
208
|
+
axeScanResults[type].forEach(result => {
|
|
209
|
+
if (result.nodes) {
|
|
210
|
+
result.nodes.forEach(node => {
|
|
211
|
+
['any', 'all', 'none'].forEach(key => {
|
|
212
|
+
if (node[key]) {
|
|
213
|
+
node[key].forEach(check => {
|
|
214
|
+
if (check.message && check.message.indexOf("Axe encountered an error") !== -1) {
|
|
215
|
+
if (check.data) {
|
|
216
|
+
// console.error(check.data);
|
|
217
|
+
console.error("Axe encountered an error: " + (check.data.stack || check.data.message || JSON.stringify(check.data)));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
238
229
|
|
|
239
230
|
// add custom Oobee violations
|
|
240
231
|
if (!(window).disableOobee) {
|
|
@@ -284,6 +275,50 @@ thresholds = { mustFix: undefined, goodToFix: undefined }, scanAboutMetadata = u
|
|
|
284
275
|
window.enableWcagAaa=${enableWcagAaa};
|
|
285
276
|
window.runA11yScan = runA11yScan;
|
|
286
277
|
`;
|
|
278
|
+
};
|
|
279
|
+
export const init = async ({ entryUrl, testLabel, name, email, includeScreenshots = false, viewportSettings = { width: 1000, height: 660 }, // cypress' default viewport settings
|
|
280
|
+
thresholds = { mustFix: undefined, goodToFix: undefined }, scanAboutMetadata = undefined, zip = 'oobee-scan-results', deviceChosen, strategy = EnqueueStrategy.All, ruleset = [RuleFlags.DEFAULT], specifiedMaxConcurrency = 25, followRobots = false, }) => {
|
|
281
|
+
consoleLogger.info('Starting Oobee');
|
|
282
|
+
const [date, time] = new Date().toLocaleString('sv').replaceAll(/-|:/g, '').split(' ');
|
|
283
|
+
const domain = new URL(entryUrl).hostname;
|
|
284
|
+
const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
|
|
285
|
+
const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
|
|
286
|
+
const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
|
|
287
|
+
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
288
|
+
// max numbers of mustFix/goodToFix occurrences before test returns a fail
|
|
289
|
+
const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
|
|
290
|
+
process.env.CRAWLEE_STORAGE_DIR = randomToken;
|
|
291
|
+
const scanDetails = {
|
|
292
|
+
startTime: new Date(),
|
|
293
|
+
endTime: new Date(),
|
|
294
|
+
deviceChosen,
|
|
295
|
+
crawlType: ScannerTypes.CUSTOM,
|
|
296
|
+
requestUrl: entryUrl,
|
|
297
|
+
urlsCrawled: { ...constants.urlsCrawledObj },
|
|
298
|
+
isIncludeScreenshots: includeScreenshots,
|
|
299
|
+
isAllowSubdomains: strategy,
|
|
300
|
+
isEnableCustomChecks: ruleset,
|
|
301
|
+
isEnableWcagAaa: ruleset,
|
|
302
|
+
isSlowScanMode: specifiedMaxConcurrency,
|
|
303
|
+
isAdhereRobots: followRobots,
|
|
304
|
+
};
|
|
305
|
+
const urlsCrawled = { ...constants.urlsCrawledObj };
|
|
306
|
+
const { dataset } = await createCrawleeSubFolders(randomToken);
|
|
307
|
+
let mustFixIssues = 0;
|
|
308
|
+
let goodToFixIssues = 0;
|
|
309
|
+
let isInstanceTerminated = false;
|
|
310
|
+
const throwErrorIfTerminated = () => {
|
|
311
|
+
if (isInstanceTerminated) {
|
|
312
|
+
throw new Error('This instance of Oobee was terminated. Please start a new instance.');
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const getAxeScript = () => {
|
|
316
|
+
throwErrorIfTerminated();
|
|
317
|
+
return getAxeScriptContent();
|
|
318
|
+
};
|
|
319
|
+
const getOobeeFunctions = () => {
|
|
320
|
+
throwErrorIfTerminated();
|
|
321
|
+
return getOobeeFunctionsScript(disableOobee, enableWcagAaa);
|
|
287
322
|
};
|
|
288
323
|
// Helper script for manually copy-paste testing in Chrome browser
|
|
289
324
|
/*
|
|
@@ -427,3 +462,179 @@ thresholds = { mustFix: undefined, goodToFix: undefined }, scanAboutMetadata = u
|
|
|
427
462
|
};
|
|
428
463
|
};
|
|
429
464
|
export default init;
|
|
465
|
+
const processAndSubmitResults = async (scanData, name, email, metadata) => {
|
|
466
|
+
const items = Array.isArray(scanData) ? scanData : [scanData];
|
|
467
|
+
const numberOfPagesScanned = items.length;
|
|
468
|
+
const allFilteredResults = items.map((item, index) => {
|
|
469
|
+
const filtered = filterAxeResults(item.axeScanResults, item.pageTitle, { pageIndex: index + 1, metadata });
|
|
470
|
+
filtered.url = item.pageUrl;
|
|
471
|
+
return filtered;
|
|
472
|
+
});
|
|
473
|
+
const mergedResults = {
|
|
474
|
+
mustFix: { totalItems: 0, rules: {} },
|
|
475
|
+
goodToFix: { totalItems: 0, rules: {} },
|
|
476
|
+
needsReview: { totalItems: 0, rules: {} },
|
|
477
|
+
// omitting passed from being processed to reduce payload size
|
|
478
|
+
// passed: { totalItems: 0, rules: {} },
|
|
479
|
+
};
|
|
480
|
+
allFilteredResults.forEach(result => {
|
|
481
|
+
['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
|
|
482
|
+
const categoryResult = result[category];
|
|
483
|
+
if (categoryResult) {
|
|
484
|
+
mergedResults[category].totalItems += categoryResult.totalItems;
|
|
485
|
+
Object.entries(categoryResult.rules).forEach(([ruleId, ruleVal]) => {
|
|
486
|
+
if (!mergedResults[category].rules[ruleId]) {
|
|
487
|
+
mergedResults[category].rules[ruleId] = JSON.parse(JSON.stringify(ruleVal));
|
|
488
|
+
// Map the description to the short description if available
|
|
489
|
+
if (constants.a11yRuleShortDescriptionMap[ruleId]) {
|
|
490
|
+
mergedResults[category].rules[ruleId].description = constants.a11yRuleShortDescriptionMap[ruleId];
|
|
491
|
+
}
|
|
492
|
+
// Add url to items
|
|
493
|
+
mergedResults[category].rules[ruleId].items.forEach((item) => {
|
|
494
|
+
item.url = result.url;
|
|
495
|
+
if (item.displayNeedsReview) {
|
|
496
|
+
delete item.displayNeedsReview;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
mergedResults[category].rules[ruleId].totalItems += ruleVal.totalItems;
|
|
502
|
+
const newItems = ruleVal.items.map((item) => {
|
|
503
|
+
const newItem = { ...item, url: result.url };
|
|
504
|
+
if (newItem.displayNeedsReview) {
|
|
505
|
+
delete newItem.displayNeedsReview;
|
|
506
|
+
}
|
|
507
|
+
return newItem;
|
|
508
|
+
});
|
|
509
|
+
mergedResults[category].rules[ruleId].items.push(...newItems);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
const basicFormHTMLSnippet = createBasicFormHTMLSnippet(mergedResults);
|
|
516
|
+
const entryUrl = items[0].pageUrl;
|
|
517
|
+
await submitForm(BrowserTypes.CHROMIUM, '', entryUrl, null, ScannerTypes.CUSTOM, email, name, JSON.stringify(basicFormHTMLSnippet), numberOfPagesScanned, 0, 0, '{}');
|
|
518
|
+
// Generate WCAG breakdown for Sentry
|
|
519
|
+
const wcagOccurrencesMap = new Map();
|
|
520
|
+
// Iterate through relevant categories to collect WCAG violation occurrences
|
|
521
|
+
['mustFix', 'goodToFix'].forEach(category => {
|
|
522
|
+
const rulesObj = mergedResults[category]?.rules;
|
|
523
|
+
if (rulesObj) {
|
|
524
|
+
Object.values(rulesObj).forEach((rule) => {
|
|
525
|
+
const count = rule.totalItems;
|
|
526
|
+
if (rule.conformance && Array.isArray(rule.conformance)) {
|
|
527
|
+
rule.conformance
|
|
528
|
+
.filter((c) => /wcag[0-9]{3,4}/.test(c))
|
|
529
|
+
.forEach((c) => {
|
|
530
|
+
const current = wcagOccurrencesMap.get(c) || 0;
|
|
531
|
+
wcagOccurrencesMap.set(c, current + count);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
const oobeeAppVersion = getVersion();
|
|
538
|
+
await sendWcagBreakdownToSentry(oobeeAppVersion, wcagOccurrencesMap, basicFormHTMLSnippet, {
|
|
539
|
+
entryUrl: entryUrl,
|
|
540
|
+
scanType: ScannerTypes.CUSTOM,
|
|
541
|
+
browser: 'chromium', // Defaulting since we might scan HTML without browser or implicit browser
|
|
542
|
+
email: email,
|
|
543
|
+
name: name,
|
|
544
|
+
}, undefined, numberOfPagesScanned);
|
|
545
|
+
// Return original single result if only one page was scanning to maintain backward compatibility structure
|
|
546
|
+
if (numberOfPagesScanned === 1) {
|
|
547
|
+
const singleResult = allFilteredResults[0];
|
|
548
|
+
// Clean up displayNeedsReview from single result
|
|
549
|
+
['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
|
|
550
|
+
const resultCategory = singleResult[category];
|
|
551
|
+
if (resultCategory && resultCategory.rules) {
|
|
552
|
+
Object.values(resultCategory.rules).forEach((rule) => {
|
|
553
|
+
// Map the description to the short description if available
|
|
554
|
+
if (constants.a11yRuleShortDescriptionMap[rule.rule]) {
|
|
555
|
+
rule.description = constants.a11yRuleShortDescriptionMap[rule.rule];
|
|
556
|
+
}
|
|
557
|
+
if (rule.items) {
|
|
558
|
+
rule.items.forEach((item) => {
|
|
559
|
+
// Ensure item URL matches the result URL
|
|
560
|
+
item.url = singleResult.url;
|
|
561
|
+
if (item.displayNeedsReview) {
|
|
562
|
+
delete item.displayNeedsReview;
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
return singleResult;
|
|
570
|
+
}
|
|
571
|
+
return mergedResults;
|
|
572
|
+
};
|
|
573
|
+
// This is an experimental feature to scan static HTML code without the need for Playwright browser
|
|
574
|
+
export const scanHTML = async (htmlContent, config) => {
|
|
575
|
+
const { name, email, pageUrl = 'raw-html', pageTitle = 'HTML Content', metadata = '', ruleset = [RuleFlags.DEFAULT], } = config;
|
|
576
|
+
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
577
|
+
const tags = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];
|
|
578
|
+
if (enableWcagAaa) {
|
|
579
|
+
tags.push('wcag2aaa');
|
|
580
|
+
}
|
|
581
|
+
const htmlItems = Array.isArray(htmlContent) ? htmlContent : [htmlContent];
|
|
582
|
+
const scanData = [];
|
|
583
|
+
for (let i = 0; i < htmlItems.length; i++) {
|
|
584
|
+
const htmlString = htmlItems[i];
|
|
585
|
+
const dom = new JSDOM(htmlString);
|
|
586
|
+
// Configure axe for node environment
|
|
587
|
+
// eslint-disable-next-line no-await-in-loop
|
|
588
|
+
const axeScanResults = await axe.run(dom.window.document.documentElement, {
|
|
589
|
+
runOnly: {
|
|
590
|
+
type: 'tag',
|
|
591
|
+
values: tags,
|
|
592
|
+
},
|
|
593
|
+
resultTypes: ['violations', 'passes', 'incomplete'],
|
|
594
|
+
});
|
|
595
|
+
scanData.push({
|
|
596
|
+
axeScanResults,
|
|
597
|
+
pageUrl: htmlItems.length > 1 ? `${pageUrl}-${i + 1}` : pageUrl,
|
|
598
|
+
pageTitle: htmlItems.length > 1 ? `${pageTitle} ${i + 1}` : pageTitle,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return processAndSubmitResults(scanData, name, email, metadata);
|
|
602
|
+
};
|
|
603
|
+
export const scanPage = async (pages, config) => {
|
|
604
|
+
const { name, email, pageTitle, metadata = '', ruleset = [RuleFlags.DEFAULT], } = config;
|
|
605
|
+
const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
|
|
606
|
+
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
607
|
+
const axeScript = getAxeScriptContent();
|
|
608
|
+
const oobeeFunctions = getOobeeFunctionsScript(disableOobee, enableWcagAaa);
|
|
609
|
+
const pagesArray = Array.isArray(pages) ? pages : [pages];
|
|
610
|
+
const scanData = [];
|
|
611
|
+
for (const page of pagesArray) {
|
|
612
|
+
await page.evaluate(`${axeScript}\n${oobeeFunctions}`);
|
|
613
|
+
// Run the scan inside the page
|
|
614
|
+
const consoleListener = (msg) => {
|
|
615
|
+
if (msg.type() === 'error') {
|
|
616
|
+
console.error(`[Browser Console] ${msg.text()}`);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
page.on('console', consoleListener);
|
|
620
|
+
try {
|
|
621
|
+
const scanResult = await page.evaluate(async () => {
|
|
622
|
+
return window.runA11yScan();
|
|
623
|
+
});
|
|
624
|
+
scanData.push({
|
|
625
|
+
axeScanResults: scanResult.axeScanResults,
|
|
626
|
+
pageUrl: page.url(),
|
|
627
|
+
pageTitle: await page.title(),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
page.off('console', consoleListener);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Allow override of page title if scanning a single page
|
|
635
|
+
if (!Array.isArray(pages) && pageTitle) {
|
|
636
|
+
scanData[0].pageTitle = pageTitle;
|
|
637
|
+
}
|
|
638
|
+
return processAndSubmitResults(scanData, name, email, metadata);
|
|
639
|
+
};
|
|
640
|
+
export { RuleFlags };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { scanHTML } from '../dist/npmIndex.js';
|
|
2
|
+
|
|
3
|
+
const htmlContent = `
|
|
4
|
+
<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<title>Test Page</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Accessibility Test</h1>
|
|
11
|
+
<button></button> <!-- Violation: button-name -->
|
|
12
|
+
<img src="test.jpg" /> <!-- Violation: image-alt -->
|
|
13
|
+
<div role="button">Fake</div> <!-- Violation: role-button (if interactive) -->
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const htmlContent2 = `
|
|
19
|
+
<!DOCTYPE html>
|
|
20
|
+
<html lang="en">
|
|
21
|
+
<head>
|
|
22
|
+
<title>Test Page 2</title>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<h1>Accessibility Test 2</h1>
|
|
26
|
+
<a href="#">Click me</a> <!-- Violation: link-name (if vague) or empty href issues -->
|
|
27
|
+
<input type="text" /> <!-- Violation: label -->
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
(async () => {
|
|
33
|
+
console.log("Scanning HTML string...");
|
|
34
|
+
try {
|
|
35
|
+
// Run scanHTML without needing full Oobee init
|
|
36
|
+
// Pass an array of HTML strings to demonstrate batch scanning
|
|
37
|
+
const results = await scanHTML(
|
|
38
|
+
[htmlContent, htmlContent2],
|
|
39
|
+
{
|
|
40
|
+
name: "Your Name",
|
|
41
|
+
email: "email@domain.com",
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
console.log(JSON.stringify(results, null, 2));
|
|
45
|
+
|
|
46
|
+
console.log(`\nScan Complete.`);
|
|
47
|
+
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("Error during scan:", error);
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { scanPage } from '../dist/npmIndex.js';
|
|
3
|
+
|
|
4
|
+
(async () => {
|
|
5
|
+
console.log("Launching browser...");
|
|
6
|
+
const browser = await chromium.launch({
|
|
7
|
+
headless: false,
|
|
8
|
+
channel: 'chrome' // Use Chrome instead of Chromium
|
|
9
|
+
});
|
|
10
|
+
const page = await browser.newPage();
|
|
11
|
+
|
|
12
|
+
console.log("Navigating to test page...");
|
|
13
|
+
// Using a sample page that likely has accessibility issues
|
|
14
|
+
await page.goto('https://govtechsg.github.io/purple-banner-embeds/purple-integrated-scan-example.htm');
|
|
15
|
+
|
|
16
|
+
const page2 = await browser.newPage();
|
|
17
|
+
console.log("Navigating to second test page...");
|
|
18
|
+
await page2.goto('https://a11y.tech.gov.sg');
|
|
19
|
+
|
|
20
|
+
console.log("Scanning page...");
|
|
21
|
+
try {
|
|
22
|
+
// Run scanPage using the existing Playwright page
|
|
23
|
+
const results = await scanPage(
|
|
24
|
+
[page, page2],
|
|
25
|
+
{
|
|
26
|
+
name: "Your Name",
|
|
27
|
+
email: "email@domain.com",
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
console.log(JSON.stringify(results, null, 2));
|
|
32
|
+
|
|
33
|
+
console.log(`\nScan Complete.`);
|
|
34
|
+
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("Error during scan:", error);
|
|
37
|
+
} finally {
|
|
38
|
+
await browser.close();
|
|
39
|
+
}
|
|
40
|
+
})();
|
package/package.json
CHANGED
package/src/mergeAxeResults.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { AsyncParser, ParserOptions } from '@json2csv/node';
|
|
|
12
12
|
import zlib from 'zlib';
|
|
13
13
|
import { Base64Encode } from 'base64-stream';
|
|
14
14
|
import { pipeline } from 'stream/promises';
|
|
15
|
-
// @ts-ignore
|
|
16
15
|
import * as Sentry from '@sentry/node';
|
|
17
16
|
import constants, {
|
|
18
17
|
BrowserTypes,
|
|
@@ -1289,34 +1288,56 @@ function updateIssuesWithOccurrences(issuesList: any[], urlOccurrencesMap: Map<s
|
|
|
1289
1288
|
});
|
|
1290
1289
|
}
|
|
1291
1290
|
|
|
1291
|
+
const extractRuleAiData = (ruleId: string, totalItems: number, items: any[], callback?: () => void) => {
|
|
1292
|
+
let snippets = [];
|
|
1293
|
+
|
|
1294
|
+
if (oobeeAiRules.includes(ruleId)) {
|
|
1295
|
+
const snippetsSet = new Set();
|
|
1296
|
+
if (items) {
|
|
1297
|
+
items.forEach(item => {
|
|
1298
|
+
snippetsSet.add(oobeeAiHtmlETL(item.html));
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
snippets = [...snippetsSet];
|
|
1302
|
+
if (callback) callback();
|
|
1303
|
+
}
|
|
1304
|
+
return {
|
|
1305
|
+
snippets,
|
|
1306
|
+
occurrences: totalItems,
|
|
1307
|
+
};
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
// This is for telemetry purposes called within mergeAxeResults.ts
|
|
1311
|
+
export
|
|
1292
1312
|
const createRuleIdJson = allIssues => {
|
|
1293
1313
|
const compiledRuleJson = {};
|
|
1294
1314
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
rule.pagesAffected.forEach(page => {
|
|
1302
|
-
page.items.forEach(htmlItem => {
|
|
1303
|
-
snippetsSet.add(oobeeAiHtmlETL(htmlItem.html));
|
|
1315
|
+
['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
|
|
1316
|
+
allIssues.items[category].rules.forEach(rule => {
|
|
1317
|
+
const allItems = rule.pagesAffected.flatMap(page => page.items || []);
|
|
1318
|
+
compiledRuleJson[rule.rule] = extractRuleAiData(rule.rule, rule.totalItems, allItems, () => {
|
|
1319
|
+
rule.pagesAffected.forEach(p => {
|
|
1320
|
+
delete p.items;
|
|
1304
1321
|
});
|
|
1305
1322
|
});
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
return compiledRuleJson;
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// This is for telemetry purposes called from npmIndex (scanPage and scanHTML) where report is not generated
|
|
1330
|
+
export const createBasicFormHTMLSnippet = filteredResults => {
|
|
1331
|
+
const compiledRuleJson = {};
|
|
1332
|
+
|
|
1333
|
+
['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
|
|
1334
|
+
if (filteredResults[category] && filteredResults[category].rules) {
|
|
1335
|
+
Object.entries(filteredResults[category].rules).forEach(([ruleId, ruleVal]: [string, any]) => {
|
|
1336
|
+
compiledRuleJson[ruleId] = extractRuleAiData(ruleId, ruleVal.totalItems, ruleVal.items);
|
|
1309
1337
|
});
|
|
1310
1338
|
}
|
|
1311
|
-
|
|
1312
|
-
snippets,
|
|
1313
|
-
occurrences: rule.totalItems,
|
|
1314
|
-
};
|
|
1315
|
-
};
|
|
1339
|
+
});
|
|
1316
1340
|
|
|
1317
|
-
allIssues.items.mustFix.rules.forEach(ruleIterator);
|
|
1318
|
-
allIssues.items.goodToFix.rules.forEach(ruleIterator);
|
|
1319
|
-
allIssues.items.needsReview.rules.forEach(ruleIterator);
|
|
1320
1341
|
return compiledRuleJson;
|
|
1321
1342
|
};
|
|
1322
1343
|
|
|
@@ -1587,7 +1608,7 @@ function populateScanPagesDetail(allIssues: AllIssues): void {
|
|
|
1587
1608
|
}
|
|
1588
1609
|
|
|
1589
1610
|
// Send WCAG criteria breakdown to Sentry
|
|
1590
|
-
const sendWcagBreakdownToSentry = async (
|
|
1611
|
+
export const sendWcagBreakdownToSentry = async (
|
|
1591
1612
|
appVersion: string,
|
|
1592
1613
|
wcagBreakdown: Map<string, number>,
|
|
1593
1614
|
ruleIdJson: any,
|
package/src/npmIndex.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import printMessage from 'print-message';
|
|
4
4
|
import axe, { AxeResults, ImpactValue } from 'axe-core';
|
|
5
|
+
import { JSDOM } from 'jsdom';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
6
7
|
import { EnqueueStrategy } from 'crawlee';
|
|
7
8
|
import constants, { BrowserTypes, RuleFlags, ScannerTypes } from './constants/constants.js';
|
|
@@ -12,8 +13,8 @@ import {
|
|
|
12
13
|
submitForm,
|
|
13
14
|
} from './constants/common.js';
|
|
14
15
|
import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
|
15
|
-
import { createAndUpdateResultsFolders } from './utils.js';
|
|
16
|
-
import generateArtifacts from './mergeAxeResults.js';
|
|
16
|
+
import { createAndUpdateResultsFolders, getVersion } from './utils.js';
|
|
17
|
+
import generateArtifacts, { createBasicFormHTMLSnippet, sendWcagBreakdownToSentry } from './mergeAxeResults.js';
|
|
17
18
|
import { takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
|
|
18
19
|
import { consoleLogger, silentLogger } from './logs.js';
|
|
19
20
|
import { alertMessageOptions } from './constants/cliFunctions.js';
|
|
@@ -26,101 +27,42 @@ import xPathToCss from './crawlers/custom/xPathToCss.js';
|
|
|
26
27
|
import { extractText } from './crawlers/custom/extractText.js';
|
|
27
28
|
import { gradeReadability } from './crawlers/custom/gradeReadability.js';
|
|
28
29
|
import { BrowserContext, Page } from 'playwright';
|
|
30
|
+
import { filter } from 'jszip';
|
|
31
|
+
|
|
32
|
+
// Define global window properties for Oobee injection functions
|
|
33
|
+
declare global {
|
|
34
|
+
interface Window {
|
|
35
|
+
runA11yScan: (
|
|
36
|
+
elements?: any[],
|
|
37
|
+
gradingReadabilityFlag?: string,
|
|
38
|
+
) => Promise<{
|
|
39
|
+
pageUrl: string;
|
|
40
|
+
pageTitle: string;
|
|
41
|
+
axeScanResults: AxeResults;
|
|
42
|
+
}>;
|
|
43
|
+
axe: any;
|
|
44
|
+
getAxeConfiguration: any;
|
|
45
|
+
flagUnlabelledClickableElements: any;
|
|
46
|
+
disableOobee: boolean;
|
|
47
|
+
enableWcagAaa: boolean;
|
|
48
|
+
xPathToCss: any;
|
|
49
|
+
evaluateAltText: any;
|
|
50
|
+
escapeCssSelector: any;
|
|
51
|
+
framesCheck: any;
|
|
52
|
+
findElementByCssSelector: any;
|
|
53
|
+
extractText: any;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
29
56
|
|
|
30
57
|
const filename = fileURLToPath(import.meta.url);
|
|
31
58
|
const dirname = path.dirname(filename);
|
|
32
59
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
name,
|
|
37
|
-
email,
|
|
38
|
-
includeScreenshots = false,
|
|
39
|
-
viewportSettings = { width: 1000, height: 660 }, // cypress' default viewport settings
|
|
40
|
-
thresholds = { mustFix: undefined, goodToFix: undefined },
|
|
41
|
-
scanAboutMetadata = undefined,
|
|
42
|
-
zip = 'oobee-scan-results',
|
|
43
|
-
deviceChosen,
|
|
44
|
-
strategy = EnqueueStrategy.All,
|
|
45
|
-
ruleset = [RuleFlags.DEFAULT],
|
|
46
|
-
specifiedMaxConcurrency = 25,
|
|
47
|
-
followRobots = false,
|
|
48
|
-
}: {
|
|
49
|
-
entryUrl: string;
|
|
50
|
-
testLabel: string;
|
|
51
|
-
name: string;
|
|
52
|
-
email: string;
|
|
53
|
-
includeScreenshots?: boolean;
|
|
54
|
-
viewportSettings?: { width: number; height: number };
|
|
55
|
-
thresholds?: { mustFix: number; goodToFix: number };
|
|
56
|
-
scanAboutMetadata?: {
|
|
57
|
-
browser?: string;
|
|
58
|
-
viewport?: { width: number; height: number };
|
|
59
|
-
};
|
|
60
|
-
zip?: string;
|
|
61
|
-
deviceChosen?: string;
|
|
62
|
-
strategy?: EnqueueStrategy;
|
|
63
|
-
ruleset?: RuleFlags[];
|
|
64
|
-
specifiedMaxConcurrency?: number;
|
|
65
|
-
followRobots?: boolean;
|
|
66
|
-
}) => {
|
|
67
|
-
consoleLogger.info('Starting Oobee');
|
|
68
|
-
|
|
69
|
-
const [date, time] = new Date().toLocaleString('sv').replaceAll(/-|:/g, '').split(' ');
|
|
70
|
-
const domain = new URL(entryUrl).hostname;
|
|
71
|
-
const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
|
|
72
|
-
const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
|
|
73
|
-
|
|
74
|
-
const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
|
|
75
|
-
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
76
|
-
|
|
77
|
-
// max numbers of mustFix/goodToFix occurrences before test returns a fail
|
|
78
|
-
const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
|
|
79
|
-
|
|
80
|
-
process.env.CRAWLEE_STORAGE_DIR = randomToken;
|
|
81
|
-
|
|
82
|
-
const scanDetails = {
|
|
83
|
-
startTime: new Date(),
|
|
84
|
-
endTime: new Date(),
|
|
85
|
-
deviceChosen,
|
|
86
|
-
crawlType: ScannerTypes.CUSTOM,
|
|
87
|
-
requestUrl: entryUrl,
|
|
88
|
-
urlsCrawled: { ...constants.urlsCrawledObj },
|
|
89
|
-
isIncludeScreenshots: includeScreenshots,
|
|
90
|
-
isAllowSubdomains: strategy,
|
|
91
|
-
isEnableCustomChecks: ruleset,
|
|
92
|
-
isEnableWcagAaa: ruleset,
|
|
93
|
-
isSlowScanMode: specifiedMaxConcurrency,
|
|
94
|
-
isAdhereRobots: followRobots,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const urlsCrawled = { ...constants.urlsCrawledObj };
|
|
98
|
-
|
|
99
|
-
const { dataset } = await createCrawleeSubFolders(randomToken);
|
|
100
|
-
|
|
101
|
-
let mustFixIssues = 0;
|
|
102
|
-
let goodToFixIssues = 0;
|
|
103
|
-
|
|
104
|
-
let isInstanceTerminated = false;
|
|
105
|
-
|
|
106
|
-
const throwErrorIfTerminated = () => {
|
|
107
|
-
if (isInstanceTerminated) {
|
|
108
|
-
throw new Error('This instance of Oobee was terminated. Please start a new instance.');
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const getAxeScript = () => {
|
|
113
|
-
throwErrorIfTerminated();
|
|
114
|
-
const axeScript = fs.readFileSync(
|
|
115
|
-
path.join(dirname, '../../../axe-core/axe.min.js'),
|
|
116
|
-
'utf-8',
|
|
117
|
-
);
|
|
118
|
-
return axeScript;
|
|
119
|
-
};
|
|
60
|
+
const getAxeScriptContent = () => {
|
|
61
|
+
return axe.source;
|
|
62
|
+
};
|
|
120
63
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return `
|
|
64
|
+
const getOobeeFunctionsScript = (disableOobee: boolean, enableWcagAaa: boolean) => {
|
|
65
|
+
return `
|
|
124
66
|
// Fix for missing __name function used by bundler
|
|
125
67
|
if (typeof __name === 'undefined') {
|
|
126
68
|
window.__name = function(fn, name) {
|
|
@@ -178,7 +120,7 @@ export const init = async ({
|
|
|
178
120
|
return !node.dataset.flagged; // fail any element with a data-flagged attribute set to true
|
|
179
121
|
},
|
|
180
122
|
},
|
|
181
|
-
...(enableWcagAaa
|
|
123
|
+
...((enableWcagAaa && !disableOobee)
|
|
182
124
|
? [
|
|
183
125
|
{
|
|
184
126
|
id: 'oobee-grading-text-contents',
|
|
@@ -226,19 +168,23 @@ export const init = async ({
|
|
|
226
168
|
helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
|
|
227
169
|
},
|
|
228
170
|
},
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
171
|
+
...((enableWcagAaa && !disableOobee)
|
|
172
|
+
? [
|
|
173
|
+
{
|
|
174
|
+
id: 'oobee-grading-text-contents',
|
|
175
|
+
selector: 'html',
|
|
176
|
+
enabled: true,
|
|
177
|
+
any: ['oobee-grading-text-contents'],
|
|
178
|
+
tags: ['wcag2aaa', 'wcag315'],
|
|
179
|
+
metadata: {
|
|
180
|
+
description:
|
|
181
|
+
'Text content should be easy to understand for individuals with education levels up to university graduates. If the text content is difficult to understand, provide supplemental content or a version that is easy to understand.',
|
|
182
|
+
help: 'Text content should be clear and plain to ensure that it is easily understood.',
|
|
183
|
+
helpUrl: 'https://www.wcag.com/uncategorized/3-1-5-reading-level/',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
]
|
|
187
|
+
: []),
|
|
242
188
|
]
|
|
243
189
|
.filter(rule => (disableOobee ? !rule.id.startsWith('oobee') : true))
|
|
244
190
|
.concat(
|
|
@@ -291,6 +237,31 @@ export const init = async ({
|
|
|
291
237
|
const axeScanResults = await (window).axe.run(elementsToScan, {
|
|
292
238
|
resultTypes: ['violations', 'passes', 'incomplete'],
|
|
293
239
|
});
|
|
240
|
+
|
|
241
|
+
if (axeScanResults) {
|
|
242
|
+
['violations', 'incomplete'].forEach(type => {
|
|
243
|
+
if (axeScanResults[type]) {
|
|
244
|
+
axeScanResults[type].forEach(result => {
|
|
245
|
+
if (result.nodes) {
|
|
246
|
+
result.nodes.forEach(node => {
|
|
247
|
+
['any', 'all', 'none'].forEach(key => {
|
|
248
|
+
if (node[key]) {
|
|
249
|
+
node[key].forEach(check => {
|
|
250
|
+
if (check.message && check.message.indexOf("Axe encountered an error") !== -1) {
|
|
251
|
+
if (check.data) {
|
|
252
|
+
// console.error(check.data);
|
|
253
|
+
console.error("Axe encountered an error: " + (check.data.stack || check.data.message || JSON.stringify(check.data)));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
294
265
|
|
|
295
266
|
// add custom Oobee violations
|
|
296
267
|
if (!(window).disableOobee) {
|
|
@@ -340,6 +311,95 @@ export const init = async ({
|
|
|
340
311
|
window.enableWcagAaa=${enableWcagAaa};
|
|
341
312
|
window.runA11yScan = runA11yScan;
|
|
342
313
|
`;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const init = async ({
|
|
317
|
+
entryUrl,
|
|
318
|
+
testLabel,
|
|
319
|
+
name,
|
|
320
|
+
email,
|
|
321
|
+
includeScreenshots = false,
|
|
322
|
+
viewportSettings = { width: 1000, height: 660 }, // cypress' default viewport settings
|
|
323
|
+
thresholds = { mustFix: undefined, goodToFix: undefined },
|
|
324
|
+
scanAboutMetadata = undefined,
|
|
325
|
+
zip = 'oobee-scan-results',
|
|
326
|
+
deviceChosen,
|
|
327
|
+
strategy = EnqueueStrategy.All,
|
|
328
|
+
ruleset = [RuleFlags.DEFAULT],
|
|
329
|
+
specifiedMaxConcurrency = 25,
|
|
330
|
+
followRobots = false,
|
|
331
|
+
}: {
|
|
332
|
+
entryUrl: string;
|
|
333
|
+
testLabel: string;
|
|
334
|
+
name: string;
|
|
335
|
+
email: string;
|
|
336
|
+
includeScreenshots?: boolean;
|
|
337
|
+
viewportSettings?: { width: number; height: number };
|
|
338
|
+
thresholds?: { mustFix: number; goodToFix: number };
|
|
339
|
+
scanAboutMetadata?: {
|
|
340
|
+
browser?: string;
|
|
341
|
+
viewport?: { width: number; height: number };
|
|
342
|
+
};
|
|
343
|
+
zip?: string;
|
|
344
|
+
deviceChosen?: string;
|
|
345
|
+
strategy?: EnqueueStrategy;
|
|
346
|
+
ruleset?: RuleFlags[];
|
|
347
|
+
specifiedMaxConcurrency?: number;
|
|
348
|
+
followRobots?: boolean;
|
|
349
|
+
}) => {
|
|
350
|
+
consoleLogger.info('Starting Oobee');
|
|
351
|
+
|
|
352
|
+
const [date, time] = new Date().toLocaleString('sv').replaceAll(/-|:/g, '').split(' ');
|
|
353
|
+
const domain = new URL(entryUrl).hostname;
|
|
354
|
+
const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
|
|
355
|
+
const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
|
|
356
|
+
|
|
357
|
+
const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
|
|
358
|
+
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
359
|
+
|
|
360
|
+
// max numbers of mustFix/goodToFix occurrences before test returns a fail
|
|
361
|
+
const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
|
|
362
|
+
|
|
363
|
+
process.env.CRAWLEE_STORAGE_DIR = randomToken;
|
|
364
|
+
|
|
365
|
+
const scanDetails = {
|
|
366
|
+
startTime: new Date(),
|
|
367
|
+
endTime: new Date(),
|
|
368
|
+
deviceChosen,
|
|
369
|
+
crawlType: ScannerTypes.CUSTOM,
|
|
370
|
+
requestUrl: entryUrl,
|
|
371
|
+
urlsCrawled: { ...constants.urlsCrawledObj },
|
|
372
|
+
isIncludeScreenshots: includeScreenshots,
|
|
373
|
+
isAllowSubdomains: strategy,
|
|
374
|
+
isEnableCustomChecks: ruleset,
|
|
375
|
+
isEnableWcagAaa: ruleset,
|
|
376
|
+
isSlowScanMode: specifiedMaxConcurrency,
|
|
377
|
+
isAdhereRobots: followRobots,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const urlsCrawled = { ...constants.urlsCrawledObj };
|
|
381
|
+
|
|
382
|
+
const { dataset } = await createCrawleeSubFolders(randomToken);
|
|
383
|
+
|
|
384
|
+
let mustFixIssues = 0;
|
|
385
|
+
let goodToFixIssues = 0;
|
|
386
|
+
|
|
387
|
+
let isInstanceTerminated = false;
|
|
388
|
+
|
|
389
|
+
const throwErrorIfTerminated = () => {
|
|
390
|
+
if (isInstanceTerminated) {
|
|
391
|
+
throw new Error('This instance of Oobee was terminated. Please start a new instance.');
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const getAxeScript = () => {
|
|
396
|
+
throwErrorIfTerminated();
|
|
397
|
+
return getAxeScriptContent();
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const getOobeeFunctions = () => {
|
|
401
|
+
throwErrorIfTerminated();
|
|
402
|
+
return getOobeeFunctionsScript(disableOobee, enableWcagAaa);
|
|
343
403
|
};
|
|
344
404
|
|
|
345
405
|
// Helper script for manually copy-paste testing in Chrome browser
|
|
@@ -527,4 +587,295 @@ export const init = async ({
|
|
|
527
587
|
};
|
|
528
588
|
};
|
|
529
589
|
|
|
530
|
-
export default init;
|
|
590
|
+
export default init;
|
|
591
|
+
|
|
592
|
+
const processAndSubmitResults = async (
|
|
593
|
+
scanData: { axeScanResults: AxeResults; pageUrl: string; pageTitle: string } | { axeScanResults: AxeResults; pageUrl: string; pageTitle: string }[],
|
|
594
|
+
name: string,
|
|
595
|
+
email: string,
|
|
596
|
+
metadata: string,
|
|
597
|
+
) => {
|
|
598
|
+
const items = Array.isArray(scanData) ? scanData : [scanData];
|
|
599
|
+
const numberOfPagesScanned = items.length;
|
|
600
|
+
|
|
601
|
+
const allFilteredResults = items.map((item, index) => {
|
|
602
|
+
const filtered = filterAxeResults(item.axeScanResults, item.pageTitle, { pageIndex: index + 1, metadata });
|
|
603
|
+
(filtered as any).url = item.pageUrl;
|
|
604
|
+
return filtered;
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
type Rule = {
|
|
608
|
+
totalItems: number;
|
|
609
|
+
items: any[];
|
|
610
|
+
[key: string]: any;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
type ResultCategory = {
|
|
614
|
+
totalItems: number;
|
|
615
|
+
rules: Record<string, Rule>;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
type CategoryKey = 'mustFix' | 'goodToFix' | 'needsReview';
|
|
619
|
+
|
|
620
|
+
const mergedResults: Record<CategoryKey, ResultCategory> = {
|
|
621
|
+
mustFix: { totalItems: 0, rules: {} },
|
|
622
|
+
goodToFix: { totalItems: 0, rules: {} },
|
|
623
|
+
needsReview: { totalItems: 0, rules: {} },
|
|
624
|
+
// omitting passed from being processed to reduce payload size
|
|
625
|
+
// passed: { totalItems: 0, rules: {} },
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
allFilteredResults.forEach(result => {
|
|
629
|
+
(['mustFix', 'goodToFix', 'needsReview'] as CategoryKey[]).forEach(category => {
|
|
630
|
+
const categoryResult = (result as any)[category];
|
|
631
|
+
if (categoryResult) {
|
|
632
|
+
mergedResults[category].totalItems += categoryResult.totalItems;
|
|
633
|
+
Object.entries(categoryResult.rules).forEach(([ruleId, ruleVal]: [string, any]) => {
|
|
634
|
+
if (!mergedResults[category].rules[ruleId]) {
|
|
635
|
+
mergedResults[category].rules[ruleId] = JSON.parse(JSON.stringify(ruleVal));
|
|
636
|
+
|
|
637
|
+
// Map the description to the short description if available
|
|
638
|
+
if (constants.a11yRuleShortDescriptionMap[ruleId]) {
|
|
639
|
+
mergedResults[category].rules[ruleId].description = constants.a11yRuleShortDescriptionMap[ruleId];
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Add url to items
|
|
643
|
+
mergedResults[category].rules[ruleId].items.forEach((item: any) => {
|
|
644
|
+
item.url = (result as any).url;
|
|
645
|
+
if (item.displayNeedsReview) {
|
|
646
|
+
delete item.displayNeedsReview;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
} else {
|
|
650
|
+
mergedResults[category].rules[ruleId].totalItems += ruleVal.totalItems;
|
|
651
|
+
const newItems = ruleVal.items.map((item: any) => {
|
|
652
|
+
const newItem = { ...item, url: (result as any).url };
|
|
653
|
+
if (newItem.displayNeedsReview) {
|
|
654
|
+
delete newItem.displayNeedsReview;
|
|
655
|
+
}
|
|
656
|
+
return newItem;
|
|
657
|
+
});
|
|
658
|
+
mergedResults[category].rules[ruleId].items.push(...newItems);
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const basicFormHTMLSnippet = createBasicFormHTMLSnippet(mergedResults);
|
|
666
|
+
const entryUrl = items[0].pageUrl;
|
|
667
|
+
|
|
668
|
+
await submitForm(
|
|
669
|
+
BrowserTypes.CHROMIUM,
|
|
670
|
+
'',
|
|
671
|
+
entryUrl,
|
|
672
|
+
null,
|
|
673
|
+
ScannerTypes.CUSTOM,
|
|
674
|
+
email,
|
|
675
|
+
name,
|
|
676
|
+
JSON.stringify(basicFormHTMLSnippet),
|
|
677
|
+
numberOfPagesScanned,
|
|
678
|
+
0,
|
|
679
|
+
0,
|
|
680
|
+
'{}',
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
// Generate WCAG breakdown for Sentry
|
|
684
|
+
const wcagOccurrencesMap = new Map<string, number>();
|
|
685
|
+
|
|
686
|
+
// Iterate through relevant categories to collect WCAG violation occurrences
|
|
687
|
+
(['mustFix', 'goodToFix'] as CategoryKey[]).forEach(category => {
|
|
688
|
+
const rulesObj = mergedResults[category]?.rules;
|
|
689
|
+
if (rulesObj) {
|
|
690
|
+
Object.values(rulesObj).forEach((rule: any) => {
|
|
691
|
+
const count = rule.totalItems;
|
|
692
|
+
if (rule.conformance && Array.isArray(rule.conformance)) {
|
|
693
|
+
rule.conformance
|
|
694
|
+
.filter((c: string) => /wcag[0-9]{3,4}/.test(c))
|
|
695
|
+
.forEach((c: string) => {
|
|
696
|
+
const current = wcagOccurrencesMap.get(c) || 0;
|
|
697
|
+
wcagOccurrencesMap.set(c, current + count);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
const oobeeAppVersion = getVersion();
|
|
705
|
+
|
|
706
|
+
await sendWcagBreakdownToSentry(
|
|
707
|
+
oobeeAppVersion,
|
|
708
|
+
wcagOccurrencesMap,
|
|
709
|
+
basicFormHTMLSnippet,
|
|
710
|
+
{
|
|
711
|
+
entryUrl: entryUrl,
|
|
712
|
+
scanType: ScannerTypes.CUSTOM,
|
|
713
|
+
browser: 'chromium', // Defaulting since we might scan HTML without browser or implicit browser
|
|
714
|
+
email: email,
|
|
715
|
+
name: name,
|
|
716
|
+
},
|
|
717
|
+
undefined,
|
|
718
|
+
numberOfPagesScanned,
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Return original single result if only one page was scanning to maintain backward compatibility structure
|
|
722
|
+
if (numberOfPagesScanned === 1) {
|
|
723
|
+
const singleResult = allFilteredResults[0];
|
|
724
|
+
|
|
725
|
+
// Clean up displayNeedsReview from single result
|
|
726
|
+
(['mustFix', 'goodToFix', 'needsReview'] as CategoryKey[]).forEach(category => {
|
|
727
|
+
const resultCategory = (singleResult as any)[category];
|
|
728
|
+
if (resultCategory && resultCategory.rules) {
|
|
729
|
+
Object.values(resultCategory.rules).forEach((rule: any) => {
|
|
730
|
+
|
|
731
|
+
// Map the description to the short description if available
|
|
732
|
+
if (constants.a11yRuleShortDescriptionMap[rule.rule]) {
|
|
733
|
+
rule.description = constants.a11yRuleShortDescriptionMap[rule.rule];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (rule.items) {
|
|
737
|
+
rule.items.forEach((item: any) => {
|
|
738
|
+
// Ensure item URL matches the result URL
|
|
739
|
+
item.url = (singleResult as any).url;
|
|
740
|
+
|
|
741
|
+
if (item.displayNeedsReview) {
|
|
742
|
+
delete item.displayNeedsReview;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
return singleResult;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return mergedResults;
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// This is an experimental feature to scan static HTML code without the need for Playwright browser
|
|
757
|
+
export const scanHTML = async (
|
|
758
|
+
htmlContent: string | string[],
|
|
759
|
+
config: {
|
|
760
|
+
name: string;
|
|
761
|
+
email: string;
|
|
762
|
+
pageUrl?: string; // If array, we will append index
|
|
763
|
+
pageTitle?: string; // If array, we will append index
|
|
764
|
+
metadata?: string;
|
|
765
|
+
ruleset?: RuleFlags[];
|
|
766
|
+
},
|
|
767
|
+
) => {
|
|
768
|
+
const {
|
|
769
|
+
name,
|
|
770
|
+
email,
|
|
771
|
+
pageUrl = 'raw-html',
|
|
772
|
+
pageTitle = 'HTML Content',
|
|
773
|
+
metadata = '',
|
|
774
|
+
ruleset = [RuleFlags.DEFAULT],
|
|
775
|
+
} = config;
|
|
776
|
+
|
|
777
|
+
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
778
|
+
const tags = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];
|
|
779
|
+
|
|
780
|
+
if (enableWcagAaa) {
|
|
781
|
+
tags.push('wcag2aaa');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const htmlItems = Array.isArray(htmlContent) ? htmlContent : [htmlContent];
|
|
785
|
+
const scanData = [];
|
|
786
|
+
|
|
787
|
+
for (let i = 0; i < htmlItems.length; i++) {
|
|
788
|
+
const htmlString = htmlItems[i];
|
|
789
|
+
const dom = new JSDOM(htmlString);
|
|
790
|
+
|
|
791
|
+
// Configure axe for node environment
|
|
792
|
+
// eslint-disable-next-line no-await-in-loop
|
|
793
|
+
const axeScanResults = await axe.run(
|
|
794
|
+
dom.window.document.documentElement as unknown as Element,
|
|
795
|
+
{
|
|
796
|
+
runOnly: {
|
|
797
|
+
type: 'tag',
|
|
798
|
+
values: tags,
|
|
799
|
+
},
|
|
800
|
+
resultTypes: ['violations', 'passes', 'incomplete'],
|
|
801
|
+
},
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
scanData.push({
|
|
805
|
+
axeScanResults,
|
|
806
|
+
pageUrl: htmlItems.length > 1 ? `${pageUrl}-${i + 1}` : pageUrl,
|
|
807
|
+
pageTitle: htmlItems.length > 1 ? `${pageTitle} ${i + 1}` : pageTitle,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return processAndSubmitResults(scanData, name, email, metadata);
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
export const scanPage = async (
|
|
815
|
+
pages: Page | Page[],
|
|
816
|
+
config: {
|
|
817
|
+
name: string;
|
|
818
|
+
email: string;
|
|
819
|
+
pageTitle?: string;
|
|
820
|
+
metadata?: string;
|
|
821
|
+
ruleset?: RuleFlags[];
|
|
822
|
+
},
|
|
823
|
+
) => {
|
|
824
|
+
const {
|
|
825
|
+
name,
|
|
826
|
+
email,
|
|
827
|
+
pageTitle,
|
|
828
|
+
metadata = '',
|
|
829
|
+
ruleset = [RuleFlags.DEFAULT],
|
|
830
|
+
} = config;
|
|
831
|
+
|
|
832
|
+
const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
|
|
833
|
+
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
834
|
+
|
|
835
|
+
const axeScript = getAxeScriptContent();
|
|
836
|
+
const oobeeFunctions = getOobeeFunctionsScript(disableOobee, enableWcagAaa);
|
|
837
|
+
|
|
838
|
+
const pagesArray = Array.isArray(pages) ? pages : [pages];
|
|
839
|
+
const scanData = [];
|
|
840
|
+
|
|
841
|
+
for (const page of pagesArray) {
|
|
842
|
+
await page.evaluate(`${axeScript}\n${oobeeFunctions}`);
|
|
843
|
+
|
|
844
|
+
// Run the scan inside the page
|
|
845
|
+
const consoleListener = (msg: any) => {
|
|
846
|
+
if (msg.type() === 'error') {
|
|
847
|
+
console.error(`[Browser Console] ${msg.text()}`);
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
page.on('console', consoleListener);
|
|
851
|
+
|
|
852
|
+
try {
|
|
853
|
+
const scanResult = await page.evaluate(async () => {
|
|
854
|
+
return window.runA11yScan();
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
scanData.push({
|
|
858
|
+
axeScanResults: scanResult.axeScanResults,
|
|
859
|
+
pageUrl: page.url(),
|
|
860
|
+
pageTitle: await page.title(),
|
|
861
|
+
});
|
|
862
|
+
} finally {
|
|
863
|
+
page.off('console', consoleListener);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Allow override of page title if scanning a single page
|
|
868
|
+
if (!Array.isArray(pages) && pageTitle) {
|
|
869
|
+
scanData[0].pageTitle = pageTitle;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return processAndSubmitResults(
|
|
873
|
+
scanData,
|
|
874
|
+
name,
|
|
875
|
+
email,
|
|
876
|
+
metadata,
|
|
877
|
+
);
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
export { RuleFlags };
|
|
881
|
+
|