@govtechsg/oobee 0.10.20
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/.dockerignore +22 -0
- package/.github/pull_request_template.md +11 -0
- package/.github/workflows/docker-test.yml +54 -0
- package/.github/workflows/image.yml +107 -0
- package/.github/workflows/publish.yml +18 -0
- package/.idea/modules.xml +8 -0
- package/.idea/purple-a11y.iml +9 -0
- package/.idea/vcs.xml +6 -0
- package/.prettierrc.json +12 -0
- package/.vscode/extensions.json +5 -0
- package/.vscode/settings.json +10 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/DETAILS.md +163 -0
- package/Dockerfile +60 -0
- package/INSTALLATION.md +146 -0
- package/INTEGRATION.md +785 -0
- package/LICENSE +22 -0
- package/README.md +587 -0
- package/SECURITY.md +5 -0
- package/__mocks__/mock-report.html +1431 -0
- package/__mocks__/mockFunctions.ts +32 -0
- package/__mocks__/mockIssues.ts +64 -0
- package/__mocks__/mock_all_issues/000000001.json +64 -0
- package/__mocks__/mock_all_issues/000000002.json +53 -0
- package/__mocks__/mock_all_issues/fake-file.txt +0 -0
- package/__tests__/logs.test.ts +25 -0
- package/__tests__/mergeAxeResults.test.ts +278 -0
- package/__tests__/utils.test.ts +118 -0
- package/a11y-scan-results.zip +0 -0
- package/eslint.config.js +53 -0
- package/exclusions.txt +2 -0
- package/gitlab-pipeline-template.yml +54 -0
- package/jest.config.js +1 -0
- package/package.json +96 -0
- package/scripts/copyFiles.js +44 -0
- package/scripts/install_oobee_dependencies.cmd +13 -0
- package/scripts/install_oobee_dependencies.command +101 -0
- package/scripts/install_oobee_dependencies.ps1 +110 -0
- package/scripts/oobee_shell.cmd +13 -0
- package/scripts/oobee_shell.command +11 -0
- package/scripts/oobee_shell.sh +55 -0
- package/scripts/oobee_shell_ps.ps1 +54 -0
- package/src/cli.ts +401 -0
- package/src/combine.ts +240 -0
- package/src/constants/__tests__/common.test.ts +44 -0
- package/src/constants/cliFunctions.ts +305 -0
- package/src/constants/common.ts +1840 -0
- package/src/constants/constants.ts +443 -0
- package/src/constants/errorMeta.json +319 -0
- package/src/constants/itemTypeDescription.ts +11 -0
- package/src/constants/oobeeAi.ts +141 -0
- package/src/constants/questions.ts +181 -0
- package/src/constants/sampleData.ts +187 -0
- package/src/crawlers/__tests__/commonCrawlerFunc.test.ts +51 -0
- package/src/crawlers/commonCrawlerFunc.ts +656 -0
- package/src/crawlers/crawlDomain.ts +877 -0
- package/src/crawlers/crawlIntelligentSitemap.ts +156 -0
- package/src/crawlers/crawlLocalFile.ts +193 -0
- package/src/crawlers/crawlSitemap.ts +356 -0
- package/src/crawlers/custom/extractAndGradeText.ts +57 -0
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +964 -0
- package/src/crawlers/custom/utils.ts +486 -0
- package/src/crawlers/customAxeFunctions.ts +82 -0
- package/src/crawlers/pdfScanFunc.ts +468 -0
- package/src/crawlers/runCustom.ts +117 -0
- package/src/index.ts +173 -0
- package/src/logs.ts +66 -0
- package/src/mergeAxeResults.ts +964 -0
- package/src/npmIndex.ts +284 -0
- package/src/screenshotFunc/htmlScreenshotFunc.ts +411 -0
- package/src/screenshotFunc/pdfScreenshotFunc.ts +762 -0
- package/src/static/ejs/partials/components/categorySelector.ejs +4 -0
- package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +57 -0
- package/src/static/ejs/partials/components/pagesScannedModal.ejs +70 -0
- package/src/static/ejs/partials/components/reportSearch.ejs +47 -0
- package/src/static/ejs/partials/components/ruleOffcanvas.ejs +105 -0
- package/src/static/ejs/partials/components/scanAbout.ejs +263 -0
- package/src/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
- package/src/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
- package/src/static/ejs/partials/components/summaryScanResults.ejs +16 -0
- package/src/static/ejs/partials/components/summaryTable.ejs +20 -0
- package/src/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
- package/src/static/ejs/partials/components/topFive.ejs +6 -0
- package/src/static/ejs/partials/components/wcagCompliance.ejs +70 -0
- package/src/static/ejs/partials/footer.ejs +21 -0
- package/src/static/ejs/partials/header.ejs +230 -0
- package/src/static/ejs/partials/main.ejs +40 -0
- package/src/static/ejs/partials/scripts/bootstrap.ejs +8 -0
- package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +190 -0
- package/src/static/ejs/partials/scripts/categorySummary.ejs +141 -0
- package/src/static/ejs/partials/scripts/highlightjs.ejs +335 -0
- package/src/static/ejs/partials/scripts/popper.ejs +7 -0
- package/src/static/ejs/partials/scripts/reportSearch.ejs +248 -0
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +801 -0
- package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +71 -0
- package/src/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
- package/src/static/ejs/partials/scripts/summaryTable.ejs +78 -0
- package/src/static/ejs/partials/scripts/utils.ejs +441 -0
- package/src/static/ejs/partials/styles/bootstrap.ejs +12375 -0
- package/src/static/ejs/partials/styles/highlightjs.ejs +54 -0
- package/src/static/ejs/partials/styles/styles.ejs +1843 -0
- package/src/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
- package/src/static/ejs/partials/summaryHeader.ejs +70 -0
- package/src/static/ejs/partials/summaryMain.ejs +75 -0
- package/src/static/ejs/report.ejs +420 -0
- package/src/static/ejs/summary.ejs +47 -0
- package/src/static/mustache/.prettierrc +4 -0
- package/src/static/mustache/Attention Deficit.mustache +11 -0
- package/src/static/mustache/Blind.mustache +11 -0
- package/src/static/mustache/Cognitive.mustache +7 -0
- package/src/static/mustache/Colorblindness.mustache +20 -0
- package/src/static/mustache/Deaf.mustache +12 -0
- package/src/static/mustache/Deafblind.mustache +7 -0
- package/src/static/mustache/Dyslexia.mustache +14 -0
- package/src/static/mustache/Low Vision.mustache +7 -0
- package/src/static/mustache/Mobility.mustache +15 -0
- package/src/static/mustache/Sighted Keyboard Users.mustache +42 -0
- package/src/static/mustache/report.mustache +1709 -0
- package/src/types/print-message.d.ts +28 -0
- package/src/types/types.ts +46 -0
- package/src/types/xpath-to-css.d.ts +3 -0
- package/src/utils.ts +332 -0
- package/tsconfig.json +15 -0
@@ -0,0 +1,964 @@
|
|
1
|
+
/* eslint-disable consistent-return */
|
2
|
+
/* eslint-disable no-console */
|
3
|
+
import os from 'os';
|
4
|
+
import fs, { ensureDirSync } from 'fs-extra';
|
5
|
+
import printMessage from 'print-message';
|
6
|
+
import path from 'path';
|
7
|
+
import ejs from 'ejs';
|
8
|
+
import { fileURLToPath } from 'url';
|
9
|
+
import { chromium } from 'playwright';
|
10
|
+
import { createWriteStream } from 'fs';
|
11
|
+
import { AsyncParser, ParserOptions } from '@json2csv/node';
|
12
|
+
import { v4 as uuidv4 } from 'uuid';
|
13
|
+
import constants, { ScannerTypes } from './constants/constants.js';
|
14
|
+
import { urlWithoutAuth } from './constants/common.js';
|
15
|
+
import {
|
16
|
+
createScreenshotsFolder,
|
17
|
+
getStoragePath,
|
18
|
+
getVersion,
|
19
|
+
getWcagPassPercentage,
|
20
|
+
retryFunction,
|
21
|
+
zipResults,
|
22
|
+
} from './utils.js';
|
23
|
+
import { consoleLogger, silentLogger } from './logs.js';
|
24
|
+
import itemTypeDescription from './constants/itemTypeDescription.js';
|
25
|
+
import { oobeeAiHtmlETL, oobeeAiRules } from './constants/oobeeAi.js';
|
26
|
+
|
27
|
+
const cwd = process.cwd();
|
28
|
+
|
29
|
+
export type ItemsInfo = {
|
30
|
+
html: string;
|
31
|
+
message: string;
|
32
|
+
screenshotPath: string;
|
33
|
+
xpath: string;
|
34
|
+
displayNeedsReview?: boolean;
|
35
|
+
};
|
36
|
+
|
37
|
+
type PageInfo = {
|
38
|
+
items: ItemsInfo[];
|
39
|
+
pageTitle: string;
|
40
|
+
url?: string;
|
41
|
+
pageImagePath?: string;
|
42
|
+
pageIndex?: number;
|
43
|
+
metadata: string;
|
44
|
+
};
|
45
|
+
|
46
|
+
export type RuleInfo = {
|
47
|
+
totalItems: number;
|
48
|
+
pagesAffected: PageInfo[];
|
49
|
+
rule: string;
|
50
|
+
description: string;
|
51
|
+
axeImpact: string;
|
52
|
+
conformance: string[];
|
53
|
+
helpUrl: string;
|
54
|
+
};
|
55
|
+
|
56
|
+
type AllIssues = {
|
57
|
+
storagePath: string;
|
58
|
+
oobeeAi: {
|
59
|
+
htmlETL: any;
|
60
|
+
rules: string[];
|
61
|
+
};
|
62
|
+
startTime: Date;
|
63
|
+
endTime: Date;
|
64
|
+
urlScanned: string;
|
65
|
+
scanType: string;
|
66
|
+
formatAboutStartTime: (dateString: any) => string;
|
67
|
+
isCustomFlow: boolean;
|
68
|
+
viewport: string;
|
69
|
+
pagesScanned: PageInfo[];
|
70
|
+
pagesNotScanned: PageInfo[];
|
71
|
+
totalPagesScanned: number;
|
72
|
+
totalPagesNotScanned: number;
|
73
|
+
totalItems: number;
|
74
|
+
topFiveMostIssues: Array<any>;
|
75
|
+
wcagViolations: string[];
|
76
|
+
customFlowLabel: string;
|
77
|
+
phAppVersion: string;
|
78
|
+
items: {
|
79
|
+
mustFix: { description: string; totalItems: number; rules: RuleInfo[] };
|
80
|
+
goodToFix: { description: string; totalItems: number; rules: RuleInfo[] };
|
81
|
+
needsReview: { description: string; totalItems: number; rules: RuleInfo[] };
|
82
|
+
passed: { description: string; totalItems: number; rules: RuleInfo[] };
|
83
|
+
};
|
84
|
+
cypressScanAboutMetadata: string;
|
85
|
+
wcagLinks: { [key: string]: string };
|
86
|
+
[key: string]: any;
|
87
|
+
};
|
88
|
+
|
89
|
+
const filename = fileURLToPath(import.meta.url);
|
90
|
+
const dirname = path.dirname(filename);
|
91
|
+
|
92
|
+
const extractFileNames = async (directory: string): Promise<string[]> => {
|
93
|
+
ensureDirSync(directory);
|
94
|
+
|
95
|
+
return fs
|
96
|
+
.readdir(directory)
|
97
|
+
.then(allFiles => allFiles.filter(file => path.extname(file).toLowerCase() === '.json'))
|
98
|
+
.catch(readdirError => {
|
99
|
+
consoleLogger.info('An error has occurred when retrieving files, please try again.');
|
100
|
+
silentLogger.error(`(extractFileNames) - ${readdirError}`);
|
101
|
+
throw readdirError;
|
102
|
+
});
|
103
|
+
};
|
104
|
+
const parseContentToJson = async rPath =>
|
105
|
+
fs
|
106
|
+
.readFile(rPath, 'utf8')
|
107
|
+
.then(content => JSON.parse(content))
|
108
|
+
.catch(parseError => {
|
109
|
+
consoleLogger.info('An error has occurred when parsing the content, please try again.');
|
110
|
+
silentLogger.error(`(parseContentToJson) - ${parseError}`);
|
111
|
+
});
|
112
|
+
|
113
|
+
const writeCsv = async (allIssues, storagePath) => {
|
114
|
+
const csvOutput = createWriteStream(`${storagePath}/report.csv`, { encoding: 'utf8' });
|
115
|
+
const formatPageViolation = pageNum => {
|
116
|
+
if (pageNum < 0) return 'Document';
|
117
|
+
return `Page ${pageNum}`;
|
118
|
+
};
|
119
|
+
|
120
|
+
// transform allIssues into the form:
|
121
|
+
// [['mustFix', rule1], ['mustFix', rule2], ['goodToFix', rule3], ...]
|
122
|
+
const getRulesByCategory = (allIssues: AllIssues) => {
|
123
|
+
return Object.entries(allIssues.items)
|
124
|
+
.filter(([category]) => category !== 'passed')
|
125
|
+
.reduce((prev: [string, RuleInfo][], [category, value]) => {
|
126
|
+
const rulesEntries = Object.entries(value.rules);
|
127
|
+
rulesEntries.forEach(([, ruleInfo]) => {
|
128
|
+
prev.push([category, ruleInfo]);
|
129
|
+
});
|
130
|
+
return prev;
|
131
|
+
}, [])
|
132
|
+
.sort((a, b) => {
|
133
|
+
// sort rules according to severity, then ruleId
|
134
|
+
const compareCategory = -a[0].localeCompare(b[0]);
|
135
|
+
return compareCategory === 0 ? a[1].rule.localeCompare(b[1].rule) : compareCategory;
|
136
|
+
});
|
137
|
+
};
|
138
|
+
// seems to go into
|
139
|
+
const flattenRule = catAndRule => {
|
140
|
+
const [severity, rule] = catAndRule;
|
141
|
+
const results = [];
|
142
|
+
const {
|
143
|
+
rule: issueId,
|
144
|
+
description: issueDescription,
|
145
|
+
axeImpact,
|
146
|
+
conformance,
|
147
|
+
pagesAffected,
|
148
|
+
helpUrl: learnMore,
|
149
|
+
} = rule;
|
150
|
+
// we filter out the below as it represents the A/AA/AAA level, not the clause itself
|
151
|
+
const clausesArr = conformance.filter(
|
152
|
+
clause => !['wcag2a', 'wcag2aa', 'wcag2aaa'].includes(clause),
|
153
|
+
);
|
154
|
+
pagesAffected.sort((a, b) => a.url.localeCompare(b.url));
|
155
|
+
// format clauses as a string
|
156
|
+
const wcagConformance = clausesArr.join(',');
|
157
|
+
pagesAffected.forEach(affectedPage => {
|
158
|
+
const { url, items } = affectedPage;
|
159
|
+
items.forEach(item => {
|
160
|
+
const { html, page, message, xpath } = item;
|
161
|
+
const howToFix = message.replace(/(\r\n|\n|\r)/g, ' '); // remove newlines
|
162
|
+
const violation = html || formatPageViolation(page); // page is a number, not a string
|
163
|
+
const context = violation.replace(/(\r\n|\n|\r)/g, ''); // remove newlines
|
164
|
+
|
165
|
+
results.push({
|
166
|
+
severity,
|
167
|
+
issueId,
|
168
|
+
issueDescription,
|
169
|
+
wcagConformance,
|
170
|
+
url,
|
171
|
+
context,
|
172
|
+
howToFix,
|
173
|
+
axeImpact,
|
174
|
+
xpath,
|
175
|
+
learnMore,
|
176
|
+
});
|
177
|
+
});
|
178
|
+
});
|
179
|
+
if (results.length === 0) return {};
|
180
|
+
return results;
|
181
|
+
};
|
182
|
+
const opts: ParserOptions<any, any> = {
|
183
|
+
transforms: [getRulesByCategory, flattenRule],
|
184
|
+
fields: [
|
185
|
+
'severity',
|
186
|
+
'issueId',
|
187
|
+
'issueDescription',
|
188
|
+
'wcagConformance',
|
189
|
+
'url',
|
190
|
+
'context',
|
191
|
+
'howToFix',
|
192
|
+
'axeImpact',
|
193
|
+
'xpath',
|
194
|
+
'learnMore',
|
195
|
+
],
|
196
|
+
includeEmptyRows: true,
|
197
|
+
};
|
198
|
+
const parser = new AsyncParser(opts);
|
199
|
+
parser.parse(allIssues).pipe(csvOutput);
|
200
|
+
};
|
201
|
+
|
202
|
+
const compileHtmlWithEJS = async (allIssues, storagePath, htmlFilename = 'report') => {
|
203
|
+
const htmlFilePath = `${path.join(storagePath, htmlFilename)}.html`;
|
204
|
+
const ejsString = fs.readFileSync(path.join(dirname, './static/ejs/report.ejs'), 'utf-8');
|
205
|
+
const template = ejs.compile(ejsString, {
|
206
|
+
filename: path.join(dirname, './static/ejs/report.ejs'),
|
207
|
+
});
|
208
|
+
const html = template(allIssues);
|
209
|
+
await fs.writeFile(htmlFilePath, html);
|
210
|
+
|
211
|
+
let htmlContent = await fs.readFile(htmlFilePath, { encoding: 'utf8' });
|
212
|
+
|
213
|
+
const headIndex = htmlContent.indexOf('</head>');
|
214
|
+
const injectScript = `
|
215
|
+
<script>
|
216
|
+
try {
|
217
|
+
const base64DecodeChunkedWithDecoder = (data, chunkSize = 1024 * 1024) => {
|
218
|
+
const encodedChunks = data.split('.');
|
219
|
+
const decoder = new TextDecoder();
|
220
|
+
const jsonParts = [];
|
221
|
+
|
222
|
+
encodedChunks.forEach(chunk => {
|
223
|
+
for (let i = 0; i < chunk.length; i += chunkSize) {
|
224
|
+
const chunkPart = chunk.slice(i, i + chunkSize);
|
225
|
+
const decodedBytes = Uint8Array.from(atob(chunkPart), c => c.charCodeAt(0));
|
226
|
+
jsonParts.push(decoder.decode(decodedBytes, { stream: true }));
|
227
|
+
}
|
228
|
+
});
|
229
|
+
|
230
|
+
return JSON.parse(jsonParts.join(''));
|
231
|
+
|
232
|
+
};
|
233
|
+
|
234
|
+
// IMPORTANT! DO NOT REMOVE ME: Decode the encoded data
|
235
|
+
} catch (error) {
|
236
|
+
console.error("Error decoding base64 data:", error);
|
237
|
+
}
|
238
|
+
</script>
|
239
|
+
`;
|
240
|
+
|
241
|
+
if (headIndex !== -1) {
|
242
|
+
htmlContent = htmlContent.slice(0, headIndex) + injectScript + htmlContent.slice(headIndex);
|
243
|
+
} else {
|
244
|
+
htmlContent += injectScript;
|
245
|
+
}
|
246
|
+
|
247
|
+
await fs.writeFile(htmlFilePath, htmlContent);
|
248
|
+
|
249
|
+
return htmlFilePath;
|
250
|
+
};
|
251
|
+
|
252
|
+
const splitHtmlAndCreateFiles = async (htmlFilePath, storagePath) => {
|
253
|
+
try {
|
254
|
+
const htmlContent = await fs.readFile(htmlFilePath, { encoding: 'utf8' });
|
255
|
+
const splitMarker = '// IMPORTANT! DO NOT REMOVE ME: Decode the encoded data';
|
256
|
+
const splitIndex = htmlContent.indexOf(splitMarker);
|
257
|
+
|
258
|
+
if (splitIndex === -1) {
|
259
|
+
throw new Error('Marker comment not found in the HTML file.');
|
260
|
+
}
|
261
|
+
|
262
|
+
const topContent = htmlContent.slice(0, splitIndex + splitMarker.length) + '\n\n';
|
263
|
+
const bottomContent = htmlContent.slice(splitIndex + splitMarker.length);
|
264
|
+
|
265
|
+
const topFilePath = path.join(storagePath, 'report-partial-top.htm.txt');
|
266
|
+
const bottomFilePath = path.join(storagePath, 'report-partial-bottom.htm.txt');
|
267
|
+
|
268
|
+
await fs.writeFile(topFilePath, topContent, { encoding: 'utf8' });
|
269
|
+
await fs.writeFile(bottomFilePath, bottomContent, { encoding: 'utf8' });
|
270
|
+
|
271
|
+
await fs.unlink(htmlFilePath);
|
272
|
+
|
273
|
+
return { topFilePath, bottomFilePath };
|
274
|
+
} catch (error) {
|
275
|
+
console.error('Error splitting HTML and creating files:', error);
|
276
|
+
}
|
277
|
+
};
|
278
|
+
|
279
|
+
const writeHTML = async (allIssues, storagePath, htmlFilename = 'report') => {
|
280
|
+
const htmlFilePath = await compileHtmlWithEJS(allIssues, storagePath, htmlFilename);
|
281
|
+
const inputFilePath = path.resolve(storagePath, 'scanDetails.csv');
|
282
|
+
const outputFilePath = `${storagePath}/${htmlFilename}.html`;
|
283
|
+
|
284
|
+
const { topFilePath, bottomFilePath } = await splitHtmlAndCreateFiles(htmlFilePath, storagePath);
|
285
|
+
|
286
|
+
const prefixData = fs.readFileSync(path.join(storagePath, 'report-partial-top.htm.txt'), 'utf-8');
|
287
|
+
const suffixData = fs.readFileSync(
|
288
|
+
path.join(storagePath, 'report-partial-bottom.htm.txt'),
|
289
|
+
'utf-8',
|
290
|
+
);
|
291
|
+
|
292
|
+
const outputStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
|
293
|
+
|
294
|
+
outputStream.write(prefixData);
|
295
|
+
|
296
|
+
// Create a readable stream for the input file with a highWaterMark set to 10MB
|
297
|
+
const BUFFER_LIMIT = 10 * 1024 * 1024; // 10 MB
|
298
|
+
const inputStream = fs.createReadStream(inputFilePath, {
|
299
|
+
encoding: 'utf-8',
|
300
|
+
highWaterMark: BUFFER_LIMIT,
|
301
|
+
});
|
302
|
+
|
303
|
+
let isFirstLine = true;
|
304
|
+
let lineEndingDetected = false;
|
305
|
+
let isFirstField = true;
|
306
|
+
let isWritingFirstDataLine = true;
|
307
|
+
let buffer = '';
|
308
|
+
|
309
|
+
function flushBuffer() {
|
310
|
+
if (buffer.length > 0) {
|
311
|
+
outputStream.write(buffer);
|
312
|
+
buffer = '';
|
313
|
+
}
|
314
|
+
}
|
315
|
+
|
316
|
+
const cleanupFiles = async () => {
|
317
|
+
try {
|
318
|
+
await Promise.all([fs.promises.unlink(topFilePath), fs.promises.unlink(bottomFilePath)]);
|
319
|
+
} catch (err) {
|
320
|
+
console.error('Error cleaning up temporary files:', err);
|
321
|
+
}
|
322
|
+
};
|
323
|
+
|
324
|
+
inputStream.on('data', chunk => {
|
325
|
+
let chunkIndex = 0;
|
326
|
+
|
327
|
+
while (chunkIndex < chunk.length) {
|
328
|
+
const char = chunk[chunkIndex];
|
329
|
+
|
330
|
+
if (isFirstLine) {
|
331
|
+
if (char === '\n' || char === '\r') {
|
332
|
+
lineEndingDetected = true;
|
333
|
+
} else if (lineEndingDetected) {
|
334
|
+
if (char !== '\n' && char !== '\r') {
|
335
|
+
isFirstLine = false;
|
336
|
+
|
337
|
+
if (isWritingFirstDataLine) {
|
338
|
+
buffer += "scanData = base64DecodeChunkedWithDecoder('";
|
339
|
+
isWritingFirstDataLine = false;
|
340
|
+
}
|
341
|
+
buffer += char;
|
342
|
+
}
|
343
|
+
lineEndingDetected = false;
|
344
|
+
}
|
345
|
+
} else {
|
346
|
+
if (char === ',') {
|
347
|
+
buffer += "')\n\n";
|
348
|
+
buffer += "scanItems = base64DecodeChunkedWithDecoder('";
|
349
|
+
isFirstField = false;
|
350
|
+
} else if (char === '\n' || char === '\r') {
|
351
|
+
if (!isFirstField) {
|
352
|
+
buffer += "')\n";
|
353
|
+
}
|
354
|
+
} else {
|
355
|
+
buffer += char;
|
356
|
+
}
|
357
|
+
|
358
|
+
if (buffer.length >= BUFFER_LIMIT) {
|
359
|
+
flushBuffer();
|
360
|
+
}
|
361
|
+
}
|
362
|
+
|
363
|
+
chunkIndex++;
|
364
|
+
}
|
365
|
+
});
|
366
|
+
|
367
|
+
inputStream.on('end', async () => {
|
368
|
+
if (!isFirstField) {
|
369
|
+
buffer += "')\n";
|
370
|
+
}
|
371
|
+
flushBuffer();
|
372
|
+
|
373
|
+
outputStream.write(suffixData);
|
374
|
+
outputStream.end();
|
375
|
+
console.log('Content appended successfully.');
|
376
|
+
|
377
|
+
await cleanupFiles();
|
378
|
+
});
|
379
|
+
|
380
|
+
inputStream.on('error', async err => {
|
381
|
+
console.error('Error reading input file:', err);
|
382
|
+
outputStream.end();
|
383
|
+
|
384
|
+
await cleanupFiles();
|
385
|
+
});
|
386
|
+
|
387
|
+
outputStream.on('error', err => {
|
388
|
+
console.error('Error writing to output file:', err);
|
389
|
+
});
|
390
|
+
};
|
391
|
+
|
392
|
+
const writeSummaryHTML = async (allIssues, storagePath, htmlFilename = 'summary') => {
|
393
|
+
const ejsString = fs.readFileSync(path.join(dirname, './static/ejs/summary.ejs'), 'utf-8');
|
394
|
+
const template = ejs.compile(ejsString, {
|
395
|
+
filename: path.join(dirname, './static/ejs/summary.ejs'),
|
396
|
+
});
|
397
|
+
const html = template(allIssues);
|
398
|
+
fs.writeFileSync(`${storagePath}/${htmlFilename}.html`, html);
|
399
|
+
};
|
400
|
+
|
401
|
+
function writeFormattedValue(value, writeStream) {
|
402
|
+
if (typeof value === 'function') {
|
403
|
+
writeStream.write('null');
|
404
|
+
} else if (value === undefined) {
|
405
|
+
writeStream.write('null');
|
406
|
+
} else if (typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number') {
|
407
|
+
writeStream.write(JSON.stringify(value));
|
408
|
+
} else if (value === null) {
|
409
|
+
writeStream.write('null');
|
410
|
+
}
|
411
|
+
}
|
412
|
+
|
413
|
+
function serializeObject(obj, writeStream, depth = 0, indent = ' ') {
|
414
|
+
const currentIndent = indent.repeat(depth);
|
415
|
+
const nextIndent = indent.repeat(depth + 1);
|
416
|
+
|
417
|
+
if (obj instanceof Date) {
|
418
|
+
writeStream.write(JSON.stringify(obj.toISOString()));
|
419
|
+
} else if (Array.isArray(obj)) {
|
420
|
+
writeStream.write('[\n');
|
421
|
+
obj.forEach((item, index) => {
|
422
|
+
if (index > 0) writeStream.write(',\n');
|
423
|
+
writeStream.write(nextIndent);
|
424
|
+
serializeObject(item, writeStream, depth + 1, indent);
|
425
|
+
});
|
426
|
+
writeStream.write(`\n${currentIndent}]`);
|
427
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
428
|
+
writeStream.write('{\n');
|
429
|
+
const keys = Object.keys(obj);
|
430
|
+
keys.forEach((key, index) => {
|
431
|
+
if (index > 0) writeStream.write(',\n');
|
432
|
+
writeStream.write(`${nextIndent}${JSON.stringify(key)}: `);
|
433
|
+
serializeObject(obj[key], writeStream, depth + 1, indent);
|
434
|
+
});
|
435
|
+
writeStream.write(`\n${currentIndent}}`);
|
436
|
+
} else {
|
437
|
+
writeFormattedValue(obj, writeStream);
|
438
|
+
}
|
439
|
+
}
|
440
|
+
|
441
|
+
function writeLargeJsonToFile(obj, filePath) {
|
442
|
+
return new Promise((resolve, reject) => {
|
443
|
+
const writeStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
444
|
+
|
445
|
+
writeStream.on('error', error => {
|
446
|
+
consoleLogger.error('Stream error:', error);
|
447
|
+
reject(error);
|
448
|
+
});
|
449
|
+
|
450
|
+
writeStream.on('finish', () => {
|
451
|
+
consoleLogger.info('Temporary file written successfully:', filePath);
|
452
|
+
resolve(true);
|
453
|
+
});
|
454
|
+
|
455
|
+
serializeObject(obj, writeStream);
|
456
|
+
writeStream.end();
|
457
|
+
});
|
458
|
+
}
|
459
|
+
|
460
|
+
const base64Encode = async (data, num) => {
|
461
|
+
try {
|
462
|
+
const tempFilename =
|
463
|
+
num === 1
|
464
|
+
? `scanItems_${uuidv4()}.json`
|
465
|
+
: num === 2
|
466
|
+
? `scanData_${uuidv4()}.json`
|
467
|
+
: `${uuidv4()}.json`;
|
468
|
+
const tempFilePath = path.join(process.cwd(), tempFilename);
|
469
|
+
|
470
|
+
await writeLargeJsonToFile(data, tempFilePath);
|
471
|
+
|
472
|
+
const outputFilename = `encoded_${uuidv4()}.txt`;
|
473
|
+
const outputFilePath = path.join(process.cwd(), outputFilename);
|
474
|
+
|
475
|
+
try {
|
476
|
+
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
|
477
|
+
const readStream = fs.createReadStream(tempFilePath, {
|
478
|
+
encoding: 'utf8',
|
479
|
+
highWaterMark: CHUNK_SIZE,
|
480
|
+
});
|
481
|
+
const writeStream = fs.createWriteStream(outputFilePath, { encoding: 'utf8' });
|
482
|
+
|
483
|
+
for await (const chunk of readStream) {
|
484
|
+
const encodedChunk = Buffer.from(chunk).toString('base64');
|
485
|
+
writeStream.write(`${encodedChunk}.`);
|
486
|
+
}
|
487
|
+
|
488
|
+
await new Promise((resolve, reject) => {
|
489
|
+
writeStream.end(resolve);
|
490
|
+
writeStream.on('error', reject);
|
491
|
+
});
|
492
|
+
|
493
|
+
return outputFilePath;
|
494
|
+
} finally {
|
495
|
+
await fs.promises
|
496
|
+
.unlink(tempFilePath)
|
497
|
+
.catch(err => console.error('Temp file delete error:', err));
|
498
|
+
}
|
499
|
+
} catch (error) {
|
500
|
+
console.error('Error encoding data to Base64:', error);
|
501
|
+
throw error;
|
502
|
+
}
|
503
|
+
};
|
504
|
+
|
505
|
+
const streamEncodedDataToFile = async (inputFilePath, writeStream, appendComma) => {
|
506
|
+
const readStream = fs.createReadStream(inputFilePath, { encoding: 'utf8' });
|
507
|
+
let isFirstChunk = true;
|
508
|
+
|
509
|
+
for await (const chunk of readStream) {
|
510
|
+
if (isFirstChunk) {
|
511
|
+
isFirstChunk = false;
|
512
|
+
writeStream.write(chunk);
|
513
|
+
} else {
|
514
|
+
writeStream.write(chunk);
|
515
|
+
}
|
516
|
+
}
|
517
|
+
|
518
|
+
if (appendComma) {
|
519
|
+
writeStream.write(',');
|
520
|
+
}
|
521
|
+
};
|
522
|
+
|
523
|
+
const writeBase64 = async (allIssues, storagePath) => {
|
524
|
+
const { items, ...rest } = allIssues;
|
525
|
+
const encodedScanItemsPath = await base64Encode(items, 1);
|
526
|
+
const encodedScanDataPath = await base64Encode(rest, 2);
|
527
|
+
|
528
|
+
const filePath = path.join(storagePath, 'scanDetails.csv');
|
529
|
+
const directoryPath = path.dirname(filePath);
|
530
|
+
|
531
|
+
if (!fs.existsSync(directoryPath)) {
|
532
|
+
fs.mkdirSync(directoryPath, { recursive: true });
|
533
|
+
}
|
534
|
+
|
535
|
+
const csvWriteStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
536
|
+
|
537
|
+
csvWriteStream.write('scanData_base64,scanItems_base64\n');
|
538
|
+
await streamEncodedDataToFile(encodedScanDataPath, csvWriteStream, true);
|
539
|
+
await streamEncodedDataToFile(encodedScanItemsPath, csvWriteStream, false);
|
540
|
+
|
541
|
+
await new Promise((resolve, reject) => {
|
542
|
+
csvWriteStream.end(resolve);
|
543
|
+
csvWriteStream.on('error', reject);
|
544
|
+
});
|
545
|
+
|
546
|
+
await fs.promises
|
547
|
+
.unlink(encodedScanDataPath)
|
548
|
+
.catch(err => console.error('Encoded file delete error:', err));
|
549
|
+
await fs.promises
|
550
|
+
.unlink(encodedScanItemsPath)
|
551
|
+
.catch(err => console.error('Encoded file delete error:', err));
|
552
|
+
};
|
553
|
+
|
554
|
+
let browserChannel = 'chrome';
|
555
|
+
|
556
|
+
if (os.platform() === 'win32') {
|
557
|
+
browserChannel = 'msedge';
|
558
|
+
}
|
559
|
+
|
560
|
+
if (os.platform() === 'linux') {
|
561
|
+
browserChannel = 'chromium';
|
562
|
+
}
|
563
|
+
|
564
|
+
const writeSummaryPdf = async (storagePath, pagesScanned, filename = 'summary') => {
|
565
|
+
const htmlFilePath = `${storagePath}/${filename}.html`;
|
566
|
+
const fileDestinationPath = `${storagePath}/${filename}.pdf`;
|
567
|
+
const browser = await chromium.launch({
|
568
|
+
headless: true,
|
569
|
+
channel: browserChannel,
|
570
|
+
});
|
571
|
+
|
572
|
+
const context = await browser.newContext({
|
573
|
+
ignoreHTTPSErrors: true,
|
574
|
+
serviceWorkers: 'block',
|
575
|
+
});
|
576
|
+
|
577
|
+
const page = await context.newPage();
|
578
|
+
|
579
|
+
const data = fs.readFileSync(htmlFilePath, { encoding: 'utf-8' });
|
580
|
+
await page.setContent(data);
|
581
|
+
|
582
|
+
await page.waitForLoadState('networkidle', { timeout: 30000 });
|
583
|
+
|
584
|
+
await page.emulateMedia({ media: 'print' });
|
585
|
+
|
586
|
+
await page.pdf({
|
587
|
+
margin: { bottom: '32px' },
|
588
|
+
path: fileDestinationPath,
|
589
|
+
format: 'A4',
|
590
|
+
displayHeaderFooter: true,
|
591
|
+
footerTemplate: `
|
592
|
+
<div style="margin-top:50px;color:#26241b;font-family:Open Sans;text-align: center;width: 100%;font-weight:400">
|
593
|
+
<span style="color:#26241b;font-size: 14px;font-weight:400">Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
|
594
|
+
</div>
|
595
|
+
`,
|
596
|
+
});
|
597
|
+
|
598
|
+
await page.close();
|
599
|
+
|
600
|
+
await context.close();
|
601
|
+
await browser.close();
|
602
|
+
|
603
|
+
if (pagesScanned < 2000) {
|
604
|
+
fs.unlinkSync(htmlFilePath);
|
605
|
+
}
|
606
|
+
};
|
607
|
+
|
608
|
+
const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
609
|
+
const { url, pageTitle, filePath } = pageResults;
|
610
|
+
|
611
|
+
const totalIssuesInPage = new Set();
|
612
|
+
Object.keys(pageResults.mustFix.rules).forEach(k => totalIssuesInPage.add(k));
|
613
|
+
Object.keys(pageResults.goodToFix.rules).forEach(k => totalIssuesInPage.add(k));
|
614
|
+
Object.keys(pageResults.needsReview.rules).forEach(k => totalIssuesInPage.add(k));
|
615
|
+
|
616
|
+
allIssues.topFiveMostIssues.push({ url, pageTitle, totalIssues: totalIssuesInPage.size });
|
617
|
+
|
618
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
619
|
+
if (!pageResults[category]) return;
|
620
|
+
|
621
|
+
const { totalItems, rules } = pageResults[category];
|
622
|
+
const currCategoryFromAllIssues = allIssues.items[category];
|
623
|
+
|
624
|
+
currCategoryFromAllIssues.totalItems += totalItems;
|
625
|
+
|
626
|
+
Object.keys(rules).forEach(rule => {
|
627
|
+
const {
|
628
|
+
description,
|
629
|
+
axeImpact,
|
630
|
+
helpUrl,
|
631
|
+
conformance,
|
632
|
+
totalItems: count,
|
633
|
+
items,
|
634
|
+
} = rules[rule];
|
635
|
+
if (!(rule in currCategoryFromAllIssues.rules)) {
|
636
|
+
currCategoryFromAllIssues.rules[rule] = {
|
637
|
+
description,
|
638
|
+
axeImpact,
|
639
|
+
helpUrl,
|
640
|
+
conformance,
|
641
|
+
totalItems: 0,
|
642
|
+
// numberOfPagesAffectedAfterRedirects: 0,
|
643
|
+
pagesAffected: {},
|
644
|
+
};
|
645
|
+
}
|
646
|
+
|
647
|
+
if (category !== 'passed' && category !== 'needsReview') {
|
648
|
+
conformance
|
649
|
+
.filter(c => /wcag[0-9]{3,4}/.test(c))
|
650
|
+
.forEach(c => {
|
651
|
+
if (!allIssues.wcagViolations.includes(c)) {
|
652
|
+
allIssues.wcagViolations.push(c);
|
653
|
+
}
|
654
|
+
});
|
655
|
+
}
|
656
|
+
|
657
|
+
const currRuleFromAllIssues = currCategoryFromAllIssues.rules[rule];
|
658
|
+
|
659
|
+
currRuleFromAllIssues.totalItems += count;
|
660
|
+
|
661
|
+
if (isCustomFlow) {
|
662
|
+
const { pageIndex, pageImagePath, metadata } = pageResults;
|
663
|
+
currRuleFromAllIssues.pagesAffected[pageIndex] = {
|
664
|
+
url,
|
665
|
+
pageTitle,
|
666
|
+
pageImagePath,
|
667
|
+
metadata,
|
668
|
+
items: [],
|
669
|
+
};
|
670
|
+
currRuleFromAllIssues.pagesAffected[pageIndex].items.push(...items);
|
671
|
+
} else {
|
672
|
+
if (!(url in currRuleFromAllIssues.pagesAffected)) {
|
673
|
+
currRuleFromAllIssues.pagesAffected[url] = {
|
674
|
+
pageTitle,
|
675
|
+
items: [],
|
676
|
+
...(filePath && { filePath }),
|
677
|
+
};
|
678
|
+
/* if (actualUrl) {
|
679
|
+
currRuleFromAllIssues.pagesAffected[url].actualUrl = actualUrl;
|
680
|
+
// Deduct duplication count from totalItems
|
681
|
+
currRuleFromAllIssues.totalItems -= 1;
|
682
|
+
// Previously using pagesAffected.length to display no. of pages affected
|
683
|
+
// However, since pagesAffected array contains duplicates, we need to deduct the duplicates
|
684
|
+
// Hence, start with negative offset, will add pagesAffected.length later
|
685
|
+
currRuleFromAllIssues.numberOfPagesAffectedAfterRedirects -= 1;
|
686
|
+
currCategoryFromAllIssues.totalItems -= 1;
|
687
|
+
} */
|
688
|
+
}
|
689
|
+
|
690
|
+
currRuleFromAllIssues.pagesAffected[url].items.push(...items);
|
691
|
+
// currRuleFromAllIssues.numberOfPagesAffectedAfterRedirects +=
|
692
|
+
// currRuleFromAllIssues.pagesAffected.length;
|
693
|
+
}
|
694
|
+
});
|
695
|
+
});
|
696
|
+
};
|
697
|
+
|
698
|
+
const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
699
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
700
|
+
allIssues.totalItems += allIssues.items[category].totalItems;
|
701
|
+
allIssues.items[category].rules = Object.entries(allIssues.items[category].rules)
|
702
|
+
.map(ruleEntry => {
|
703
|
+
const [rule, ruleInfo] = ruleEntry as [string, RuleInfo];
|
704
|
+
ruleInfo.pagesAffected = Object.entries(ruleInfo.pagesAffected)
|
705
|
+
.map(pageEntry => {
|
706
|
+
if (isCustomFlow) {
|
707
|
+
const [pageIndex, pageInfo] = pageEntry as unknown as [number, PageInfo];
|
708
|
+
return { pageIndex, ...pageInfo };
|
709
|
+
}
|
710
|
+
const [url, pageInfo] = pageEntry as unknown as [string, PageInfo];
|
711
|
+
return { url, ...pageInfo };
|
712
|
+
})
|
713
|
+
.sort((page1, page2) => page2.items.length - page1.items.length);
|
714
|
+
return { rule, ...ruleInfo };
|
715
|
+
})
|
716
|
+
.sort((rule1, rule2) => rule2.totalItems - rule1.totalItems);
|
717
|
+
});
|
718
|
+
allIssues.topFiveMostIssues.sort((page1, page2) => page2.totalIssues - page1.totalIssues);
|
719
|
+
allIssues.topFiveMostIssues = allIssues.topFiveMostIssues.slice(0, 5);
|
720
|
+
};
|
721
|
+
|
722
|
+
const createRuleIdJson = allIssues => {
|
723
|
+
const compiledRuleJson = {};
|
724
|
+
|
725
|
+
const ruleIterator = rule => {
|
726
|
+
const ruleId = rule.rule;
|
727
|
+
let snippets = [];
|
728
|
+
|
729
|
+
if (oobeeAiRules.includes(ruleId)) {
|
730
|
+
const snippetsSet = new Set();
|
731
|
+
rule.pagesAffected.forEach(page => {
|
732
|
+
page.items.forEach(htmlItem => {
|
733
|
+
snippetsSet.add(oobeeAiHtmlETL(htmlItem.html));
|
734
|
+
});
|
735
|
+
});
|
736
|
+
snippets = [...snippetsSet];
|
737
|
+
}
|
738
|
+
compiledRuleJson[ruleId] = {
|
739
|
+
snippets,
|
740
|
+
occurrences: rule.totalItems,
|
741
|
+
};
|
742
|
+
};
|
743
|
+
|
744
|
+
allIssues.items.mustFix.rules.forEach(ruleIterator);
|
745
|
+
allIssues.items.goodToFix.rules.forEach(ruleIterator);
|
746
|
+
allIssues.items.needsReview.rules.forEach(ruleIterator);
|
747
|
+
return compiledRuleJson;
|
748
|
+
};
|
749
|
+
|
750
|
+
const moveElemScreenshots = (randomToken, storagePath) => {
|
751
|
+
const currentScreenshotsPath = `${randomToken}/elemScreenshots`;
|
752
|
+
const resultsScreenshotsPath = `${storagePath}/elemScreenshots`;
|
753
|
+
if (fs.existsSync(currentScreenshotsPath)) {
|
754
|
+
fs.moveSync(currentScreenshotsPath, resultsScreenshotsPath);
|
755
|
+
}
|
756
|
+
};
|
757
|
+
|
758
|
+
const generateArtifacts = async (
|
759
|
+
randomToken,
|
760
|
+
urlScanned,
|
761
|
+
scanType,
|
762
|
+
viewport,
|
763
|
+
pagesScanned,
|
764
|
+
pagesNotScanned,
|
765
|
+
customFlowLabel,
|
766
|
+
cypressScanAboutMetadata,
|
767
|
+
scanDetails,
|
768
|
+
zip = undefined, // optional
|
769
|
+
) => {
|
770
|
+
const intermediateDatasetsPath = `${randomToken}/datasets/${randomToken}`;
|
771
|
+
const phAppVersion = getVersion();
|
772
|
+
const storagePath = getStoragePath(randomToken);
|
773
|
+
|
774
|
+
urlScanned =
|
775
|
+
scanType === ScannerTypes.SITEMAP || scanType === ScannerTypes.LOCALFILE
|
776
|
+
? urlScanned
|
777
|
+
: urlWithoutAuth(urlScanned);
|
778
|
+
|
779
|
+
const formatAboutStartTime = dateString => {
|
780
|
+
const utcStartTimeDate = new Date(dateString);
|
781
|
+
const formattedStartTime = utcStartTimeDate.toLocaleTimeString('en-GB', {
|
782
|
+
year: 'numeric',
|
783
|
+
month: 'short',
|
784
|
+
day: 'numeric',
|
785
|
+
hour12: false,
|
786
|
+
hour: 'numeric',
|
787
|
+
minute: '2-digit',
|
788
|
+
timeZoneName: 'shortGeneric',
|
789
|
+
});
|
790
|
+
|
791
|
+
const timezoneAbbreviation = new Intl.DateTimeFormat('en', {
|
792
|
+
timeZoneName: 'shortOffset',
|
793
|
+
})
|
794
|
+
.formatToParts(utcStartTimeDate)
|
795
|
+
.find(part => part.type === 'timeZoneName').value;
|
796
|
+
|
797
|
+
// adding a breakline between the time and timezone so it looks neater on report
|
798
|
+
const timeColonIndex = formattedStartTime.lastIndexOf(':');
|
799
|
+
const timePart = formattedStartTime.slice(0, timeColonIndex + 3);
|
800
|
+
const timeZonePart = formattedStartTime.slice(timeColonIndex + 4);
|
801
|
+
const htmlFormattedStartTime = `${timePart}<br>${timeZonePart} ${timezoneAbbreviation}`;
|
802
|
+
|
803
|
+
return htmlFormattedStartTime;
|
804
|
+
};
|
805
|
+
|
806
|
+
const isCustomFlow = scanType === ScannerTypes.CUSTOM;
|
807
|
+
|
808
|
+
const allIssues: AllIssues = {
|
809
|
+
storagePath,
|
810
|
+
oobeeAi: {
|
811
|
+
htmlETL: oobeeAiHtmlETL,
|
812
|
+
rules: oobeeAiRules,
|
813
|
+
},
|
814
|
+
startTime: scanDetails.startTime ? scanDetails.startTime : new Date(),
|
815
|
+
endTime: scanDetails.endTime ? scanDetails.endTime : new Date(),
|
816
|
+
urlScanned,
|
817
|
+
scanType,
|
818
|
+
formatAboutStartTime,
|
819
|
+
isCustomFlow,
|
820
|
+
viewport,
|
821
|
+
pagesScanned,
|
822
|
+
pagesNotScanned,
|
823
|
+
totalPagesScanned: pagesScanned.length,
|
824
|
+
totalPagesNotScanned: pagesNotScanned.length,
|
825
|
+
totalItems: 0,
|
826
|
+
topFiveMostIssues: [],
|
827
|
+
wcagViolations: [],
|
828
|
+
customFlowLabel,
|
829
|
+
phAppVersion,
|
830
|
+
items: {
|
831
|
+
mustFix: { description: itemTypeDescription.mustFix, totalItems: 0, rules: [] },
|
832
|
+
goodToFix: { description: itemTypeDescription.goodToFix, totalItems: 0, rules: [] },
|
833
|
+
needsReview: { description: itemTypeDescription.needsReview, totalItems: 0, rules: [] },
|
834
|
+
passed: { description: itemTypeDescription.passed, totalItems: 0, rules: [] },
|
835
|
+
},
|
836
|
+
cypressScanAboutMetadata,
|
837
|
+
wcagLinks: constants.wcagLinks,
|
838
|
+
};
|
839
|
+
|
840
|
+
const allFiles = await extractFileNames(intermediateDatasetsPath);
|
841
|
+
|
842
|
+
const jsonArray = await Promise.all(
|
843
|
+
allFiles.map(async file => parseContentToJson(`${intermediateDatasetsPath}/${file}`)),
|
844
|
+
);
|
845
|
+
|
846
|
+
await Promise.all(
|
847
|
+
jsonArray.map(async pageResults => {
|
848
|
+
await pushResults(pageResults, allIssues, isCustomFlow);
|
849
|
+
}),
|
850
|
+
).catch(flattenIssuesError => {
|
851
|
+
consoleLogger.info('An error has occurred when flattening the issues, please try again.');
|
852
|
+
silentLogger.error(flattenIssuesError.stack);
|
853
|
+
});
|
854
|
+
|
855
|
+
flattenAndSortResults(allIssues, isCustomFlow);
|
856
|
+
|
857
|
+
printMessage([
|
858
|
+
'Scan Summary',
|
859
|
+
'',
|
860
|
+
`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'}`,
|
861
|
+
`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'}`,
|
862
|
+
`Needs Review: ${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'}`,
|
863
|
+
`Passed: ${allIssues.items.passed.totalItems} ${allIssues.items.passed.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
|
864
|
+
]);
|
865
|
+
|
866
|
+
// move screenshots folder to report folders
|
867
|
+
moveElemScreenshots(randomToken, storagePath);
|
868
|
+
if (isCustomFlow) {
|
869
|
+
createScreenshotsFolder(randomToken);
|
870
|
+
}
|
871
|
+
|
872
|
+
allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations);
|
873
|
+
|
874
|
+
const getAxeImpactCount = (allIssues: AllIssues) => {
|
875
|
+
const impactCount = {
|
876
|
+
critical: 0,
|
877
|
+
serious: 0,
|
878
|
+
moderate: 0,
|
879
|
+
minor: 0,
|
880
|
+
};
|
881
|
+
Object.values(allIssues.items).forEach(category => {
|
882
|
+
if (category.totalItems > 0) {
|
883
|
+
Object.values(category.rules).forEach(rule => {
|
884
|
+
if (rule.axeImpact === 'critical') {
|
885
|
+
impactCount.critical += rule.totalItems;
|
886
|
+
} else if (rule.axeImpact === 'serious') {
|
887
|
+
impactCount.serious += rule.totalItems;
|
888
|
+
} else if (rule.axeImpact === 'moderate') {
|
889
|
+
impactCount.moderate += rule.totalItems;
|
890
|
+
} else if (rule.axeImpact === 'minor') {
|
891
|
+
impactCount.minor += rule.totalItems;
|
892
|
+
}
|
893
|
+
});
|
894
|
+
}
|
895
|
+
});
|
896
|
+
|
897
|
+
return impactCount;
|
898
|
+
};
|
899
|
+
|
900
|
+
if (process.env.OOBEE_VERBOSE) {
|
901
|
+
const axeImpactCount = getAxeImpactCount(allIssues);
|
902
|
+
const { items, startTime, endTime, ...rest } = allIssues;
|
903
|
+
|
904
|
+
rest.critical = axeImpactCount.critical;
|
905
|
+
rest.serious = axeImpactCount.serious;
|
906
|
+
rest.moderate = axeImpactCount.moderate;
|
907
|
+
rest.minor = axeImpactCount.minor;
|
908
|
+
}
|
909
|
+
|
910
|
+
await writeCsv(allIssues, storagePath);
|
911
|
+
await writeBase64(allIssues, storagePath);
|
912
|
+
await writeSummaryHTML(allIssues, storagePath);
|
913
|
+
await writeHTML(allIssues, storagePath);
|
914
|
+
await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length), 1);
|
915
|
+
|
916
|
+
// Take option if set
|
917
|
+
if (typeof zip === 'string') {
|
918
|
+
constants.cliZipFileName = zip;
|
919
|
+
|
920
|
+
if (!zip.endsWith('.zip')) {
|
921
|
+
constants.cliZipFileName += '.zip';
|
922
|
+
}
|
923
|
+
}
|
924
|
+
|
925
|
+
await fs
|
926
|
+
.ensureDir(storagePath)
|
927
|
+
.then(() => {
|
928
|
+
zipResults(constants.cliZipFileName, storagePath);
|
929
|
+
const messageToDisplay = [
|
930
|
+
`Report of this run is at ${constants.cliZipFileName}`,
|
931
|
+
`Results directory is at ${storagePath}`,
|
932
|
+
];
|
933
|
+
|
934
|
+
if (process.env.REPORT_BREAKDOWN === '1') {
|
935
|
+
messageToDisplay.push(
|
936
|
+
'Reports have been further broken down according to their respective impact level.',
|
937
|
+
);
|
938
|
+
}
|
939
|
+
|
940
|
+
if (process.send && process.env.OOBEE_VERBOSE && process.env.REPORT_BREAKDOWN != '1') {
|
941
|
+
const zipFileNameMessage = {
|
942
|
+
type: 'zipFileName',
|
943
|
+
payload: `${constants.cliZipFileName}`,
|
944
|
+
};
|
945
|
+
const storagePathMessage = {
|
946
|
+
type: 'storagePath',
|
947
|
+
payload: `${storagePath}`,
|
948
|
+
};
|
949
|
+
|
950
|
+
process.send(JSON.stringify(storagePathMessage));
|
951
|
+
|
952
|
+
process.send(JSON.stringify(zipFileNameMessage));
|
953
|
+
}
|
954
|
+
|
955
|
+
printMessage(messageToDisplay);
|
956
|
+
})
|
957
|
+
.catch(error => {
|
958
|
+
printMessage([`Error in zipping results: ${error}`]);
|
959
|
+
});
|
960
|
+
|
961
|
+
return createRuleIdJson(allIssues);
|
962
|
+
};
|
963
|
+
|
964
|
+
export default generateArtifacts;
|