@govtechsg/oobee 0.10.76 → 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/.github/workflows/publish.yml +8 -1
- package/INTEGRATION.md +50 -3
- package/dist/cli.js +252 -0
- package/dist/combine.js +221 -0
- package/dist/constants/cliFunctions.js +306 -0
- package/dist/constants/common.js +1669 -0
- package/dist/constants/constants.js +913 -0
- package/dist/constants/errorMeta.json +319 -0
- package/dist/constants/itemTypeDescription.js +7 -0
- package/dist/constants/oobeeAi.js +121 -0
- package/dist/constants/questions.js +151 -0
- package/dist/constants/sampleData.js +176 -0
- package/dist/crawlers/commonCrawlerFunc.js +428 -0
- package/dist/crawlers/crawlDomain.js +613 -0
- package/dist/crawlers/crawlIntelligentSitemap.js +135 -0
- package/dist/crawlers/crawlLocalFile.js +151 -0
- package/dist/crawlers/crawlSitemap.js +303 -0
- package/dist/crawlers/custom/escapeCssSelector.js +10 -0
- package/dist/crawlers/custom/evaluateAltText.js +11 -0
- package/dist/crawlers/custom/extractAndGradeText.js +44 -0
- package/dist/crawlers/custom/extractText.js +27 -0
- package/dist/crawlers/custom/findElementByCssSelector.js +36 -0
- package/dist/crawlers/custom/flagUnlabelledClickableElements.js +963 -0
- package/dist/crawlers/custom/framesCheck.js +37 -0
- package/dist/crawlers/custom/getAxeConfiguration.js +111 -0
- package/dist/crawlers/custom/gradeReadability.js +23 -0
- package/dist/crawlers/custom/utils.js +1024 -0
- package/dist/crawlers/custom/xPathToCss.js +147 -0
- package/dist/crawlers/guards/urlGuard.js +71 -0
- package/dist/crawlers/pdfScanFunc.js +276 -0
- package/dist/crawlers/runCustom.js +89 -0
- package/dist/exclusions.txt +7 -0
- package/dist/generateHtmlReport.js +144 -0
- package/dist/index.js +62 -0
- package/dist/logs.js +84 -0
- package/dist/mergeAxeResults.js +1588 -0
- package/dist/npmIndex.js +640 -0
- package/dist/proxyService.js +360 -0
- package/dist/runGenerateJustHtmlReport.js +16 -0
- package/dist/screenshotFunc/htmlScreenshotFunc.js +355 -0
- package/dist/screenshotFunc/pdfScreenshotFunc.js +645 -0
- package/dist/services/s3Uploader.js +127 -0
- package/dist/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
- package/dist/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
- package/dist/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
- package/dist/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
- package/dist/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
- package/dist/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
- package/dist/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
- package/dist/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
- package/dist/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
- package/dist/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
- package/dist/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
- package/dist/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
- package/dist/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
- package/dist/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
- package/dist/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
- package/dist/static/ejs/partials/components/summaryScanResults.ejs +16 -0
- package/dist/static/ejs/partials/components/summaryTable.ejs +20 -0
- package/dist/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
- package/dist/static/ejs/partials/components/topTen.ejs +6 -0
- package/dist/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
- package/dist/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
- package/dist/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
- package/dist/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
- package/dist/static/ejs/partials/footer.ejs +24 -0
- package/dist/static/ejs/partials/header.ejs +14 -0
- package/dist/static/ejs/partials/main.ejs +29 -0
- package/dist/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
- package/dist/static/ejs/partials/scripts/bootstrap.ejs +8 -0
- package/dist/static/ejs/partials/scripts/categorySummary.ejs +141 -0
- package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
- package/dist/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
- package/dist/static/ejs/partials/scripts/highlightjs.ejs +335 -0
- package/dist/static/ejs/partials/scripts/popper.ejs +7 -0
- package/dist/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
- package/dist/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
- package/dist/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
- package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +957 -0
- package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +353 -0
- package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
- package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
- package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
- package/dist/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
- package/dist/static/ejs/partials/scripts/screenshotLightbox.ejs +75 -0
- package/dist/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
- package/dist/static/ejs/partials/scripts/summaryTable.ejs +78 -0
- package/dist/static/ejs/partials/scripts/topTen.ejs +61 -0
- package/dist/static/ejs/partials/scripts/utils.ejs +453 -0
- package/dist/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
- package/dist/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
- package/dist/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
- package/dist/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
- package/dist/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
- package/dist/static/ejs/partials/styles/bootstrap.ejs +12391 -0
- package/dist/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
- package/dist/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
- package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
- package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
- package/dist/static/ejs/partials/styles/header.ejs +7 -0
- package/dist/static/ejs/partials/styles/highlightjs.ejs +54 -0
- package/dist/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
- package/dist/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
- package/dist/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
- package/dist/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
- package/dist/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
- package/dist/static/ejs/partials/styles/styles.ejs +1607 -0
- package/dist/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
- package/dist/static/ejs/partials/styles/topTenCard.ejs +44 -0
- package/dist/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
- package/dist/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
- package/dist/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
- package/dist/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
- package/dist/static/ejs/partials/summaryHeader.ejs +70 -0
- package/dist/static/ejs/partials/summaryMain.ejs +49 -0
- package/dist/static/ejs/report.ejs +226 -0
- package/dist/static/ejs/summary.ejs +47 -0
- package/dist/types/types.js +1 -0
- package/dist/utils.js +1070 -0
- package/examples/oobee-cypress-integration-js/cypress/support/e2e.js +36 -6
- package/examples/oobee-cypress-integration-js/cypress.config.js +45 -1
- package/examples/oobee-cypress-integration-ts/cypress.config.ts +47 -1
- package/examples/oobee-cypress-integration-ts/src/cypress/support/e2e.ts +36 -6
- package/examples/oobee-playwright-integration-js/oobee-playwright-demo.js +2 -1
- package/examples/oobee-playwright-integration-ts/src/oobee-playwright-demo.ts +2 -1
- package/examples/oobee-scan-html-demo.js +51 -0
- package/examples/oobee-scan-page-demo.js +40 -0
- package/package.json +9 -3
- package/src/constants/common.ts +2 -2
- package/src/constants/constants.ts +3 -1
- package/src/crawlers/crawlDomain.ts +1 -0
- package/src/crawlers/runCustom.ts +0 -1
- package/src/mergeAxeResults.ts +43 -22
- package/src/npmIndex.ts +500 -131
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';
|
|
@@ -25,101 +26,43 @@ import { flagUnlabelledClickableElements } from './crawlers/custom/flagUnlabelle
|
|
|
25
26
|
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';
|
|
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
|
+
}
|
|
28
56
|
|
|
29
57
|
const filename = fileURLToPath(import.meta.url);
|
|
30
58
|
const dirname = path.dirname(filename);
|
|
31
59
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
name,
|
|
36
|
-
email,
|
|
37
|
-
includeScreenshots = false,
|
|
38
|
-
viewportSettings = { width: 1000, height: 660 }, // cypress' default viewport settings
|
|
39
|
-
thresholds = { mustFix: undefined, goodToFix: undefined },
|
|
40
|
-
scanAboutMetadata = undefined,
|
|
41
|
-
zip = 'oobee-scan-results',
|
|
42
|
-
deviceChosen,
|
|
43
|
-
strategy = EnqueueStrategy.All,
|
|
44
|
-
ruleset = [RuleFlags.DEFAULT],
|
|
45
|
-
specifiedMaxConcurrency = 25,
|
|
46
|
-
followRobots = false,
|
|
47
|
-
}: {
|
|
48
|
-
entryUrl: string;
|
|
49
|
-
testLabel: string;
|
|
50
|
-
name: string;
|
|
51
|
-
email: string;
|
|
52
|
-
includeScreenshots?: boolean;
|
|
53
|
-
viewportSettings?: { width: number; height: number };
|
|
54
|
-
thresholds?: { mustFix: number; goodToFix: number };
|
|
55
|
-
scanAboutMetadata?: {
|
|
56
|
-
browser?: string;
|
|
57
|
-
viewport?: { width: number; height: number };
|
|
58
|
-
};
|
|
59
|
-
zip?: string;
|
|
60
|
-
deviceChosen?: string;
|
|
61
|
-
strategy?: EnqueueStrategy;
|
|
62
|
-
ruleset?: RuleFlags[];
|
|
63
|
-
specifiedMaxConcurrency?: number;
|
|
64
|
-
followRobots?: boolean;
|
|
65
|
-
}) => {
|
|
66
|
-
consoleLogger.info('Starting Oobee');
|
|
67
|
-
|
|
68
|
-
const [date, time] = new Date().toLocaleString('sv').replaceAll(/-|:/g, '').split(' ');
|
|
69
|
-
const domain = new URL(entryUrl).hostname;
|
|
70
|
-
const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
|
|
71
|
-
const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
|
|
72
|
-
|
|
73
|
-
const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
|
|
74
|
-
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
|
75
|
-
|
|
76
|
-
// max numbers of mustFix/goodToFix occurrences before test returns a fail
|
|
77
|
-
const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
|
|
78
|
-
|
|
79
|
-
process.env.CRAWLEE_STORAGE_DIR = randomToken;
|
|
80
|
-
|
|
81
|
-
const scanDetails = {
|
|
82
|
-
startTime: new Date(),
|
|
83
|
-
endTime: new Date(),
|
|
84
|
-
deviceChosen,
|
|
85
|
-
crawlType: ScannerTypes.CUSTOM,
|
|
86
|
-
requestUrl: entryUrl,
|
|
87
|
-
urlsCrawled: { ...constants.urlsCrawledObj },
|
|
88
|
-
isIncludeScreenshots: includeScreenshots,
|
|
89
|
-
isAllowSubdomains: strategy,
|
|
90
|
-
isEnableCustomChecks: ruleset,
|
|
91
|
-
isEnableWcagAaa: ruleset,
|
|
92
|
-
isSlowScanMode: specifiedMaxConcurrency,
|
|
93
|
-
isAdhereRobots: followRobots,
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const urlsCrawled = { ...constants.urlsCrawledObj };
|
|
97
|
-
|
|
98
|
-
const { dataset } = await createCrawleeSubFolders(randomToken);
|
|
99
|
-
|
|
100
|
-
let mustFixIssues = 0;
|
|
101
|
-
let goodToFixIssues = 0;
|
|
102
|
-
|
|
103
|
-
let isInstanceTerminated = false;
|
|
104
|
-
|
|
105
|
-
const throwErrorIfTerminated = () => {
|
|
106
|
-
if (isInstanceTerminated) {
|
|
107
|
-
throw new Error('This instance of Oobee was terminated. Please start a new instance.');
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const getAxeScript = () => {
|
|
112
|
-
throwErrorIfTerminated();
|
|
113
|
-
const axeScript = fs.readFileSync(
|
|
114
|
-
path.join(dirname, '../../../axe-core/axe.min.js'),
|
|
115
|
-
'utf-8',
|
|
116
|
-
);
|
|
117
|
-
return axeScript;
|
|
118
|
-
};
|
|
60
|
+
const getAxeScriptContent = () => {
|
|
61
|
+
return axe.source;
|
|
62
|
+
};
|
|
119
63
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return `
|
|
64
|
+
const getOobeeFunctionsScript = (disableOobee: boolean, enableWcagAaa: boolean) => {
|
|
65
|
+
return `
|
|
123
66
|
// Fix for missing __name function used by bundler
|
|
124
67
|
if (typeof __name === 'undefined') {
|
|
125
68
|
window.__name = function(fn, name) {
|
|
@@ -177,7 +120,7 @@ export const init = async ({
|
|
|
177
120
|
return !node.dataset.flagged; // fail any element with a data-flagged attribute set to true
|
|
178
121
|
},
|
|
179
122
|
},
|
|
180
|
-
...(enableWcagAaa
|
|
123
|
+
...((enableWcagAaa && !disableOobee)
|
|
181
124
|
? [
|
|
182
125
|
{
|
|
183
126
|
id: 'oobee-grading-text-contents',
|
|
@@ -225,19 +168,23 @@ export const init = async ({
|
|
|
225
168
|
helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
|
|
226
169
|
},
|
|
227
170
|
},
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
: []),
|
|
241
188
|
]
|
|
242
189
|
.filter(rule => (disableOobee ? !rule.id.startsWith('oobee') : true))
|
|
243
190
|
.concat(
|
|
@@ -290,6 +237,31 @@ export const init = async ({
|
|
|
290
237
|
const axeScanResults = await (window).axe.run(elementsToScan, {
|
|
291
238
|
resultTypes: ['violations', 'passes', 'incomplete'],
|
|
292
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
|
+
}
|
|
293
265
|
|
|
294
266
|
// add custom Oobee violations
|
|
295
267
|
if (!(window).disableOobee) {
|
|
@@ -339,6 +311,95 @@ export const init = async ({
|
|
|
339
311
|
window.enableWcagAaa=${enableWcagAaa};
|
|
340
312
|
window.runA11yScan = runA11yScan;
|
|
341
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);
|
|
342
403
|
};
|
|
343
404
|
|
|
344
405
|
// Helper script for manually copy-paste testing in Chrome browser
|
|
@@ -351,49 +412,66 @@ export const init = async ({
|
|
|
351
412
|
res: { pageUrl: string; pageTitle: string; axeScanResults: AxeResults },
|
|
352
413
|
metadata: string,
|
|
353
414
|
elementsToClick: string[],
|
|
415
|
+
page?: Page,
|
|
416
|
+
disableScreenshots: boolean = false, // Only for Cypress (or other library that wants to use it's own screenshotting)
|
|
354
417
|
) => {
|
|
355
418
|
throwErrorIfTerminated();
|
|
356
|
-
if (includeScreenshots) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
|
|
419
|
+
if (includeScreenshots && !disableScreenshots) {
|
|
420
|
+
let browserContext: BrowserContext | undefined;
|
|
421
|
+
let browserToRun: BrowserTypes | undefined;
|
|
422
|
+
let clonedBrowserDataDir: string | undefined;
|
|
423
|
+
let pageToScan: Page;
|
|
424
|
+
|
|
425
|
+
if (page) {
|
|
426
|
+
pageToScan = page;
|
|
427
|
+
} else {
|
|
428
|
+
// use chrome by default
|
|
429
|
+
const browserData = getBrowserToRun(randomToken, BrowserTypes.CHROME, false);
|
|
430
|
+
browserToRun = browserData.browserToRun;
|
|
431
|
+
clonedBrowserDataDir = browserData.clonedBrowserDataDir;
|
|
432
|
+
|
|
433
|
+
browserContext = await constants.launcher.launchPersistentContext(clonedBrowserDataDir, {
|
|
434
|
+
viewport: viewportSettings,
|
|
435
|
+
...getPlaywrightLaunchOptions(browserToRun),
|
|
436
|
+
});
|
|
437
|
+
const newPage = await browserContext.newPage();
|
|
438
|
+
await newPage.goto(res.pageUrl);
|
|
439
|
+
try {
|
|
440
|
+
await newPage.waitForLoadState('networkidle', { timeout: 10000 });
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.log('Network idle timeout, continuing with screenshot capture...');
|
|
443
|
+
// Fall back to domcontentloaded if networkidle times out
|
|
444
|
+
await newPage.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
445
|
+
} // click on elements to reveal hidden elements so screenshots can be taken
|
|
446
|
+
if (elementsToClick) {
|
|
447
|
+
for (const elem of elementsToClick) {
|
|
365
448
|
try {
|
|
366
|
-
|
|
449
|
+
await newPage.locator(elem).click();
|
|
367
450
|
} catch (e) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
371
|
-
} // click on elements to reveal hidden elements so screenshots can be taken
|
|
372
|
-
if (elementsToClick) {
|
|
373
|
-
for (const elem of elementsToClick) {
|
|
374
|
-
try {
|
|
375
|
-
await page.locator(elem).click();
|
|
376
|
-
} catch (e) {
|
|
377
|
-
// do nothing if element is not found or not clickable
|
|
451
|
+
// do nothing if element is not found or not clickable
|
|
452
|
+
}
|
|
378
453
|
}
|
|
379
454
|
}
|
|
455
|
+
pageToScan = newPage;
|
|
380
456
|
}
|
|
381
457
|
|
|
382
458
|
res.axeScanResults.violations = await takeScreenshotForHTMLElements(
|
|
383
459
|
res.axeScanResults.violations,
|
|
384
|
-
|
|
460
|
+
pageToScan,
|
|
385
461
|
randomToken,
|
|
386
462
|
3000,
|
|
387
463
|
);
|
|
388
464
|
res.axeScanResults.incomplete = await takeScreenshotForHTMLElements(
|
|
389
465
|
res.axeScanResults.incomplete,
|
|
390
|
-
|
|
466
|
+
pageToScan,
|
|
391
467
|
randomToken,
|
|
392
468
|
3000,
|
|
393
469
|
);
|
|
394
470
|
|
|
395
|
-
|
|
396
|
-
|
|
471
|
+
if (browserContext && browserToRun) {
|
|
472
|
+
await browserContext.close();
|
|
473
|
+
deleteClonedProfiles(browserToRun, randomToken);
|
|
474
|
+
}
|
|
397
475
|
}
|
|
398
476
|
const pageIndex = urlsCrawled.scanned.length + 1;
|
|
399
477
|
const filteredResults = filterAxeResults(res.axeScanResults, res.pageTitle, {
|
|
@@ -509,4 +587,295 @@ export const init = async ({
|
|
|
509
587
|
};
|
|
510
588
|
};
|
|
511
589
|
|
|
512
|
-
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
|
+
|