@govtechsg/oobee 0.10.76 → 0.10.77
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 +7 -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 +1571 -0
- package/dist/npmIndex.js +429 -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/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/npmIndex.ts +42 -24
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
import fs, { ensureDirSync } from 'fs-extra';
|
|
2
|
+
import printMessage from 'print-message';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createWriteStream } from 'fs';
|
|
7
|
+
import { AsyncParser } from '@json2csv/node';
|
|
8
|
+
import zlib from 'zlib';
|
|
9
|
+
import { Base64Encode } from 'base64-stream';
|
|
10
|
+
import { pipeline } from 'stream/promises';
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
import * as Sentry from '@sentry/node';
|
|
13
|
+
import constants, { BrowserTypes, ScannerTypes, sentryConfig, setSentryUser, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, } from './constants/constants.js';
|
|
14
|
+
import { getBrowserToRun, getPlaywrightLaunchOptions } from './constants/common.js';
|
|
15
|
+
import { createScreenshotsFolder, getStoragePath, getVersion, getWcagPassPercentage, getProgressPercentage, retryFunction, zipResults, getIssuesPercentage, getWcagCriteriaMap, categorizeWcagCriteria, getUserDataTxt, register, } from './utils.js';
|
|
16
|
+
import { consoleLogger } from './logs.js';
|
|
17
|
+
import itemTypeDescription from './constants/itemTypeDescription.js';
|
|
18
|
+
import { oobeeAiHtmlETL, oobeeAiRules } from './constants/oobeeAi.js';
|
|
19
|
+
const filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const dirname = path.dirname(filename);
|
|
21
|
+
const BUFFER_LIMIT = 100 * 1024 * 1024; // 100MB size
|
|
22
|
+
const extractFileNames = async (directory) => {
|
|
23
|
+
ensureDirSync(directory);
|
|
24
|
+
return fs
|
|
25
|
+
.readdir(directory)
|
|
26
|
+
.then(allFiles => allFiles.filter(file => path.extname(file).toLowerCase() === '.json'))
|
|
27
|
+
.catch(readdirError => {
|
|
28
|
+
consoleLogger.info('An error has occurred when retrieving files, please try again.');
|
|
29
|
+
throw readdirError;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const parseContentToJson = async (rPath) => fs
|
|
33
|
+
.readFile(rPath, 'utf8')
|
|
34
|
+
.then(content => JSON.parse(content))
|
|
35
|
+
.catch(parseError => {
|
|
36
|
+
consoleLogger.info('An error has occurred when parsing the content, please try again.');
|
|
37
|
+
});
|
|
38
|
+
const writeCsv = async (allIssues, storagePath) => {
|
|
39
|
+
const csvOutput = createWriteStream(`${storagePath}/report.csv`, { encoding: 'utf8' });
|
|
40
|
+
const formatPageViolation = pageNum => {
|
|
41
|
+
if (pageNum < 0)
|
|
42
|
+
return 'Document';
|
|
43
|
+
return `Page ${pageNum}`;
|
|
44
|
+
};
|
|
45
|
+
// transform allIssues into the form:
|
|
46
|
+
// [['mustFix', rule1], ['mustFix', rule2], ['goodToFix', rule3], ...]
|
|
47
|
+
const getRulesByCategory = (allIssues) => {
|
|
48
|
+
return Object.entries(allIssues.items)
|
|
49
|
+
.filter(([category]) => category !== 'passed')
|
|
50
|
+
.reduce((prev, [category, value]) => {
|
|
51
|
+
const rulesEntries = Object.entries(value.rules);
|
|
52
|
+
rulesEntries.forEach(([, ruleInfo]) => {
|
|
53
|
+
prev.push([category, ruleInfo]);
|
|
54
|
+
});
|
|
55
|
+
return prev;
|
|
56
|
+
}, [])
|
|
57
|
+
.sort((a, b) => {
|
|
58
|
+
// sort rules according to severity, then ruleId
|
|
59
|
+
const compareCategory = -a[0].localeCompare(b[0]);
|
|
60
|
+
return compareCategory === 0 ? a[1].rule.localeCompare(b[1].rule) : compareCategory;
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const flattenRule = catAndRule => {
|
|
64
|
+
const [severity, rule] = catAndRule;
|
|
65
|
+
const results = [];
|
|
66
|
+
const { rule: issueId, description: issueDescription, axeImpact, conformance, pagesAffected, helpUrl: learnMore, } = rule;
|
|
67
|
+
// format clauses as a string
|
|
68
|
+
const wcagConformance = conformance.join(',');
|
|
69
|
+
pagesAffected.sort((a, b) => a.url.localeCompare(b.url));
|
|
70
|
+
pagesAffected.forEach(affectedPage => {
|
|
71
|
+
const { url, items } = affectedPage;
|
|
72
|
+
items.forEach(item => {
|
|
73
|
+
const { html, page, message, xpath } = item;
|
|
74
|
+
const howToFix = message.replace(/(\r\n|\n|\r)/g, '\\n'); // preserve newlines as \n
|
|
75
|
+
const violation = html || formatPageViolation(page); // page is a number, not a string
|
|
76
|
+
const context = violation.replace(/(\r\n|\n|\r)/g, ''); // remove newlines
|
|
77
|
+
results.push({
|
|
78
|
+
customFlowLabel: allIssues.customFlowLabel || '',
|
|
79
|
+
deviceChosen: allIssues.deviceChosen || '',
|
|
80
|
+
scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
81
|
+
severity: severity || '',
|
|
82
|
+
issueId: issueId || '',
|
|
83
|
+
issueDescription: a11yRuleShortDescriptionMap[issueId] || issueDescription || '',
|
|
84
|
+
wcagConformance: wcagConformance || '',
|
|
85
|
+
url: url || '',
|
|
86
|
+
pageTitle: affectedPage.pageTitle || 'No page title',
|
|
87
|
+
context: context || '',
|
|
88
|
+
howToFix: howToFix || '',
|
|
89
|
+
axeImpact: axeImpact || '',
|
|
90
|
+
xpath: xpath || '',
|
|
91
|
+
learnMore: learnMore || '',
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
if (results.length === 0)
|
|
96
|
+
return {};
|
|
97
|
+
return results;
|
|
98
|
+
};
|
|
99
|
+
const opts = {
|
|
100
|
+
transforms: [getRulesByCategory, flattenRule],
|
|
101
|
+
fields: [
|
|
102
|
+
'customFlowLabel',
|
|
103
|
+
'deviceChosen',
|
|
104
|
+
'scanCompletedAt',
|
|
105
|
+
'severity',
|
|
106
|
+
'issueId',
|
|
107
|
+
'issueDescription',
|
|
108
|
+
'wcagConformance',
|
|
109
|
+
'url',
|
|
110
|
+
'pageTitle',
|
|
111
|
+
'context',
|
|
112
|
+
'howToFix',
|
|
113
|
+
'axeImpact',
|
|
114
|
+
'xpath',
|
|
115
|
+
'learnMore',
|
|
116
|
+
],
|
|
117
|
+
includeEmptyRows: true,
|
|
118
|
+
};
|
|
119
|
+
// Create the parse stream (it's asynchronous)
|
|
120
|
+
const parser = new AsyncParser(opts);
|
|
121
|
+
const parseStream = parser.parse(allIssues);
|
|
122
|
+
// Pipe JSON2CSV output into the file, but don't end automatically
|
|
123
|
+
parseStream.pipe(csvOutput, { end: false });
|
|
124
|
+
// Once JSON2CSV is done writing all normal rows, append any "pagesNotScanned"
|
|
125
|
+
parseStream.on('end', () => {
|
|
126
|
+
if (allIssues.pagesNotScanned && allIssues.pagesNotScanned.length > 0) {
|
|
127
|
+
csvOutput.write('\n');
|
|
128
|
+
allIssues.pagesNotScanned.forEach(page => {
|
|
129
|
+
const skippedPage = {
|
|
130
|
+
customFlowLabel: allIssues.customFlowLabel || '',
|
|
131
|
+
deviceChosen: allIssues.deviceChosen || '',
|
|
132
|
+
scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
133
|
+
severity: 'error',
|
|
134
|
+
issueId: 'error-pages-skipped',
|
|
135
|
+
issueDescription: page.metadata
|
|
136
|
+
? page.metadata
|
|
137
|
+
: 'An unknown error caused the page to be skipped',
|
|
138
|
+
wcagConformance: '',
|
|
139
|
+
url: page.url || page || '',
|
|
140
|
+
pageTitle: 'Error',
|
|
141
|
+
context: '',
|
|
142
|
+
howToFix: '',
|
|
143
|
+
axeImpact: '',
|
|
144
|
+
xpath: '',
|
|
145
|
+
learnMore: '',
|
|
146
|
+
};
|
|
147
|
+
csvOutput.write(`${Object.values(skippedPage).join(',')}\n`);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// Now close the CSV file
|
|
151
|
+
csvOutput.end();
|
|
152
|
+
});
|
|
153
|
+
parseStream.on('error', err => {
|
|
154
|
+
console.error('Error parsing CSV:', err);
|
|
155
|
+
csvOutput.end();
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
const compileHtmlWithEJS = async (allIssues, storagePath, htmlFilename = 'report') => {
|
|
159
|
+
const htmlFilePath = `${path.join(storagePath, htmlFilename)}.html`;
|
|
160
|
+
const ejsString = fs.readFileSync(path.join(dirname, './static/ejs/report.ejs'), 'utf-8');
|
|
161
|
+
const template = ejs.compile(ejsString, {
|
|
162
|
+
filename: path.join(dirname, './static/ejs/report.ejs'),
|
|
163
|
+
});
|
|
164
|
+
const html = template({ ...allIssues, storagePath: JSON.stringify(storagePath) });
|
|
165
|
+
await fs.writeFile(htmlFilePath, html);
|
|
166
|
+
let htmlContent = await fs.readFile(htmlFilePath, { encoding: 'utf8' });
|
|
167
|
+
const headIndex = htmlContent.indexOf('</head>');
|
|
168
|
+
const injectScript = `
|
|
169
|
+
<script>
|
|
170
|
+
// IMPORTANT! DO NOT REMOVE ME: Decode the encoded data
|
|
171
|
+
|
|
172
|
+
</script>
|
|
173
|
+
`;
|
|
174
|
+
if (headIndex !== -1) {
|
|
175
|
+
htmlContent = htmlContent.slice(0, headIndex) + injectScript + htmlContent.slice(headIndex);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
htmlContent += injectScript;
|
|
179
|
+
}
|
|
180
|
+
await fs.writeFile(htmlFilePath, htmlContent);
|
|
181
|
+
return htmlFilePath;
|
|
182
|
+
};
|
|
183
|
+
const splitHtmlAndCreateFiles = async (htmlFilePath, storagePath) => {
|
|
184
|
+
try {
|
|
185
|
+
const htmlContent = await fs.readFile(htmlFilePath, { encoding: 'utf8' });
|
|
186
|
+
const splitMarker = '// IMPORTANT! DO NOT REMOVE ME: Decode the encoded data';
|
|
187
|
+
const splitIndex = htmlContent.indexOf(splitMarker);
|
|
188
|
+
if (splitIndex === -1) {
|
|
189
|
+
throw new Error('Marker comment not found in the HTML file.');
|
|
190
|
+
}
|
|
191
|
+
const topContent = `${htmlContent.slice(0, splitIndex + splitMarker.length)}\n\n`;
|
|
192
|
+
const bottomContent = htmlContent.slice(splitIndex + splitMarker.length);
|
|
193
|
+
const topFilePath = path.join(storagePath, 'report-partial-top.htm.txt');
|
|
194
|
+
const bottomFilePath = path.join(storagePath, 'report-partial-bottom.htm.txt');
|
|
195
|
+
await fs.writeFile(topFilePath, topContent, { encoding: 'utf8' });
|
|
196
|
+
await fs.writeFile(bottomFilePath, bottomContent, { encoding: 'utf8' });
|
|
197
|
+
await fs.unlink(htmlFilePath);
|
|
198
|
+
return { topFilePath, bottomFilePath };
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error('Error splitting HTML and creating files:', error);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDetailsFilePath, scanItemsFilePath) => {
|
|
205
|
+
const htmlFilePath = await compileHtmlWithEJS(allIssues, storagePath, htmlFilename);
|
|
206
|
+
const { topFilePath, bottomFilePath } = await splitHtmlAndCreateFiles(htmlFilePath, storagePath);
|
|
207
|
+
const prefixData = fs.readFileSync(path.join(storagePath, 'report-partial-top.htm.txt'), 'utf-8');
|
|
208
|
+
const suffixData = fs.readFileSync(path.join(storagePath, 'report-partial-bottom.htm.txt'), 'utf-8');
|
|
209
|
+
const scanDetailsReadStream = fs.createReadStream(scanDetailsFilePath, {
|
|
210
|
+
encoding: 'utf8',
|
|
211
|
+
highWaterMark: BUFFER_LIMIT,
|
|
212
|
+
});
|
|
213
|
+
const scanItemsReadStream = fs.createReadStream(scanItemsFilePath, {
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
highWaterMark: BUFFER_LIMIT,
|
|
216
|
+
});
|
|
217
|
+
const outputFilePath = `${storagePath}/${htmlFilename}.html`;
|
|
218
|
+
const outputStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
|
|
219
|
+
const cleanupFiles = async () => {
|
|
220
|
+
try {
|
|
221
|
+
await Promise.all([fs.promises.unlink(topFilePath), fs.promises.unlink(bottomFilePath)]);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
console.error('Error cleaning up temporary files:', err);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
outputStream.write(prefixData);
|
|
228
|
+
// For Proxied AI environments only
|
|
229
|
+
outputStream.write(`let proxyUrl = "${process.env.PROXY_API_BASE_URL || ""}"\n`);
|
|
230
|
+
// Initialize GenAI feature flag
|
|
231
|
+
outputStream.write(`
|
|
232
|
+
// Fetch GenAI feature flag from backend
|
|
233
|
+
window.oobeeGenAiFeatureEnabled = false;
|
|
234
|
+
if (proxyUrl !== "" && proxyUrl !== undefined && proxyUrl !== null) {
|
|
235
|
+
(async () => {
|
|
236
|
+
try {
|
|
237
|
+
const featuresUrl = proxyUrl + '/api/ai/features';
|
|
238
|
+
const response = await fetch(featuresUrl, {
|
|
239
|
+
method: 'GET',
|
|
240
|
+
headers: { 'Accept': 'application/json' }
|
|
241
|
+
});
|
|
242
|
+
if (response.ok) {
|
|
243
|
+
const features = await response.json();
|
|
244
|
+
window.oobeeGenAiFeatureEnabled = features.genai_ui_enabled || false;
|
|
245
|
+
console.log('GenAI UI feature flag:', window.oobeeGenAiFeatureEnabled);
|
|
246
|
+
} else {
|
|
247
|
+
console.warn('Failed to fetch GenAI feature flag:', response.status);
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.warn('Error fetching GenAI feature flag:', error);
|
|
251
|
+
}
|
|
252
|
+
})();
|
|
253
|
+
} else {
|
|
254
|
+
console.warn('Skipping fetch GenAI feature as it is local report');
|
|
255
|
+
}
|
|
256
|
+
\n`);
|
|
257
|
+
// outputStream.write("scanData = decompressJsonObject('");
|
|
258
|
+
outputStream.write("let scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse('");
|
|
259
|
+
scanDetailsReadStream.pipe(outputStream, { end: false });
|
|
260
|
+
scanDetailsReadStream.on('end', () => {
|
|
261
|
+
// outputStream.write("')\n\n");
|
|
262
|
+
outputStream.write("'); })();\n\n");
|
|
263
|
+
// outputStream.write("(scanItems = decompressJsonObject('");
|
|
264
|
+
outputStream.write("let scanItemsPromise = (async () => { console.log('Loading scanItems...'); scanItems = await decodeUnzipParse('");
|
|
265
|
+
scanItemsReadStream.pipe(outputStream, { end: false });
|
|
266
|
+
});
|
|
267
|
+
scanDetailsReadStream.on('error', err => {
|
|
268
|
+
console.error('Read stream error:', err);
|
|
269
|
+
outputStream.end();
|
|
270
|
+
});
|
|
271
|
+
scanItemsReadStream.on('end', () => {
|
|
272
|
+
// outputStream.write("')\n\n");
|
|
273
|
+
outputStream.write("'); })();\n\n");
|
|
274
|
+
outputStream.write(suffixData);
|
|
275
|
+
outputStream.end();
|
|
276
|
+
});
|
|
277
|
+
scanItemsReadStream.on('error', err => {
|
|
278
|
+
console.error('Read stream error:', err);
|
|
279
|
+
outputStream.end();
|
|
280
|
+
});
|
|
281
|
+
consoleLogger.info('Content appended successfully.');
|
|
282
|
+
await cleanupFiles();
|
|
283
|
+
outputStream.on('error', err => {
|
|
284
|
+
consoleLogger.error('Error writing to output file:', err);
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
const writeSummaryHTML = async (allIssues, storagePath, htmlFilename = 'summary') => {
|
|
288
|
+
const ejsString = fs.readFileSync(path.join(dirname, './static/ejs/summary.ejs'), 'utf-8');
|
|
289
|
+
const template = ejs.compile(ejsString, {
|
|
290
|
+
filename: path.join(dirname, './static/ejs/summary.ejs'),
|
|
291
|
+
});
|
|
292
|
+
const html = template(allIssues);
|
|
293
|
+
fs.writeFileSync(`${storagePath}/${htmlFilename}.html`, html);
|
|
294
|
+
};
|
|
295
|
+
const writeSitemap = async (pagesScanned, storagePath) => {
|
|
296
|
+
const sitemapPath = path.join(storagePath, 'sitemap.txt');
|
|
297
|
+
const content = pagesScanned.map(p => p.url).join('\n');
|
|
298
|
+
await fs.writeFile(sitemapPath, content, { encoding: 'utf-8' });
|
|
299
|
+
consoleLogger.info(`Sitemap written to ${sitemapPath}`);
|
|
300
|
+
};
|
|
301
|
+
const cleanUpJsonFiles = async (filesToDelete) => {
|
|
302
|
+
consoleLogger.info('Cleaning up JSON files...');
|
|
303
|
+
filesToDelete.forEach(file => {
|
|
304
|
+
fs.unlinkSync(file);
|
|
305
|
+
consoleLogger.info(`Deleted ${file}`);
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
function* serializeObject(obj, depth = 0, indent = ' ') {
|
|
309
|
+
const currentIndent = indent.repeat(depth);
|
|
310
|
+
const nextIndent = indent.repeat(depth + 1);
|
|
311
|
+
if (obj instanceof Date) {
|
|
312
|
+
yield JSON.stringify(obj.toISOString());
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(obj)) {
|
|
316
|
+
yield '[\n';
|
|
317
|
+
for (let i = 0; i < obj.length; i++) {
|
|
318
|
+
if (i > 0)
|
|
319
|
+
yield ',\n';
|
|
320
|
+
yield nextIndent;
|
|
321
|
+
yield* serializeObject(obj[i], depth + 1, indent);
|
|
322
|
+
}
|
|
323
|
+
yield `\n${currentIndent}]`;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (obj !== null && typeof obj === 'object') {
|
|
327
|
+
yield '{\n';
|
|
328
|
+
const keys = Object.keys(obj);
|
|
329
|
+
for (let i = 0; i < keys.length; i++) {
|
|
330
|
+
const key = keys[i];
|
|
331
|
+
if (i > 0)
|
|
332
|
+
yield ',\n';
|
|
333
|
+
yield `${nextIndent}${JSON.stringify(key)}: `;
|
|
334
|
+
yield* serializeObject(obj[key], depth + 1, indent);
|
|
335
|
+
}
|
|
336
|
+
yield `\n${currentIndent}}`;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (obj === null || typeof obj === 'function' || typeof obj === 'undefined') {
|
|
340
|
+
yield 'null';
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
yield JSON.stringify(obj);
|
|
344
|
+
}
|
|
345
|
+
function writeLargeJsonToFile(obj, filePath) {
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
const writeStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
|
348
|
+
writeStream.on('error', error => {
|
|
349
|
+
consoleLogger.error('Stream error:', error);
|
|
350
|
+
reject(error);
|
|
351
|
+
});
|
|
352
|
+
writeStream.on('finish', () => {
|
|
353
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
354
|
+
resolve(true);
|
|
355
|
+
});
|
|
356
|
+
const generator = serializeObject(obj);
|
|
357
|
+
function write() {
|
|
358
|
+
let next;
|
|
359
|
+
while (!(next = generator.next()).done) {
|
|
360
|
+
if (!writeStream.write(next.value)) {
|
|
361
|
+
writeStream.once('drain', write);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
writeStream.end();
|
|
366
|
+
}
|
|
367
|
+
write();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const writeLargeScanItemsJsonToFile = async (obj, filePath) => {
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
const writeStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
|
|
373
|
+
const writeQueue = [];
|
|
374
|
+
let isWriting = false;
|
|
375
|
+
const processNextWrite = async () => {
|
|
376
|
+
if (isWriting || writeQueue.length === 0)
|
|
377
|
+
return;
|
|
378
|
+
isWriting = true;
|
|
379
|
+
const data = writeQueue.shift();
|
|
380
|
+
try {
|
|
381
|
+
if (!writeStream.write(data)) {
|
|
382
|
+
await new Promise(resolve => {
|
|
383
|
+
writeStream.once('drain', () => {
|
|
384
|
+
resolve();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
writeStream.destroy(error);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
isWriting = false;
|
|
394
|
+
processNextWrite();
|
|
395
|
+
};
|
|
396
|
+
const queueWrite = (data) => {
|
|
397
|
+
writeQueue.push(data);
|
|
398
|
+
processNextWrite();
|
|
399
|
+
};
|
|
400
|
+
writeStream.on('error', error => {
|
|
401
|
+
consoleLogger.error(`Error writing object to JSON file: ${error}`);
|
|
402
|
+
reject(error);
|
|
403
|
+
});
|
|
404
|
+
writeStream.on('finish', () => {
|
|
405
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
406
|
+
resolve(true);
|
|
407
|
+
});
|
|
408
|
+
try {
|
|
409
|
+
queueWrite('{\n');
|
|
410
|
+
const keys = Object.keys(obj);
|
|
411
|
+
keys.forEach((key, i) => {
|
|
412
|
+
const value = obj[key];
|
|
413
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
414
|
+
queueWrite(` "${key}": ${JSON.stringify(value)}`);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
queueWrite(` "${key}": {\n`);
|
|
418
|
+
const { rules, ...otherProperties } = value;
|
|
419
|
+
// Write other properties
|
|
420
|
+
Object.entries(otherProperties).forEach(([propKey, propValue], j) => {
|
|
421
|
+
const propValueString = propValue === null ||
|
|
422
|
+
typeof propValue === 'function' ||
|
|
423
|
+
typeof propValue === 'undefined'
|
|
424
|
+
? 'null'
|
|
425
|
+
: JSON.stringify(propValue);
|
|
426
|
+
queueWrite(` "${propKey}": ${propValueString}`);
|
|
427
|
+
if (j < Object.keys(otherProperties).length - 1 || (rules && rules.length >= 0)) {
|
|
428
|
+
queueWrite(',\n');
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
queueWrite('\n');
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
if (rules && Array.isArray(rules)) {
|
|
435
|
+
queueWrite(' "rules": [\n');
|
|
436
|
+
rules.forEach((rule, j) => {
|
|
437
|
+
queueWrite(' {\n');
|
|
438
|
+
const { pagesAffected, ...otherRuleProperties } = rule;
|
|
439
|
+
Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
|
|
440
|
+
const ruleValueString = ruleValue === null ||
|
|
441
|
+
typeof ruleValue === 'function' ||
|
|
442
|
+
typeof ruleValue === 'undefined'
|
|
443
|
+
? 'null'
|
|
444
|
+
: JSON.stringify(ruleValue);
|
|
445
|
+
queueWrite(` "${ruleKey}": ${ruleValueString}`);
|
|
446
|
+
if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
|
|
447
|
+
queueWrite(',\n');
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
queueWrite('\n');
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
if (pagesAffected && Array.isArray(pagesAffected)) {
|
|
454
|
+
queueWrite(' "pagesAffected": [\n');
|
|
455
|
+
pagesAffected.forEach((page, p) => {
|
|
456
|
+
const pageJson = JSON.stringify(page, null, 2)
|
|
457
|
+
.split('\n')
|
|
458
|
+
.map((line, idx) => (idx === 0 ? ` ${line}` : ` ${line}`))
|
|
459
|
+
.join('\n');
|
|
460
|
+
queueWrite(pageJson);
|
|
461
|
+
if (p < pagesAffected.length - 1) {
|
|
462
|
+
queueWrite(',\n');
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
queueWrite('\n');
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
queueWrite(' ]');
|
|
469
|
+
}
|
|
470
|
+
queueWrite('\n }');
|
|
471
|
+
if (j < rules.length - 1) {
|
|
472
|
+
queueWrite(',\n');
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
queueWrite('\n');
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
queueWrite(' ]');
|
|
479
|
+
}
|
|
480
|
+
queueWrite('\n }');
|
|
481
|
+
}
|
|
482
|
+
if (i < keys.length - 1) {
|
|
483
|
+
queueWrite(',\n');
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
queueWrite('\n');
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
queueWrite('}\n');
|
|
490
|
+
// Ensure all queued writes are processed before ending
|
|
491
|
+
const checkQueueAndEnd = () => {
|
|
492
|
+
if (writeQueue.length === 0 && !isWriting) {
|
|
493
|
+
writeStream.end();
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
setTimeout(checkQueueAndEnd, 100);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
checkQueueAndEnd();
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
writeStream.destroy(err);
|
|
503
|
+
reject(err);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
async function compressJsonFileStreaming(inputPath, outputPath) {
|
|
508
|
+
// Create the read and write streams
|
|
509
|
+
const readStream = fs.createReadStream(inputPath);
|
|
510
|
+
const writeStream = fs.createWriteStream(outputPath);
|
|
511
|
+
// Create a gzip transform stream
|
|
512
|
+
const gzip = zlib.createGzip();
|
|
513
|
+
// Create a Base64 transform stream
|
|
514
|
+
const base64Encode = new Base64Encode();
|
|
515
|
+
// Pipe the streams:
|
|
516
|
+
// read -> gzip -> base64 -> write
|
|
517
|
+
await pipeline(readStream, gzip, base64Encode, writeStream);
|
|
518
|
+
consoleLogger.info(`File successfully compressed and saved to ${outputPath}`);
|
|
519
|
+
}
|
|
520
|
+
const writeJsonFileAndCompressedJsonFile = async (data, storagePath, filename) => {
|
|
521
|
+
try {
|
|
522
|
+
consoleLogger.info(`Writing JSON to ${filename}.json`);
|
|
523
|
+
const jsonFilePath = path.join(storagePath, `${filename}.json`);
|
|
524
|
+
if (filename === 'scanItems') {
|
|
525
|
+
await writeLargeScanItemsJsonToFile(data, jsonFilePath);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
await writeLargeJsonToFile(data, jsonFilePath);
|
|
529
|
+
}
|
|
530
|
+
consoleLogger.info(`Reading ${filename}.json, gzipping and base64 encoding it into ${filename}.json.gz.b64`);
|
|
531
|
+
const base64FilePath = path.join(storagePath, `${filename}.json.gz.b64`);
|
|
532
|
+
await compressJsonFileStreaming(jsonFilePath, base64FilePath);
|
|
533
|
+
consoleLogger.info(`Finished compression and base64 encoding for ${filename}`);
|
|
534
|
+
return {
|
|
535
|
+
jsonFilePath,
|
|
536
|
+
base64FilePath,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
consoleLogger.error(`Error compressing and encoding ${filename}`);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const streamEncodedDataToFile = async (inputFilePath, writeStream, appendComma) => {
|
|
545
|
+
const readStream = fs.createReadStream(inputFilePath, { encoding: 'utf8' });
|
|
546
|
+
let isFirstChunk = true;
|
|
547
|
+
for await (const chunk of readStream) {
|
|
548
|
+
if (isFirstChunk) {
|
|
549
|
+
isFirstChunk = false;
|
|
550
|
+
writeStream.write(chunk);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
writeStream.write(chunk);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (appendComma) {
|
|
557
|
+
writeStream.write(',');
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
const writeJsonAndBase64Files = async (allIssues, storagePath) => {
|
|
561
|
+
const { items, ...rest } = allIssues;
|
|
562
|
+
const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } = await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
|
|
563
|
+
const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...items }, storagePath, 'scanItems');
|
|
564
|
+
// Add pagesAffectedCount to each rule in scanItemsMiniReport (items) and sort them in descending order of pagesAffectedCount
|
|
565
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
566
|
+
if (items[category].rules && Array.isArray(items[category].rules)) {
|
|
567
|
+
items[category].rules.forEach(rule => {
|
|
568
|
+
rule.pagesAffectedCount = Array.isArray(rule.pagesAffected) ? rule.pagesAffected.length : 0;
|
|
569
|
+
});
|
|
570
|
+
// Sort in descending order of pagesAffectedCount
|
|
571
|
+
items[category].rules.sort((a, b) => (b.pagesAffectedCount || 0) - (a.pagesAffectedCount || 0));
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
// Refactor scanIssuesSummary to reuse the scanItemsMiniReport structure by stripping out pagesAffected
|
|
575
|
+
const scanIssuesSummary = {
|
|
576
|
+
// Replace rule descriptions with short descriptions from the map
|
|
577
|
+
mustFix: items.mustFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
578
|
+
...ruleInfo,
|
|
579
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
580
|
+
})),
|
|
581
|
+
goodToFix: items.goodToFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
582
|
+
...ruleInfo,
|
|
583
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
584
|
+
})),
|
|
585
|
+
needsReview: items.needsReview.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
586
|
+
...ruleInfo,
|
|
587
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
588
|
+
})),
|
|
589
|
+
passed: items.passed.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
590
|
+
...ruleInfo,
|
|
591
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
592
|
+
})),
|
|
593
|
+
};
|
|
594
|
+
// Write out the scanIssuesSummary JSON using the new structure
|
|
595
|
+
const { jsonFilePath: scanIssuesSummaryJsonFilePath, base64FilePath: scanIssuesSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanIssuesSummary }, storagePath, 'scanIssuesSummary');
|
|
596
|
+
// scanItemsSummary
|
|
597
|
+
// the below mutates the original items object, since it is expensive to clone
|
|
598
|
+
items.mustFix.rules.forEach(rule => {
|
|
599
|
+
rule.pagesAffected.forEach(page => {
|
|
600
|
+
page.itemsCount = page.items.length;
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
items.goodToFix.rules.forEach(rule => {
|
|
604
|
+
rule.pagesAffected.forEach(page => {
|
|
605
|
+
page.itemsCount = page.items.length;
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
items.needsReview.rules.forEach(rule => {
|
|
609
|
+
rule.pagesAffected.forEach(page => {
|
|
610
|
+
page.itemsCount = page.items.length;
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
items.passed.rules.forEach(rule => {
|
|
614
|
+
rule.pagesAffected.forEach(page => {
|
|
615
|
+
page.itemsCount = page.items.length;
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
items.mustFix.totalRuleIssues = items.mustFix.rules.length;
|
|
619
|
+
items.goodToFix.totalRuleIssues = items.goodToFix.rules.length;
|
|
620
|
+
items.needsReview.totalRuleIssues = items.needsReview.rules.length;
|
|
621
|
+
items.passed.totalRuleIssues = items.passed.rules.length;
|
|
622
|
+
const { pagesScanned, topTenPagesWithMostIssues, pagesNotScanned, wcagLinks, wcagPassPercentage, progressPercentage, issuesPercentage, totalPagesScanned, totalPagesNotScanned, topTenIssues, } = rest;
|
|
623
|
+
const summaryItemsMini = {
|
|
624
|
+
...items,
|
|
625
|
+
pagesScanned,
|
|
626
|
+
topTenPagesWithMostIssues,
|
|
627
|
+
pagesNotScanned,
|
|
628
|
+
wcagLinks,
|
|
629
|
+
wcagPassPercentage,
|
|
630
|
+
progressPercentage,
|
|
631
|
+
issuesPercentage,
|
|
632
|
+
totalPagesScanned,
|
|
633
|
+
totalPagesNotScanned,
|
|
634
|
+
topTenIssues,
|
|
635
|
+
};
|
|
636
|
+
const { jsonFilePath: scanItemsMiniReportJsonFilePath, base64FilePath: scanItemsMiniReportBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItemsMini }, storagePath, 'scanItemsSummaryMiniReport');
|
|
637
|
+
const summaryItems = {
|
|
638
|
+
mustFix: {
|
|
639
|
+
totalItems: items.mustFix?.totalItems || 0,
|
|
640
|
+
totalRuleIssues: items.mustFix?.totalRuleIssues || 0,
|
|
641
|
+
},
|
|
642
|
+
goodToFix: {
|
|
643
|
+
totalItems: items.goodToFix?.totalItems || 0,
|
|
644
|
+
totalRuleIssues: items.goodToFix?.totalRuleIssues || 0,
|
|
645
|
+
},
|
|
646
|
+
needsReview: {
|
|
647
|
+
totalItems: items.needsReview?.totalItems || 0,
|
|
648
|
+
totalRuleIssues: items.needsReview?.totalRuleIssues || 0,
|
|
649
|
+
},
|
|
650
|
+
topTenPagesWithMostIssues,
|
|
651
|
+
wcagLinks,
|
|
652
|
+
wcagPassPercentage,
|
|
653
|
+
progressPercentage,
|
|
654
|
+
issuesPercentage,
|
|
655
|
+
totalPagesScanned,
|
|
656
|
+
totalPagesNotScanned,
|
|
657
|
+
topTenIssues,
|
|
658
|
+
};
|
|
659
|
+
const { jsonFilePath: scanItemsSummaryJsonFilePath, base64FilePath: scanItemsSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItems }, storagePath, 'scanItemsSummary');
|
|
660
|
+
const { jsonFilePath: scanPagesDetailJsonFilePath, base64FilePath: scanPagesDetailBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesDetail }, storagePath, 'scanPagesDetail');
|
|
661
|
+
const { jsonFilePath: scanPagesSummaryJsonFilePath, base64FilePath: scanPagesSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesSummary }, storagePath, 'scanPagesSummary');
|
|
662
|
+
return {
|
|
663
|
+
scanDataJsonFilePath,
|
|
664
|
+
scanDataBase64FilePath,
|
|
665
|
+
scanItemsJsonFilePath,
|
|
666
|
+
scanItemsBase64FilePath,
|
|
667
|
+
scanItemsSummaryJsonFilePath,
|
|
668
|
+
scanItemsSummaryBase64FilePath,
|
|
669
|
+
scanItemsMiniReportJsonFilePath,
|
|
670
|
+
scanItemsMiniReportBase64FilePath,
|
|
671
|
+
scanIssuesSummaryJsonFilePath,
|
|
672
|
+
scanIssuesSummaryBase64FilePath,
|
|
673
|
+
scanPagesDetailJsonFilePath,
|
|
674
|
+
scanPagesDetailBase64FilePath,
|
|
675
|
+
scanPagesSummaryJsonFilePath,
|
|
676
|
+
scanPagesSummaryBase64FilePath,
|
|
677
|
+
scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
|
|
678
|
+
scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
|
|
679
|
+
};
|
|
680
|
+
};
|
|
681
|
+
const writeScanDetailsCsv = async (scanDataFilePath, scanItemsFilePath, scanItemsSummaryFilePath, storagePath) => {
|
|
682
|
+
const filePath = path.join(storagePath, 'scanDetails.csv');
|
|
683
|
+
const csvWriteStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
|
684
|
+
const directoryPath = path.dirname(filePath);
|
|
685
|
+
if (!fs.existsSync(directoryPath)) {
|
|
686
|
+
fs.mkdirSync(directoryPath, { recursive: true });
|
|
687
|
+
}
|
|
688
|
+
csvWriteStream.write('scanData_base64,scanItems_base64,scanItemsSummary_base64\n');
|
|
689
|
+
await streamEncodedDataToFile(scanDataFilePath, csvWriteStream, true);
|
|
690
|
+
await streamEncodedDataToFile(scanItemsFilePath, csvWriteStream, true);
|
|
691
|
+
await streamEncodedDataToFile(scanItemsSummaryFilePath, csvWriteStream, false);
|
|
692
|
+
await new Promise((resolve, reject) => {
|
|
693
|
+
csvWriteStream.end(resolve);
|
|
694
|
+
csvWriteStream.on('error', reject);
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
const writeSummaryPdf = async (storagePath, pagesScanned, filename = 'summary', browser, userDataDirectory) => {
|
|
698
|
+
const htmlFilePath = `${storagePath}/${filename}.html`;
|
|
699
|
+
const fileDestinationPath = `${storagePath}/${filename}.pdf`;
|
|
700
|
+
const effectiveUserDataDirectory = process.env.CRAWLEE_HEADLESS === '1' ? userDataDirectory : '';
|
|
701
|
+
const context = await constants.launcher.launchPersistentContext(effectiveUserDataDirectory, {
|
|
702
|
+
headless: true,
|
|
703
|
+
...getPlaywrightLaunchOptions(browser),
|
|
704
|
+
});
|
|
705
|
+
register(context);
|
|
706
|
+
const page = await context.newPage();
|
|
707
|
+
const data = fs.readFileSync(htmlFilePath, { encoding: 'utf-8' });
|
|
708
|
+
await page.setContent(data);
|
|
709
|
+
await page.waitForLoadState('networkidle', { timeout: 30000 });
|
|
710
|
+
await page.emulateMedia({ media: 'print' });
|
|
711
|
+
await page.pdf({
|
|
712
|
+
margin: { bottom: '32px' },
|
|
713
|
+
path: fileDestinationPath,
|
|
714
|
+
format: 'A4',
|
|
715
|
+
displayHeaderFooter: true,
|
|
716
|
+
footerTemplate: `
|
|
717
|
+
<div style="margin-top:50px;color:#26241b;font-family:Open Sans;text-align: center;width: 100%;font-weight:400">
|
|
718
|
+
<span style="color:#26241b;font-size: 14px;font-weight:400">Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
|
|
719
|
+
</div>
|
|
720
|
+
`,
|
|
721
|
+
});
|
|
722
|
+
await page.close();
|
|
723
|
+
await context.close().catch(() => { });
|
|
724
|
+
if (pagesScanned < 2000) {
|
|
725
|
+
fs.unlinkSync(htmlFilePath);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
// Tracking WCAG occurrences
|
|
729
|
+
const wcagOccurrencesMap = new Map();
|
|
730
|
+
// Format WCAG tag in requested format: wcag111a_Occurrences
|
|
731
|
+
const formatWcagTag = async (wcagId) => {
|
|
732
|
+
// Get dynamic WCAG criteria map
|
|
733
|
+
const wcagCriteriaMap = await getWcagCriteriaMap();
|
|
734
|
+
if (wcagCriteriaMap[wcagId]) {
|
|
735
|
+
const { level } = wcagCriteriaMap[wcagId];
|
|
736
|
+
return `${wcagId}${level}_Occurrences`;
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
};
|
|
740
|
+
const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
741
|
+
const { url, pageTitle, filePath } = pageResults;
|
|
742
|
+
const totalIssuesInPage = new Set();
|
|
743
|
+
Object.keys(pageResults.mustFix.rules).forEach(k => totalIssuesInPage.add(k));
|
|
744
|
+
Object.keys(pageResults.goodToFix.rules).forEach(k => totalIssuesInPage.add(k));
|
|
745
|
+
Object.keys(pageResults.needsReview.rules).forEach(k => totalIssuesInPage.add(k));
|
|
746
|
+
allIssues.topFiveMostIssues.push({
|
|
747
|
+
url,
|
|
748
|
+
pageTitle,
|
|
749
|
+
totalIssues: totalIssuesInPage.size,
|
|
750
|
+
totalOccurrences: 0,
|
|
751
|
+
});
|
|
752
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
753
|
+
if (!pageResults[category])
|
|
754
|
+
return;
|
|
755
|
+
const { totalItems, rules } = pageResults[category];
|
|
756
|
+
const currCategoryFromAllIssues = allIssues.items[category];
|
|
757
|
+
currCategoryFromAllIssues.totalItems += totalItems;
|
|
758
|
+
Object.keys(rules).forEach(rule => {
|
|
759
|
+
const { description, axeImpact, helpUrl, conformance, totalItems: count, items, } = rules[rule];
|
|
760
|
+
if (!(rule in currCategoryFromAllIssues.rules)) {
|
|
761
|
+
currCategoryFromAllIssues.rules[rule] = {
|
|
762
|
+
description,
|
|
763
|
+
axeImpact,
|
|
764
|
+
helpUrl,
|
|
765
|
+
conformance,
|
|
766
|
+
totalItems: 0,
|
|
767
|
+
// numberOfPagesAffectedAfterRedirects: 0,
|
|
768
|
+
pagesAffected: {},
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
if (category !== 'passed' && category !== 'needsReview') {
|
|
772
|
+
conformance
|
|
773
|
+
.filter(c => /wcag[0-9]{3,4}/.test(c))
|
|
774
|
+
.forEach(c => {
|
|
775
|
+
if (!allIssues.wcagViolations.includes(c)) {
|
|
776
|
+
allIssues.wcagViolations.push(c);
|
|
777
|
+
}
|
|
778
|
+
// Track WCAG criteria occurrences for Sentry
|
|
779
|
+
const currentCount = wcagOccurrencesMap.get(c) || 0;
|
|
780
|
+
wcagOccurrencesMap.set(c, currentCount + count);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
const currRuleFromAllIssues = currCategoryFromAllIssues.rules[rule];
|
|
784
|
+
currRuleFromAllIssues.totalItems += count;
|
|
785
|
+
if (isCustomFlow) {
|
|
786
|
+
const { pageIndex, pageImagePath, metadata } = pageResults;
|
|
787
|
+
currRuleFromAllIssues.pagesAffected[pageIndex] = {
|
|
788
|
+
url,
|
|
789
|
+
pageTitle,
|
|
790
|
+
pageImagePath,
|
|
791
|
+
metadata,
|
|
792
|
+
items: [],
|
|
793
|
+
};
|
|
794
|
+
currRuleFromAllIssues.pagesAffected[pageIndex].items.push(...items);
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
if (!(url in currRuleFromAllIssues.pagesAffected)) {
|
|
798
|
+
currRuleFromAllIssues.pagesAffected[url] = {
|
|
799
|
+
pageTitle,
|
|
800
|
+
items: [],
|
|
801
|
+
...(filePath && { filePath }),
|
|
802
|
+
};
|
|
803
|
+
/* if (actualUrl) {
|
|
804
|
+
currRuleFromAllIssues.pagesAffected[url].actualUrl = actualUrl;
|
|
805
|
+
// Deduct duplication count from totalItems
|
|
806
|
+
currRuleFromAllIssues.totalItems -= 1;
|
|
807
|
+
// Previously using pagesAffected.length to display no. of pages affected
|
|
808
|
+
// However, since pagesAffected array contains duplicates, we need to deduct the duplicates
|
|
809
|
+
// Hence, start with negative offset, will add pagesAffected.length later
|
|
810
|
+
currRuleFromAllIssues.numberOfPagesAffectedAfterRedirects -= 1;
|
|
811
|
+
currCategoryFromAllIssues.totalItems -= 1;
|
|
812
|
+
} */
|
|
813
|
+
}
|
|
814
|
+
currRuleFromAllIssues.pagesAffected[url].items.push(...items);
|
|
815
|
+
// currRuleFromAllIssues.numberOfPagesAffectedAfterRedirects +=
|
|
816
|
+
// currRuleFromAllIssues.pagesAffected.length;
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
};
|
|
821
|
+
const getTopTenIssues = allIssues => {
|
|
822
|
+
const categories = ['mustFix', 'goodToFix'];
|
|
823
|
+
const rulesWithCounts = [];
|
|
824
|
+
// This is no longer required and shall not be maintained in future
|
|
825
|
+
/*
|
|
826
|
+
const conformanceLevels = {
|
|
827
|
+
wcag2a: 'A',
|
|
828
|
+
wcag2aa: 'AA',
|
|
829
|
+
wcag21aa: 'AA',
|
|
830
|
+
wcag22aa: 'AA',
|
|
831
|
+
wcag2aaa: 'AAA',
|
|
832
|
+
};
|
|
833
|
+
*/
|
|
834
|
+
categories.forEach(category => {
|
|
835
|
+
const rules = allIssues.items[category]?.rules || [];
|
|
836
|
+
rules.forEach(rule => {
|
|
837
|
+
// This is not needed anymore since we want to have the clause number too
|
|
838
|
+
/*
|
|
839
|
+
const wcagLevel = rule.conformance[0];
|
|
840
|
+
const aLevel = conformanceLevels[wcagLevel] || wcagLevel;
|
|
841
|
+
*/
|
|
842
|
+
rulesWithCounts.push({
|
|
843
|
+
category,
|
|
844
|
+
ruleId: rule.rule,
|
|
845
|
+
// Replace description with new Oobee short description if available
|
|
846
|
+
description: a11yRuleShortDescriptionMap[rule.rule] || rule.description,
|
|
847
|
+
axeImpact: rule.axeImpact,
|
|
848
|
+
conformance: rule.conformance,
|
|
849
|
+
totalItems: rule.totalItems,
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
rulesWithCounts.sort((a, b) => b.totalItems - a.totalItems);
|
|
854
|
+
return rulesWithCounts.slice(0, 10);
|
|
855
|
+
};
|
|
856
|
+
const flattenAndSortResults = (allIssues, isCustomFlow) => {
|
|
857
|
+
// Create a map that will sum items only from mustFix, goodToFix, and needsReview.
|
|
858
|
+
const urlOccurrencesMap = new Map();
|
|
859
|
+
// Iterate over all categories; update the map only if the category is not "passed"
|
|
860
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
861
|
+
// Accumulate totalItems regardless of category.
|
|
862
|
+
allIssues.totalItems += allIssues.items[category].totalItems;
|
|
863
|
+
allIssues.items[category].rules = Object.entries(allIssues.items[category].rules)
|
|
864
|
+
.map(ruleEntry => {
|
|
865
|
+
const [rule, ruleInfo] = ruleEntry;
|
|
866
|
+
ruleInfo.pagesAffected = Object.entries(ruleInfo.pagesAffected)
|
|
867
|
+
.map(pageEntry => {
|
|
868
|
+
if (isCustomFlow) {
|
|
869
|
+
const [pageIndex, pageInfo] = pageEntry;
|
|
870
|
+
// Only update the occurrences map if not passed.
|
|
871
|
+
if (category !== 'passed') {
|
|
872
|
+
urlOccurrencesMap.set(pageInfo.url, (urlOccurrencesMap.get(pageInfo.url) || 0) + pageInfo.items.length);
|
|
873
|
+
}
|
|
874
|
+
return { pageIndex, ...pageInfo };
|
|
875
|
+
}
|
|
876
|
+
const [url, pageInfo] = pageEntry;
|
|
877
|
+
if (category !== 'passed') {
|
|
878
|
+
urlOccurrencesMap.set(url, (urlOccurrencesMap.get(url) || 0) + pageInfo.items.length);
|
|
879
|
+
}
|
|
880
|
+
return { url, ...pageInfo };
|
|
881
|
+
})
|
|
882
|
+
// Sort pages so that those with the most items come first
|
|
883
|
+
.sort((page1, page2) => page2.items.length - page1.items.length);
|
|
884
|
+
return { rule, ...ruleInfo };
|
|
885
|
+
})
|
|
886
|
+
// Sort the rules by totalItems (descending)
|
|
887
|
+
.sort((rule1, rule2) => rule2.totalItems - rule1.totalItems);
|
|
888
|
+
});
|
|
889
|
+
// Sort top pages (assumes topFiveMostIssues is already populated)
|
|
890
|
+
allIssues.topFiveMostIssues.sort((p1, p2) => p2.totalIssues - p1.totalIssues);
|
|
891
|
+
allIssues.topTenPagesWithMostIssues = allIssues.topFiveMostIssues.slice(0, 10);
|
|
892
|
+
allIssues.topFiveMostIssues = allIssues.topFiveMostIssues.slice(0, 5);
|
|
893
|
+
// Update each issue in topTenPagesWithMostIssues with the computed occurrences,
|
|
894
|
+
// excluding passed items.
|
|
895
|
+
updateIssuesWithOccurrences(allIssues.topTenPagesWithMostIssues, urlOccurrencesMap);
|
|
896
|
+
// Get and assign the topTenIssues (using your existing helper)
|
|
897
|
+
const topTenIssues = getTopTenIssues(allIssues);
|
|
898
|
+
allIssues.topTenIssues = topTenIssues;
|
|
899
|
+
};
|
|
900
|
+
// Helper: Update totalOccurrences for each issue using our urlOccurrencesMap.
|
|
901
|
+
// For pages that have only passed items, the map will return undefined, so default to 0.
|
|
902
|
+
function updateIssuesWithOccurrences(issuesList, urlOccurrencesMap) {
|
|
903
|
+
issuesList.forEach(issue => {
|
|
904
|
+
issue.totalOccurrences = urlOccurrencesMap.get(issue.url) || 0;
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
const createRuleIdJson = allIssues => {
|
|
908
|
+
const compiledRuleJson = {};
|
|
909
|
+
const ruleIterator = rule => {
|
|
910
|
+
const ruleId = rule.rule;
|
|
911
|
+
let snippets = [];
|
|
912
|
+
if (oobeeAiRules.includes(ruleId)) {
|
|
913
|
+
const snippetsSet = new Set();
|
|
914
|
+
rule.pagesAffected.forEach(page => {
|
|
915
|
+
page.items.forEach(htmlItem => {
|
|
916
|
+
snippetsSet.add(oobeeAiHtmlETL(htmlItem.html));
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
snippets = [...snippetsSet];
|
|
920
|
+
rule.pagesAffected.forEach(p => {
|
|
921
|
+
delete p.items;
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
compiledRuleJson[ruleId] = {
|
|
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);
|
|
932
|
+
return compiledRuleJson;
|
|
933
|
+
};
|
|
934
|
+
const moveElemScreenshots = (randomToken, storagePath) => {
|
|
935
|
+
const currentScreenshotsPath = `${randomToken}/elemScreenshots`;
|
|
936
|
+
const resultsScreenshotsPath = `${storagePath}/elemScreenshots`;
|
|
937
|
+
if (fs.existsSync(currentScreenshotsPath)) {
|
|
938
|
+
fs.moveSync(currentScreenshotsPath, resultsScreenshotsPath);
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
/**
|
|
942
|
+
* Build allIssues.scanPagesDetail and allIssues.scanPagesSummary
|
|
943
|
+
* by analyzing pagesScanned (including mustFix/goodToFix/etc.).
|
|
944
|
+
*/
|
|
945
|
+
function populateScanPagesDetail(allIssues) {
|
|
946
|
+
// --------------------------------------------
|
|
947
|
+
// 1) Gather your "scanned" pages from allIssues
|
|
948
|
+
// --------------------------------------------
|
|
949
|
+
const allScannedPages = Array.isArray(allIssues.pagesScanned) ? allIssues.pagesScanned : [];
|
|
950
|
+
// --------------------------------------------
|
|
951
|
+
// 2) Define category constants (optional, just for clarity)
|
|
952
|
+
// --------------------------------------------
|
|
953
|
+
const mustFixCategory = 'mustFix';
|
|
954
|
+
const goodToFixCategory = 'goodToFix';
|
|
955
|
+
const needsReviewCategory = 'needsReview';
|
|
956
|
+
const passedCategory = 'passed';
|
|
957
|
+
// --------------------------------------------
|
|
958
|
+
// 4) We'll accumulate pages in a map keyed by URL
|
|
959
|
+
// --------------------------------------------
|
|
960
|
+
const pagesMap = {};
|
|
961
|
+
// --------------------------------------------
|
|
962
|
+
// 5) Build pagesMap by iterating over each category in allIssues.items
|
|
963
|
+
// --------------------------------------------
|
|
964
|
+
Object.entries(allIssues.items).forEach(([categoryName, categoryData]) => {
|
|
965
|
+
if (!categoryData?.rules)
|
|
966
|
+
return; // no rules in this category? skip
|
|
967
|
+
categoryData.rules.forEach(rule => {
|
|
968
|
+
const { rule: ruleId, conformance = [] } = rule;
|
|
969
|
+
rule.pagesAffected.forEach(p => {
|
|
970
|
+
const { url, pageTitle, items = [] } = p;
|
|
971
|
+
const itemsCount = items.length;
|
|
972
|
+
// Ensure the page is in pagesMap
|
|
973
|
+
if (!pagesMap[url]) {
|
|
974
|
+
pagesMap[url] = {
|
|
975
|
+
pageTitle,
|
|
976
|
+
url,
|
|
977
|
+
totalOccurrencesFailedIncludingNeedsReview: 0,
|
|
978
|
+
totalOccurrencesFailedExcludingNeedsReview: 0,
|
|
979
|
+
totalOccurrencesNeedsReview: 0,
|
|
980
|
+
totalOccurrencesPassed: 0,
|
|
981
|
+
typesOfIssues: {},
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
// Ensure the rule is present for this page
|
|
985
|
+
if (!pagesMap[url].typesOfIssues[ruleId]) {
|
|
986
|
+
pagesMap[url].typesOfIssues[ruleId] = {
|
|
987
|
+
ruleId,
|
|
988
|
+
wcagConformance: conformance,
|
|
989
|
+
occurrencesMustFix: 0,
|
|
990
|
+
occurrencesGoodToFix: 0,
|
|
991
|
+
occurrencesNeedsReview: 0,
|
|
992
|
+
occurrencesPassed: 0,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
// Depending on the category, increment the relevant occurrence counts
|
|
996
|
+
if (categoryName === mustFixCategory) {
|
|
997
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesMustFix += itemsCount;
|
|
998
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
999
|
+
pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
|
|
1000
|
+
}
|
|
1001
|
+
else if (categoryName === goodToFixCategory) {
|
|
1002
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesGoodToFix += itemsCount;
|
|
1003
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
1004
|
+
pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
|
|
1005
|
+
}
|
|
1006
|
+
else if (categoryName === needsReviewCategory) {
|
|
1007
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesNeedsReview += itemsCount;
|
|
1008
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
1009
|
+
pagesMap[url].totalOccurrencesNeedsReview += itemsCount;
|
|
1010
|
+
}
|
|
1011
|
+
else if (categoryName === passedCategory) {
|
|
1012
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesPassed += itemsCount;
|
|
1013
|
+
pagesMap[url].totalOccurrencesPassed += itemsCount;
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
1018
|
+
// --------------------------------------------
|
|
1019
|
+
// 6) Separate scanned pages into “affected” vs. “notAffected”
|
|
1020
|
+
// --------------------------------------------
|
|
1021
|
+
const pagesInMap = Object.values(pagesMap); // All pages that have some record in pagesMap
|
|
1022
|
+
const pagesInMapUrls = new Set(Object.keys(pagesMap));
|
|
1023
|
+
// (a) Pages with only passed (no mustFix/goodToFix/needsReview)
|
|
1024
|
+
const pagesAllPassed = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview === 0);
|
|
1025
|
+
// (b) Pages that do NOT appear in pagesMap at all => scanned but no items found
|
|
1026
|
+
const pagesNoEntries = allScannedPages
|
|
1027
|
+
.filter(sp => !pagesInMapUrls.has(sp.url))
|
|
1028
|
+
.map(sp => ({
|
|
1029
|
+
pageTitle: sp.pageTitle,
|
|
1030
|
+
url: sp.url,
|
|
1031
|
+
totalOccurrencesFailedIncludingNeedsReview: 0,
|
|
1032
|
+
totalOccurrencesFailedExcludingNeedsReview: 0,
|
|
1033
|
+
totalOccurrencesNeedsReview: 0,
|
|
1034
|
+
totalOccurrencesPassed: 0,
|
|
1035
|
+
typesOfIssues: {},
|
|
1036
|
+
}));
|
|
1037
|
+
// Combine these into "notAffected"
|
|
1038
|
+
const pagesNotAffectedRaw = [...pagesAllPassed, ...pagesNoEntries];
|
|
1039
|
+
// "affected" pages => have at least 1 mustFix/goodToFix/needsReview
|
|
1040
|
+
const pagesAffectedRaw = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview > 0);
|
|
1041
|
+
// --------------------------------------------
|
|
1042
|
+
// 7) Transform both arrays to the final shape
|
|
1043
|
+
// --------------------------------------------
|
|
1044
|
+
function transformPageData(page) {
|
|
1045
|
+
const typesOfIssuesArray = Object.values(page.typesOfIssues);
|
|
1046
|
+
// Compute sums for each failing category
|
|
1047
|
+
const mustFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesMustFix, 0);
|
|
1048
|
+
const goodToFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesGoodToFix, 0);
|
|
1049
|
+
const needsReviewSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesNeedsReview, 0);
|
|
1050
|
+
// Build categoriesPresent based on nonzero failing counts
|
|
1051
|
+
const categoriesPresent = [];
|
|
1052
|
+
if (mustFixSum > 0)
|
|
1053
|
+
categoriesPresent.push('mustFix');
|
|
1054
|
+
if (goodToFixSum > 0)
|
|
1055
|
+
categoriesPresent.push('goodToFix');
|
|
1056
|
+
if (needsReviewSum > 0)
|
|
1057
|
+
categoriesPresent.push('needsReview');
|
|
1058
|
+
// Count how many rules have failing issues
|
|
1059
|
+
const failedRuleIds = new Set();
|
|
1060
|
+
typesOfIssuesArray.forEach(r => {
|
|
1061
|
+
if ((r.occurrencesMustFix || 0) > 0 ||
|
|
1062
|
+
(r.occurrencesGoodToFix || 0) > 0 ||
|
|
1063
|
+
(r.occurrencesNeedsReview || 0) > 0) {
|
|
1064
|
+
failedRuleIds.add(r.ruleId); // Ensure ruleId is unique
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
const failedRuleCount = failedRuleIds.size;
|
|
1068
|
+
// Possibly these two for future convenience
|
|
1069
|
+
const typesOfIssuesExcludingNeedsReviewCount = typesOfIssuesArray.filter(r => (r.occurrencesMustFix || 0) + (r.occurrencesGoodToFix || 0) > 0).length;
|
|
1070
|
+
const typesOfIssuesExclusiveToNeedsReviewCount = typesOfIssuesArray.filter(r => (r.occurrencesNeedsReview || 0) > 0 &&
|
|
1071
|
+
(r.occurrencesMustFix || 0) === 0 &&
|
|
1072
|
+
(r.occurrencesGoodToFix || 0) === 0).length;
|
|
1073
|
+
// Aggregate wcagConformance for rules that actually fail
|
|
1074
|
+
const allConformance = typesOfIssuesArray.reduce((acc, curr) => {
|
|
1075
|
+
const nonPassedCount = (curr.occurrencesMustFix || 0) +
|
|
1076
|
+
(curr.occurrencesGoodToFix || 0) +
|
|
1077
|
+
(curr.occurrencesNeedsReview || 0);
|
|
1078
|
+
if (nonPassedCount > 0) {
|
|
1079
|
+
return acc.concat(curr.wcagConformance || []);
|
|
1080
|
+
}
|
|
1081
|
+
return acc;
|
|
1082
|
+
}, []);
|
|
1083
|
+
// Remove duplicates
|
|
1084
|
+
const conformance = Array.from(new Set(allConformance));
|
|
1085
|
+
return {
|
|
1086
|
+
pageTitle: page.pageTitle,
|
|
1087
|
+
url: page.url,
|
|
1088
|
+
totalOccurrencesFailedIncludingNeedsReview: page.totalOccurrencesFailedIncludingNeedsReview,
|
|
1089
|
+
totalOccurrencesFailedExcludingNeedsReview: page.totalOccurrencesFailedExcludingNeedsReview,
|
|
1090
|
+
totalOccurrencesMustFix: mustFixSum,
|
|
1091
|
+
totalOccurrencesGoodToFix: goodToFixSum,
|
|
1092
|
+
totalOccurrencesNeedsReview: needsReviewSum,
|
|
1093
|
+
totalOccurrencesPassed: page.totalOccurrencesPassed,
|
|
1094
|
+
typesOfIssuesExclusiveToNeedsReviewCount,
|
|
1095
|
+
typesOfIssuesCount: failedRuleCount,
|
|
1096
|
+
typesOfIssuesExcludingNeedsReviewCount,
|
|
1097
|
+
categoriesPresent,
|
|
1098
|
+
conformance,
|
|
1099
|
+
// Keep full detail for "scanPagesDetail"
|
|
1100
|
+
typesOfIssues: typesOfIssuesArray,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
// Transform raw pages
|
|
1104
|
+
const pagesAffected = pagesAffectedRaw.map(transformPageData);
|
|
1105
|
+
const pagesNotAffected = pagesNotAffectedRaw.map(transformPageData);
|
|
1106
|
+
// --------------------------------------------
|
|
1107
|
+
// 8) Sort pages by typesOfIssuesCount (descending) for both arrays
|
|
1108
|
+
// --------------------------------------------
|
|
1109
|
+
pagesAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
|
|
1110
|
+
pagesNotAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
|
|
1111
|
+
// --------------------------------------------
|
|
1112
|
+
// 9) Compute scanned/ skipped counts
|
|
1113
|
+
// --------------------------------------------
|
|
1114
|
+
const scannedPagesCount = pagesAffected.length + pagesNotAffected.length;
|
|
1115
|
+
const pagesNotScannedCount = Array.isArray(allIssues.pagesNotScanned)
|
|
1116
|
+
? allIssues.pagesNotScanned.length
|
|
1117
|
+
: 0;
|
|
1118
|
+
// --------------------------------------------
|
|
1119
|
+
// 10) Build scanPagesDetail (with full "typesOfIssues")
|
|
1120
|
+
// --------------------------------------------
|
|
1121
|
+
allIssues.scanPagesDetail = {
|
|
1122
|
+
pagesAffected,
|
|
1123
|
+
pagesNotAffected,
|
|
1124
|
+
scannedPagesCount,
|
|
1125
|
+
pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
|
|
1126
|
+
pagesNotScannedCount,
|
|
1127
|
+
};
|
|
1128
|
+
// --------------------------------------------
|
|
1129
|
+
// 11) Build scanPagesSummary (strip out "typesOfIssues")
|
|
1130
|
+
// --------------------------------------------
|
|
1131
|
+
function stripTypesOfIssues(page) {
|
|
1132
|
+
const { typesOfIssues, ...rest } = page;
|
|
1133
|
+
return rest;
|
|
1134
|
+
}
|
|
1135
|
+
const summaryPagesAffected = pagesAffected.map(stripTypesOfIssues);
|
|
1136
|
+
const summaryPagesNotAffected = pagesNotAffected.map(stripTypesOfIssues);
|
|
1137
|
+
allIssues.scanPagesSummary = {
|
|
1138
|
+
pagesAffected: summaryPagesAffected,
|
|
1139
|
+
pagesNotAffected: summaryPagesNotAffected,
|
|
1140
|
+
scannedPagesCount,
|
|
1141
|
+
pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
|
|
1142
|
+
pagesNotScannedCount,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
// Send WCAG criteria breakdown to Sentry
|
|
1146
|
+
const sendWcagBreakdownToSentry = async (appVersion, wcagBreakdown, ruleIdJson, scanInfo, allIssues, pagesScannedCount = 0) => {
|
|
1147
|
+
try {
|
|
1148
|
+
// Initialize Sentry
|
|
1149
|
+
Sentry.init(sentryConfig);
|
|
1150
|
+
// Set user ID for Sentry tracking
|
|
1151
|
+
const userData = getUserDataTxt();
|
|
1152
|
+
if (userData && userData.userId) {
|
|
1153
|
+
setSentryUser(userData.userId);
|
|
1154
|
+
}
|
|
1155
|
+
// Prepare tags for the event
|
|
1156
|
+
const tags = {};
|
|
1157
|
+
const wcagCriteriaBreakdown = {};
|
|
1158
|
+
// Tag app version
|
|
1159
|
+
tags.version = appVersion;
|
|
1160
|
+
// Get dynamic WCAG criteria map once
|
|
1161
|
+
const wcagCriteriaMap = await getWcagCriteriaMap();
|
|
1162
|
+
// Categorize all WCAG criteria for reporting
|
|
1163
|
+
const wcagIds = Array.from(new Set([...Object.keys(wcagCriteriaMap), ...Array.from(wcagBreakdown.keys())]));
|
|
1164
|
+
const categorizedWcag = await categorizeWcagCriteria(wcagIds);
|
|
1165
|
+
// First ensure all WCAG criteria are included in the tags with a value of 0
|
|
1166
|
+
// This ensures criteria with no violations are still reported
|
|
1167
|
+
for (const [wcagId, info] of Object.entries(wcagCriteriaMap)) {
|
|
1168
|
+
const formattedTag = await formatWcagTag(wcagId);
|
|
1169
|
+
if (formattedTag) {
|
|
1170
|
+
// Initialize with zero
|
|
1171
|
+
tags[formattedTag] = '0';
|
|
1172
|
+
// Store in breakdown object with category information
|
|
1173
|
+
wcagCriteriaBreakdown[formattedTag] = {
|
|
1174
|
+
count: 0,
|
|
1175
|
+
category: categorizedWcag[wcagId] || 'mustFix', // Default to mustFix if not found
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
// Now override with actual counts from the scan
|
|
1180
|
+
for (const [wcagId, count] of wcagBreakdown.entries()) {
|
|
1181
|
+
const formattedTag = await formatWcagTag(wcagId);
|
|
1182
|
+
if (formattedTag) {
|
|
1183
|
+
// Add as a tag with the count as value
|
|
1184
|
+
tags[formattedTag] = String(count);
|
|
1185
|
+
// Update count in breakdown object
|
|
1186
|
+
if (wcagCriteriaBreakdown[formattedTag]) {
|
|
1187
|
+
wcagCriteriaBreakdown[formattedTag].count = count;
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
// If somehow this wasn't in our initial map
|
|
1191
|
+
wcagCriteriaBreakdown[formattedTag] = {
|
|
1192
|
+
count,
|
|
1193
|
+
category: categorizedWcag[wcagId] || 'mustFix',
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
// Calculate category counts based on actual issue counts from the report
|
|
1199
|
+
// rather than occurrence counts from wcagBreakdown
|
|
1200
|
+
const categoryCounts = {
|
|
1201
|
+
mustFix: 0,
|
|
1202
|
+
goodToFix: 0,
|
|
1203
|
+
needsReview: 0,
|
|
1204
|
+
};
|
|
1205
|
+
if (allIssues) {
|
|
1206
|
+
// Use the actual report data for the counts
|
|
1207
|
+
categoryCounts.mustFix = allIssues.items.mustFix.rules.length;
|
|
1208
|
+
categoryCounts.goodToFix = allIssues.items.goodToFix.rules.length;
|
|
1209
|
+
categoryCounts.needsReview = allIssues.items.needsReview.rules.length;
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
// Fallback to the old way if allIssues not provided
|
|
1213
|
+
Object.values(wcagCriteriaBreakdown).forEach(item => {
|
|
1214
|
+
if (item.count > 0 && categoryCounts[item.category] !== undefined) {
|
|
1215
|
+
categoryCounts[item.category] += 1; // Count rules, not occurrences
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
// Add category counts as tags
|
|
1220
|
+
tags['WCAG-MustFix-Count'] = String(categoryCounts.mustFix);
|
|
1221
|
+
tags['WCAG-GoodToFix-Count'] = String(categoryCounts.goodToFix);
|
|
1222
|
+
tags['WCAG-NeedsReview-Count'] = String(categoryCounts.needsReview);
|
|
1223
|
+
// Also add occurrence counts for reference
|
|
1224
|
+
if (allIssues) {
|
|
1225
|
+
tags['WCAG-MustFix-Occurrences'] = String(allIssues.items.mustFix.totalItems);
|
|
1226
|
+
tags['WCAG-GoodToFix-Occurrences'] = String(allIssues.items.goodToFix.totalItems);
|
|
1227
|
+
tags['WCAG-NeedsReview-Occurrences'] = String(allIssues.items.needsReview.totalItems);
|
|
1228
|
+
// Add number of pages scanned tag
|
|
1229
|
+
tags['Pages-Scanned-Count'] = String(allIssues.totalPagesScanned);
|
|
1230
|
+
}
|
|
1231
|
+
else if (pagesScannedCount > 0) {
|
|
1232
|
+
// Still add the pages scanned count even if we don't have allIssues
|
|
1233
|
+
tags['Pages-Scanned-Count'] = String(pagesScannedCount);
|
|
1234
|
+
}
|
|
1235
|
+
// Send the event to Sentry
|
|
1236
|
+
await Sentry.captureEvent({
|
|
1237
|
+
message: 'Accessibility Scan Completed',
|
|
1238
|
+
level: 'info',
|
|
1239
|
+
tags: {
|
|
1240
|
+
...tags,
|
|
1241
|
+
event_type: 'accessibility_scan',
|
|
1242
|
+
scanType: scanInfo.scanType,
|
|
1243
|
+
browser: scanInfo.browser,
|
|
1244
|
+
entryUrl: scanInfo.entryUrl,
|
|
1245
|
+
},
|
|
1246
|
+
user: {
|
|
1247
|
+
...(scanInfo.email && scanInfo.name
|
|
1248
|
+
? {
|
|
1249
|
+
email: scanInfo.email,
|
|
1250
|
+
username: scanInfo.name,
|
|
1251
|
+
}
|
|
1252
|
+
: {}),
|
|
1253
|
+
...(userData && userData.userId ? { id: userData.userId } : {}),
|
|
1254
|
+
},
|
|
1255
|
+
extra: {
|
|
1256
|
+
additionalScanMetadata: ruleIdJson != null ? JSON.stringify(ruleIdJson) : '{}',
|
|
1257
|
+
wcagBreakdown: wcagCriteriaBreakdown,
|
|
1258
|
+
reportCounts: allIssues
|
|
1259
|
+
? {
|
|
1260
|
+
mustFix: {
|
|
1261
|
+
issues: allIssues.items.mustFix.rules?.length ?? 0,
|
|
1262
|
+
occurrences: allIssues.items.mustFix.totalItems ?? 0,
|
|
1263
|
+
},
|
|
1264
|
+
goodToFix: {
|
|
1265
|
+
issues: allIssues.items.goodToFix.rules?.length ?? 0,
|
|
1266
|
+
occurrences: allIssues.items.goodToFix.totalItems ?? 0,
|
|
1267
|
+
},
|
|
1268
|
+
needsReview: {
|
|
1269
|
+
issues: allIssues.items.needsReview.rules?.length ?? 0,
|
|
1270
|
+
occurrences: allIssues.items.needsReview.totalItems ?? 0,
|
|
1271
|
+
},
|
|
1272
|
+
}
|
|
1273
|
+
: undefined,
|
|
1274
|
+
},
|
|
1275
|
+
});
|
|
1276
|
+
// Wait for events to be sent
|
|
1277
|
+
await Sentry.flush(2000);
|
|
1278
|
+
}
|
|
1279
|
+
catch (error) {
|
|
1280
|
+
console.error('Error sending WCAG breakdown to Sentry:', error);
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
const formatAboutStartTime = (dateString) => {
|
|
1284
|
+
const utcStartTimeDate = new Date(dateString);
|
|
1285
|
+
const formattedStartTime = utcStartTimeDate.toLocaleTimeString('en-GB', {
|
|
1286
|
+
year: 'numeric',
|
|
1287
|
+
month: 'short',
|
|
1288
|
+
day: 'numeric',
|
|
1289
|
+
hour12: false,
|
|
1290
|
+
hour: 'numeric',
|
|
1291
|
+
minute: '2-digit',
|
|
1292
|
+
timeZoneName: 'shortGeneric',
|
|
1293
|
+
});
|
|
1294
|
+
const timezoneAbbreviation = new Intl.DateTimeFormat('en', {
|
|
1295
|
+
timeZoneName: 'shortOffset',
|
|
1296
|
+
})
|
|
1297
|
+
.formatToParts(utcStartTimeDate)
|
|
1298
|
+
.find(part => part.type === 'timeZoneName').value;
|
|
1299
|
+
// adding a breakline between the time and timezone so it looks neater on report
|
|
1300
|
+
const timeColonIndex = formattedStartTime.lastIndexOf(':');
|
|
1301
|
+
const timePart = formattedStartTime.slice(0, timeColonIndex + 3);
|
|
1302
|
+
const timeZonePart = formattedStartTime.slice(timeColonIndex + 4);
|
|
1303
|
+
const htmlFormattedStartTime = `${timePart}<br>${timeZonePart} ${timezoneAbbreviation}`;
|
|
1304
|
+
return htmlFormattedStartTime;
|
|
1305
|
+
};
|
|
1306
|
+
const generateArtifacts = async (randomToken, urlScanned, scanType, viewport, pagesScanned, pagesNotScanned, customFlowLabel, cypressScanAboutMetadata, scanDetails, zip = undefined, // optional
|
|
1307
|
+
generateJsonFiles = false) => {
|
|
1308
|
+
const storagePath = getStoragePath(randomToken);
|
|
1309
|
+
const intermediateDatasetsPath = `${storagePath}/crawlee`;
|
|
1310
|
+
const oobeeAppVersion = getVersion();
|
|
1311
|
+
const isCustomFlow = scanType === ScannerTypes.CUSTOM;
|
|
1312
|
+
const allIssues = {
|
|
1313
|
+
storagePath,
|
|
1314
|
+
oobeeAi: {
|
|
1315
|
+
htmlETL: oobeeAiHtmlETL,
|
|
1316
|
+
rules: oobeeAiRules,
|
|
1317
|
+
},
|
|
1318
|
+
siteName: (pagesScanned[0]?.pageTitle ?? '').replace(/^\d+\s*:\s*/, '').trim(),
|
|
1319
|
+
startTime: scanDetails.startTime ? scanDetails.startTime : new Date(),
|
|
1320
|
+
endTime: scanDetails.endTime ? scanDetails.endTime : new Date(),
|
|
1321
|
+
urlScanned,
|
|
1322
|
+
scanType,
|
|
1323
|
+
deviceChosen: scanDetails.deviceChosen || 'Desktop',
|
|
1324
|
+
formatAboutStartTime,
|
|
1325
|
+
isCustomFlow,
|
|
1326
|
+
viewport,
|
|
1327
|
+
pagesScanned,
|
|
1328
|
+
pagesNotScanned,
|
|
1329
|
+
totalPagesScanned: pagesScanned.length,
|
|
1330
|
+
totalPagesNotScanned: pagesNotScanned.length,
|
|
1331
|
+
totalItems: 0,
|
|
1332
|
+
topFiveMostIssues: [],
|
|
1333
|
+
topTenPagesWithMostIssues: [],
|
|
1334
|
+
topTenIssues: [],
|
|
1335
|
+
wcagViolations: [],
|
|
1336
|
+
customFlowLabel,
|
|
1337
|
+
oobeeAppVersion,
|
|
1338
|
+
items: {
|
|
1339
|
+
mustFix: {
|
|
1340
|
+
description: itemTypeDescription.mustFix,
|
|
1341
|
+
totalItems: 0,
|
|
1342
|
+
totalRuleIssues: 0,
|
|
1343
|
+
rules: [],
|
|
1344
|
+
},
|
|
1345
|
+
goodToFix: {
|
|
1346
|
+
description: itemTypeDescription.goodToFix,
|
|
1347
|
+
totalItems: 0,
|
|
1348
|
+
totalRuleIssues: 0,
|
|
1349
|
+
rules: [],
|
|
1350
|
+
},
|
|
1351
|
+
needsReview: {
|
|
1352
|
+
description: itemTypeDescription.needsReview,
|
|
1353
|
+
totalItems: 0,
|
|
1354
|
+
totalRuleIssues: 0,
|
|
1355
|
+
rules: [],
|
|
1356
|
+
},
|
|
1357
|
+
passed: {
|
|
1358
|
+
description: itemTypeDescription.passed,
|
|
1359
|
+
totalItems: 0,
|
|
1360
|
+
totalRuleIssues: 0,
|
|
1361
|
+
rules: [],
|
|
1362
|
+
},
|
|
1363
|
+
},
|
|
1364
|
+
cypressScanAboutMetadata,
|
|
1365
|
+
wcagLinks: constants.wcagLinks,
|
|
1366
|
+
wcagClauses: WCAGclauses,
|
|
1367
|
+
a11yRuleShortDescriptionMap,
|
|
1368
|
+
disabilityBadgesMap,
|
|
1369
|
+
a11yRuleLongDescriptionMap,
|
|
1370
|
+
wcagCriteriaLabels: constants.wcagCriteriaLabels,
|
|
1371
|
+
scanPagesDetail: {
|
|
1372
|
+
pagesAffected: [],
|
|
1373
|
+
pagesNotAffected: [],
|
|
1374
|
+
scannedPagesCount: 0,
|
|
1375
|
+
pagesNotScanned: [],
|
|
1376
|
+
pagesNotScannedCount: 0,
|
|
1377
|
+
},
|
|
1378
|
+
// Populate boolean values for id="advancedScanOptionsSummary"
|
|
1379
|
+
advancedScanOptionsSummaryItems: {
|
|
1380
|
+
showIncludeScreenshots: [true].includes(scanDetails.isIncludeScreenshots),
|
|
1381
|
+
showAllowSubdomains: ['same-domain'].includes(scanDetails.isAllowSubdomains),
|
|
1382
|
+
showEnableCustomChecks: ['default', 'enable-wcag-aaa'].includes(scanDetails.isEnableCustomChecks?.[0]),
|
|
1383
|
+
showEnableWcagAaa: (scanDetails.isEnableWcagAaa || []).includes('enable-wcag-aaa'),
|
|
1384
|
+
showSlowScanMode: [1].includes(scanDetails.isSlowScanMode),
|
|
1385
|
+
showAdhereRobots: [true].includes(scanDetails.isAdhereRobots),
|
|
1386
|
+
},
|
|
1387
|
+
};
|
|
1388
|
+
const allFiles = await extractFileNames(intermediateDatasetsPath);
|
|
1389
|
+
const jsonArray = await Promise.all(allFiles.map(async (file) => parseContentToJson(`${intermediateDatasetsPath}/${file}`)));
|
|
1390
|
+
await Promise.all(jsonArray.map(async (pageResults) => {
|
|
1391
|
+
await pushResults(pageResults, allIssues, isCustomFlow);
|
|
1392
|
+
})).catch(flattenIssuesError => {
|
|
1393
|
+
consoleLogger.info('An error has occurred when flattening the issues, please try again.');
|
|
1394
|
+
});
|
|
1395
|
+
flattenAndSortResults(allIssues, isCustomFlow);
|
|
1396
|
+
const labelKey = scanType.toLowerCase() === 'custom' ? 'CustomFlowLabel' : 'Label';
|
|
1397
|
+
const labelValue = allIssues.customFlowLabel || 'N/A';
|
|
1398
|
+
printMessage([
|
|
1399
|
+
'Scan Summary',
|
|
1400
|
+
`Oobee App Version: ${allIssues.oobeeAppVersion}`,
|
|
1401
|
+
'',
|
|
1402
|
+
`Site Name: ${allIssues.siteName}`,
|
|
1403
|
+
`URL: ${allIssues.urlScanned}`,
|
|
1404
|
+
`Pages Scanned: ${allIssues.totalPagesScanned}`,
|
|
1405
|
+
`Start Time: ${allIssues.startTime}`,
|
|
1406
|
+
`End Time: ${allIssues.endTime}`,
|
|
1407
|
+
`Elapsed Time: ${(new Date(allIssues.endTime).getTime() - new Date(allIssues.startTime).getTime()) / 1000}s`,
|
|
1408
|
+
`Device: ${allIssues.deviceChosen}`,
|
|
1409
|
+
`Viewport: ${allIssues.viewport}`,
|
|
1410
|
+
`Scan Type: ${allIssues.scanType}`,
|
|
1411
|
+
`${labelKey}: ${labelValue}`,
|
|
1412
|
+
'',
|
|
1413
|
+
`Must Fix: ${allIssues.items.mustFix.rules.length} ${Object.keys(allIssues.items.mustFix.rules).length === 1 ? 'issue' : 'issues'} / ${allIssues.items.mustFix.totalItems} ${allIssues.items.mustFix.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
|
|
1414
|
+
`Good to Fix: ${allIssues.items.goodToFix.rules.length} ${Object.keys(allIssues.items.goodToFix.rules).length === 1 ? 'issue' : 'issues'} / ${allIssues.items.goodToFix.totalItems} ${allIssues.items.goodToFix.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
|
|
1415
|
+
`Manual Review Required: ${allIssues.items.needsReview.rules.length} ${Object.keys(allIssues.items.needsReview.rules).length === 1 ? 'issue' : 'issues'} / ${allIssues.items.needsReview.totalItems} ${allIssues.items.needsReview.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
|
|
1416
|
+
`Passed: ${allIssues.items.passed.totalItems} ${allIssues.items.passed.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
|
|
1417
|
+
]);
|
|
1418
|
+
// move screenshots folder to report folders
|
|
1419
|
+
moveElemScreenshots(randomToken, storagePath);
|
|
1420
|
+
if (isCustomFlow) {
|
|
1421
|
+
createScreenshotsFolder(randomToken);
|
|
1422
|
+
}
|
|
1423
|
+
populateScanPagesDetail(allIssues);
|
|
1424
|
+
allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
|
|
1425
|
+
allIssues.progressPercentage = getProgressPercentage(allIssues.scanPagesDetail, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
|
|
1426
|
+
allIssues.issuesPercentage = await getIssuesPercentage(allIssues.scanPagesDetail, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa, allIssues.advancedScanOptionsSummaryItems.disableOobee);
|
|
1427
|
+
consoleLogger.info(`Site Name: ${allIssues.siteName}`);
|
|
1428
|
+
consoleLogger.info(`URL: ${allIssues.urlScanned}`);
|
|
1429
|
+
consoleLogger.info(`Pages Scanned: ${allIssues.totalPagesScanned}`);
|
|
1430
|
+
consoleLogger.info(`Start Time: ${allIssues.startTime}`);
|
|
1431
|
+
consoleLogger.info(`End Time: ${allIssues.endTime}`);
|
|
1432
|
+
const elapsedSeconds = (new Date(allIssues.endTime).getTime() - new Date(allIssues.startTime).getTime()) / 1000;
|
|
1433
|
+
consoleLogger.info(`Elapsed Time: ${elapsedSeconds}s`);
|
|
1434
|
+
consoleLogger.info(`Device: ${allIssues.deviceChosen}`);
|
|
1435
|
+
consoleLogger.info(`Viewport: ${allIssues.viewport}`);
|
|
1436
|
+
consoleLogger.info(`Scan Type: ${allIssues.scanType}`);
|
|
1437
|
+
consoleLogger.info(`Label: ${allIssues.customFlowLabel || 'N/A'}`);
|
|
1438
|
+
const getAxeImpactCount = (allIssues) => {
|
|
1439
|
+
const impactCount = {
|
|
1440
|
+
critical: 0,
|
|
1441
|
+
serious: 0,
|
|
1442
|
+
moderate: 0,
|
|
1443
|
+
minor: 0,
|
|
1444
|
+
};
|
|
1445
|
+
Object.values(allIssues.items).forEach(category => {
|
|
1446
|
+
if (category.totalItems > 0) {
|
|
1447
|
+
Object.values(category.rules).forEach(rule => {
|
|
1448
|
+
if (rule.axeImpact === 'critical') {
|
|
1449
|
+
impactCount.critical += rule.totalItems;
|
|
1450
|
+
}
|
|
1451
|
+
else if (rule.axeImpact === 'serious') {
|
|
1452
|
+
impactCount.serious += rule.totalItems;
|
|
1453
|
+
}
|
|
1454
|
+
else if (rule.axeImpact === 'moderate') {
|
|
1455
|
+
impactCount.moderate += rule.totalItems;
|
|
1456
|
+
}
|
|
1457
|
+
else if (rule.axeImpact === 'minor') {
|
|
1458
|
+
impactCount.minor += rule.totalItems;
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
return impactCount;
|
|
1464
|
+
};
|
|
1465
|
+
if (process.env.OOBEE_VERBOSE) {
|
|
1466
|
+
const axeImpactCount = getAxeImpactCount(allIssues);
|
|
1467
|
+
const { items, startTime, endTime, ...rest } = allIssues;
|
|
1468
|
+
rest.critical = axeImpactCount.critical;
|
|
1469
|
+
rest.serious = axeImpactCount.serious;
|
|
1470
|
+
rest.moderate = axeImpactCount.moderate;
|
|
1471
|
+
rest.minor = axeImpactCount.minor;
|
|
1472
|
+
}
|
|
1473
|
+
await writeCsv(allIssues, storagePath);
|
|
1474
|
+
await writeSitemap(pagesScanned, storagePath);
|
|
1475
|
+
const { scanDataJsonFilePath, scanDataBase64FilePath, scanItemsJsonFilePath, scanItemsBase64FilePath, scanItemsSummaryJsonFilePath, scanItemsSummaryBase64FilePath, scanItemsMiniReportJsonFilePath, scanItemsMiniReportBase64FilePath, scanIssuesSummaryJsonFilePath, scanIssuesSummaryBase64FilePath, scanPagesDetailJsonFilePath, scanPagesDetailBase64FilePath, scanPagesSummaryJsonFilePath, scanPagesSummaryBase64FilePath, scanDataJsonFileSize, scanItemsJsonFileSize, } = await writeJsonAndBase64Files(allIssues, storagePath);
|
|
1476
|
+
const BIG_RESULTS_THRESHOLD = 500 * 1024 * 1024; // 500 MB
|
|
1477
|
+
const resultsTooBig = scanDataJsonFileSize + scanItemsJsonFileSize > BIG_RESULTS_THRESHOLD;
|
|
1478
|
+
await writeScanDetailsCsv(scanDataBase64FilePath, scanItemsBase64FilePath, scanItemsSummaryBase64FilePath, storagePath);
|
|
1479
|
+
await writeSummaryHTML(allIssues, storagePath);
|
|
1480
|
+
await writeHTML(allIssues, storagePath, 'report', scanDataBase64FilePath, resultsTooBig ? scanItemsMiniReportBase64FilePath : scanItemsBase64FilePath);
|
|
1481
|
+
if (!generateJsonFiles) {
|
|
1482
|
+
await cleanUpJsonFiles([
|
|
1483
|
+
scanDataJsonFilePath,
|
|
1484
|
+
scanDataBase64FilePath,
|
|
1485
|
+
scanItemsJsonFilePath,
|
|
1486
|
+
scanItemsBase64FilePath,
|
|
1487
|
+
scanItemsSummaryJsonFilePath,
|
|
1488
|
+
scanItemsSummaryBase64FilePath,
|
|
1489
|
+
scanItemsMiniReportJsonFilePath,
|
|
1490
|
+
scanItemsMiniReportBase64FilePath,
|
|
1491
|
+
scanIssuesSummaryJsonFilePath,
|
|
1492
|
+
scanIssuesSummaryBase64FilePath,
|
|
1493
|
+
scanPagesDetailJsonFilePath,
|
|
1494
|
+
scanPagesDetailBase64FilePath,
|
|
1495
|
+
scanPagesSummaryJsonFilePath,
|
|
1496
|
+
scanPagesSummaryBase64FilePath,
|
|
1497
|
+
]);
|
|
1498
|
+
}
|
|
1499
|
+
const browserChannel = getBrowserToRun(randomToken, BrowserTypes.CHROME, false).browserToRun;
|
|
1500
|
+
// Should consider refactor constants.userDataDirectory to be a parameter in future
|
|
1501
|
+
await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length, 'summary', browserChannel, constants.userDataDirectory), 1);
|
|
1502
|
+
try {
|
|
1503
|
+
fs.rmSync(path.join(storagePath, 'crawlee'), { recursive: true, force: true });
|
|
1504
|
+
}
|
|
1505
|
+
catch (error) {
|
|
1506
|
+
consoleLogger.warn(`Unable to force remove crawlee folder: ${error.message}`);
|
|
1507
|
+
}
|
|
1508
|
+
try {
|
|
1509
|
+
fs.rmSync(path.join(storagePath, 'pdfs'), { recursive: true, force: true });
|
|
1510
|
+
}
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
consoleLogger.warn(`Unable to force remove pdfs folder: ${error.message}`);
|
|
1513
|
+
}
|
|
1514
|
+
// Take option if set
|
|
1515
|
+
if (typeof zip === 'string') {
|
|
1516
|
+
constants.cliZipFileName = zip;
|
|
1517
|
+
if (!zip.endsWith('.zip')) {
|
|
1518
|
+
constants.cliZipFileName += '.zip';
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (!path.isAbsolute(constants.cliZipFileName) ||
|
|
1522
|
+
path.dirname(constants.cliZipFileName) === '.') {
|
|
1523
|
+
constants.cliZipFileName = path.join(storagePath, constants.cliZipFileName);
|
|
1524
|
+
}
|
|
1525
|
+
try {
|
|
1526
|
+
await fs.ensureDir(storagePath);
|
|
1527
|
+
await zipResults(constants.cliZipFileName, storagePath);
|
|
1528
|
+
const messageToDisplay = [
|
|
1529
|
+
`Report of this run is at ${constants.cliZipFileName}`,
|
|
1530
|
+
`Results directory is at ${storagePath}`,
|
|
1531
|
+
];
|
|
1532
|
+
if (process.send && process.env.OOBEE_VERBOSE) {
|
|
1533
|
+
const zipFileNameMessage = {
|
|
1534
|
+
type: 'zipFileName',
|
|
1535
|
+
payload: `${constants.cliZipFileName}`,
|
|
1536
|
+
};
|
|
1537
|
+
const storagePathMessage = {
|
|
1538
|
+
type: 'storagePath',
|
|
1539
|
+
payload: `${storagePath}`,
|
|
1540
|
+
};
|
|
1541
|
+
process.send(JSON.stringify(storagePathMessage));
|
|
1542
|
+
process.send(JSON.stringify(zipFileNameMessage));
|
|
1543
|
+
}
|
|
1544
|
+
printMessage(messageToDisplay);
|
|
1545
|
+
}
|
|
1546
|
+
catch (error) {
|
|
1547
|
+
printMessage([`Error in zipping results: ${error}`]);
|
|
1548
|
+
}
|
|
1549
|
+
// Generate scrubbed HTML Code Snippets
|
|
1550
|
+
const ruleIdJson = createRuleIdJson(allIssues);
|
|
1551
|
+
// At the end of the function where results are generated, add:
|
|
1552
|
+
try {
|
|
1553
|
+
// Always send WCAG breakdown to Sentry, even if no violations were found
|
|
1554
|
+
// This ensures that all criteria are reported, including those with 0 occurrences
|
|
1555
|
+
await sendWcagBreakdownToSentry(oobeeAppVersion, wcagOccurrencesMap, ruleIdJson, {
|
|
1556
|
+
entryUrl: urlScanned,
|
|
1557
|
+
scanType,
|
|
1558
|
+
browser: scanDetails.deviceChosen,
|
|
1559
|
+
email: scanDetails.nameEmail?.email,
|
|
1560
|
+
name: scanDetails.nameEmail?.name,
|
|
1561
|
+
}, allIssues, pagesScanned.length);
|
|
1562
|
+
}
|
|
1563
|
+
catch (error) {
|
|
1564
|
+
console.error('Error sending WCAG data to Sentry:', error);
|
|
1565
|
+
}
|
|
1566
|
+
if (process.env.RUNNING_FROM_PH_GUI || process.env.OOBEE_VERBOSE)
|
|
1567
|
+
console.log('Report generated successfully');
|
|
1568
|
+
return ruleIdJson;
|
|
1569
|
+
};
|
|
1570
|
+
export { writeHTML, compressJsonFileStreaming, flattenAndSortResults, populateScanPagesDetail, getWcagPassPercentage, getProgressPercentage, getIssuesPercentage, itemTypeDescription, oobeeAiHtmlETL, oobeeAiRules, formatAboutStartTime, };
|
|
1571
|
+
export default generateArtifacts;
|