@govtechsg/oobee 0.10.20 → 0.10.28
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/docker-test.yml +1 -1
- package/DETAILS.md +40 -25
- package/Dockerfile +41 -47
- package/LICENSE-3RD-PARTY-REPORT.txt +448 -0
- package/LICENSE-3RD-PARTY.txt +19913 -0
- package/README.md +26 -0
- package/__mocks__/mock-report.html +1503 -1360
- package/package.json +9 -5
- package/scripts/decodeUnzipParse.js +29 -0
- package/scripts/install_oobee_dependencies.command +2 -2
- package/scripts/install_oobee_dependencies.ps1 +3 -3
- package/src/cli.ts +9 -7
- package/src/combine.ts +13 -5
- package/src/constants/cliFunctions.ts +38 -1
- package/src/constants/common.ts +31 -5
- package/src/constants/constants.ts +28 -26
- package/src/constants/questions.ts +4 -1
- package/src/crawlers/commonCrawlerFunc.ts +114 -152
- package/src/crawlers/crawlDomain.ts +25 -32
- package/src/crawlers/crawlIntelligentSitemap.ts +7 -1
- package/src/crawlers/crawlLocalFile.ts +1 -1
- package/src/crawlers/crawlSitemap.ts +1 -1
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +546 -472
- package/src/crawlers/customAxeFunctions.ts +1 -1
- package/src/index.ts +2 -2
- package/src/mergeAxeResults.ts +590 -214
- package/src/screenshotFunc/pdfScreenshotFunc.ts +3 -3
- package/src/static/ejs/partials/components/scanAbout.ejs +65 -0
- package/src/static/ejs/partials/components/wcagCompliance.ejs +10 -29
- package/src/static/ejs/partials/footer.ejs +10 -13
- package/src/static/ejs/partials/scripts/categorySummary.ejs +2 -2
- package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
- package/src/static/ejs/partials/scripts/reportSearch.ejs +1 -0
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +54 -52
- package/src/static/ejs/partials/scripts/scanAboutScript.ejs +38 -0
- package/src/static/ejs/partials/styles/styles.ejs +26 -1
- package/src/static/ejs/partials/summaryMain.ejs +15 -42
- package/src/static/ejs/report.ejs +22 -12
- package/src/utils.ts +10 -2
- package/src/xPathToCss.ts +186 -0
- package/a11y-scan-results.zip +0 -0
- package/src/types/xpath-to-css.d.ts +0 -3
package/src/mergeAxeResults.ts
CHANGED
@@ -9,9 +9,11 @@ import { fileURLToPath } from 'url';
|
|
9
9
|
import { chromium } from 'playwright';
|
10
10
|
import { createWriteStream } from 'fs';
|
11
11
|
import { AsyncParser, ParserOptions } from '@json2csv/node';
|
12
|
-
import
|
12
|
+
import zlib from 'zlib';
|
13
|
+
import { Base64Encode } from 'base64-stream';
|
14
|
+
import { pipeline } from 'stream/promises';
|
13
15
|
import constants, { ScannerTypes } from './constants/constants.js';
|
14
|
-
import { urlWithoutAuth } from './constants/common.js';
|
16
|
+
import { urlWithoutAuth, prepareData } from './constants/common.js';
|
15
17
|
import {
|
16
18
|
createScreenshotsFolder,
|
17
19
|
getStoragePath,
|
@@ -24,8 +26,6 @@ import { consoleLogger, silentLogger } from './logs.js';
|
|
24
26
|
import itemTypeDescription from './constants/itemTypeDescription.js';
|
25
27
|
import { oobeeAiHtmlETL, oobeeAiRules } from './constants/oobeeAi.js';
|
26
28
|
|
27
|
-
const cwd = process.cwd();
|
28
|
-
|
29
29
|
export type ItemsInfo = {
|
30
30
|
html: string;
|
31
31
|
message: string;
|
@@ -36,6 +36,7 @@ export type ItemsInfo = {
|
|
36
36
|
|
37
37
|
type PageInfo = {
|
38
38
|
items: ItemsInfo[];
|
39
|
+
itemsCount?: number;
|
39
40
|
pageTitle: string;
|
40
41
|
url?: string;
|
41
42
|
pageImagePath?: string;
|
@@ -53,6 +54,13 @@ export type RuleInfo = {
|
|
53
54
|
helpUrl: string;
|
54
55
|
};
|
55
56
|
|
57
|
+
type Category = {
|
58
|
+
description: string;
|
59
|
+
totalItems: number;
|
60
|
+
totalRuleIssues: number;
|
61
|
+
rules: RuleInfo[];
|
62
|
+
};
|
63
|
+
|
56
64
|
type AllIssues = {
|
57
65
|
storagePath: string;
|
58
66
|
oobeeAi: {
|
@@ -63,6 +71,7 @@ type AllIssues = {
|
|
63
71
|
endTime: Date;
|
64
72
|
urlScanned: string;
|
65
73
|
scanType: string;
|
74
|
+
deviceChosen: string;
|
66
75
|
formatAboutStartTime: (dateString: any) => string;
|
67
76
|
isCustomFlow: boolean;
|
68
77
|
viewport: string;
|
@@ -72,22 +81,26 @@ type AllIssues = {
|
|
72
81
|
totalPagesNotScanned: number;
|
73
82
|
totalItems: number;
|
74
83
|
topFiveMostIssues: Array<any>;
|
84
|
+
topTenPagesWithMostIssues: Array<any>;
|
85
|
+
topTenIssues: Array<any>;
|
75
86
|
wcagViolations: string[];
|
76
87
|
customFlowLabel: string;
|
77
88
|
phAppVersion: string;
|
78
89
|
items: {
|
79
|
-
mustFix:
|
80
|
-
goodToFix:
|
81
|
-
needsReview:
|
82
|
-
passed:
|
90
|
+
mustFix: Category;
|
91
|
+
goodToFix: Category;
|
92
|
+
needsReview: Category;
|
93
|
+
passed: Category;
|
83
94
|
};
|
84
95
|
cypressScanAboutMetadata: string;
|
85
96
|
wcagLinks: { [key: string]: string };
|
86
97
|
[key: string]: any;
|
98
|
+
advancedScanOptionsSummaryItems: { [key: string]: boolean };
|
87
99
|
};
|
88
100
|
|
89
101
|
const filename = fileURLToPath(import.meta.url);
|
90
102
|
const dirname = path.dirname(filename);
|
103
|
+
const BUFFER_LIMIT = 100 * 1024 * 1024; // 100MB size
|
91
104
|
|
92
105
|
const extractFileNames = async (directory: string): Promise<string[]> => {
|
93
106
|
ensureDirSync(directory);
|
@@ -135,7 +148,7 @@ const writeCsv = async (allIssues, storagePath) => {
|
|
135
148
|
return compareCategory === 0 ? a[1].rule.localeCompare(b[1].rule) : compareCategory;
|
136
149
|
});
|
137
150
|
};
|
138
|
-
|
151
|
+
|
139
152
|
const flattenRule = catAndRule => {
|
140
153
|
const [severity, rule] = catAndRule;
|
141
154
|
const results = [];
|
@@ -154,39 +167,49 @@ const writeCsv = async (allIssues, storagePath) => {
|
|
154
167
|
pagesAffected.sort((a, b) => a.url.localeCompare(b.url));
|
155
168
|
// format clauses as a string
|
156
169
|
const wcagConformance = clausesArr.join(',');
|
170
|
+
|
157
171
|
pagesAffected.forEach(affectedPage => {
|
158
172
|
const { url, items } = affectedPage;
|
159
173
|
items.forEach(item => {
|
160
174
|
const { html, page, message, xpath } = item;
|
161
|
-
const howToFix = message.replace(/(\r\n|\n|\r)/g, '
|
175
|
+
const howToFix = message.replace(/(\r\n|\n|\r)/g, '\\n'); // preserve newlines as \n
|
162
176
|
const violation = html || formatPageViolation(page); // page is a number, not a string
|
163
177
|
const context = violation.replace(/(\r\n|\n|\r)/g, ''); // remove newlines
|
164
178
|
|
165
179
|
results.push({
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
180
|
+
customFlowLabel: allIssues.customFlowLabel || '',
|
181
|
+
deviceChosen: allIssues.deviceChosen || '',
|
182
|
+
scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
183
|
+
severity: severity || '',
|
184
|
+
issueId: issueId || '',
|
185
|
+
issueDescription: issueDescription || '',
|
186
|
+
wcagConformance: wcagConformance || '',
|
187
|
+
url: url || '',
|
188
|
+
pageTitle: affectedPage.pageTitle || 'No page title',
|
189
|
+
context: context || '',
|
190
|
+
howToFix: howToFix || '',
|
191
|
+
axeImpact: axeImpact || '',
|
192
|
+
xpath: xpath || '',
|
193
|
+
learnMore: learnMore || '',
|
176
194
|
});
|
177
195
|
});
|
178
196
|
});
|
179
197
|
if (results.length === 0) return {};
|
180
198
|
return results;
|
181
199
|
};
|
200
|
+
|
182
201
|
const opts: ParserOptions<any, any> = {
|
183
202
|
transforms: [getRulesByCategory, flattenRule],
|
184
203
|
fields: [
|
204
|
+
'customFlowLabel',
|
205
|
+
'deviceChosen',
|
206
|
+
'scanCompletedAt',
|
185
207
|
'severity',
|
186
208
|
'issueId',
|
187
209
|
'issueDescription',
|
188
210
|
'wcagConformance',
|
189
211
|
'url',
|
212
|
+
'pageTitle',
|
190
213
|
'context',
|
191
214
|
'howToFix',
|
192
215
|
'axeImpact',
|
@@ -195,17 +218,23 @@ const writeCsv = async (allIssues, storagePath) => {
|
|
195
218
|
],
|
196
219
|
includeEmptyRows: true,
|
197
220
|
};
|
221
|
+
|
198
222
|
const parser = new AsyncParser(opts);
|
199
223
|
parser.parse(allIssues).pipe(csvOutput);
|
200
224
|
};
|
201
225
|
|
202
|
-
const compileHtmlWithEJS = async (
|
226
|
+
const compileHtmlWithEJS = async (
|
227
|
+
allIssues: AllIssues,
|
228
|
+
storagePath: string,
|
229
|
+
htmlFilename = 'report',
|
230
|
+
) => {
|
203
231
|
const htmlFilePath = `${path.join(storagePath, htmlFilename)}.html`;
|
204
232
|
const ejsString = fs.readFileSync(path.join(dirname, './static/ejs/report.ejs'), 'utf-8');
|
205
233
|
const template = ejs.compile(ejsString, {
|
206
234
|
filename: path.join(dirname, './static/ejs/report.ejs'),
|
207
235
|
});
|
208
|
-
|
236
|
+
|
237
|
+
const html = template({...allIssues, storagePath: JSON.stringify(storagePath)});
|
209
238
|
await fs.writeFile(htmlFilePath, html);
|
210
239
|
|
211
240
|
let htmlContent = await fs.readFile(htmlFilePath, { encoding: 'utf8' });
|
@@ -213,28 +242,8 @@ const compileHtmlWithEJS = async (allIssues, storagePath, htmlFilename = 'report
|
|
213
242
|
const headIndex = htmlContent.indexOf('</head>');
|
214
243
|
const injectScript = `
|
215
244
|
<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
245
|
// IMPORTANT! DO NOT REMOVE ME: Decode the encoded data
|
235
|
-
|
236
|
-
console.error("Error decoding base64 data:", error);
|
237
|
-
}
|
246
|
+
|
238
247
|
</script>
|
239
248
|
`;
|
240
249
|
|
@@ -259,7 +268,7 @@ const splitHtmlAndCreateFiles = async (htmlFilePath, storagePath) => {
|
|
259
268
|
throw new Error('Marker comment not found in the HTML file.');
|
260
269
|
}
|
261
270
|
|
262
|
-
const topContent = htmlContent.slice(0, splitIndex + splitMarker.length)
|
271
|
+
const topContent = `${htmlContent.slice(0, splitIndex + splitMarker.length)}\n\n`;
|
263
272
|
const bottomContent = htmlContent.slice(splitIndex + splitMarker.length);
|
264
273
|
|
265
274
|
const topFilePath = path.join(storagePath, 'report-partial-top.htm.txt');
|
@@ -276,42 +285,32 @@ const splitHtmlAndCreateFiles = async (htmlFilePath, storagePath) => {
|
|
276
285
|
}
|
277
286
|
};
|
278
287
|
|
279
|
-
const writeHTML = async (
|
288
|
+
const writeHTML = async (
|
289
|
+
allIssues: AllIssues,
|
290
|
+
storagePath: string,
|
291
|
+
htmlFilename = 'report',
|
292
|
+
scanDetailsFilePath: string,
|
293
|
+
scanItemsFilePath: string,
|
294
|
+
) => {
|
280
295
|
const htmlFilePath = await compileHtmlWithEJS(allIssues, storagePath, htmlFilename);
|
281
|
-
const inputFilePath = path.resolve(storagePath, 'scanDetails.csv');
|
282
|
-
const outputFilePath = `${storagePath}/${htmlFilename}.html`;
|
283
|
-
|
284
296
|
const { topFilePath, bottomFilePath } = await splitHtmlAndCreateFiles(htmlFilePath, storagePath);
|
285
|
-
|
286
297
|
const prefixData = fs.readFileSync(path.join(storagePath, 'report-partial-top.htm.txt'), 'utf-8');
|
287
298
|
const suffixData = fs.readFileSync(
|
288
299
|
path.join(storagePath, 'report-partial-bottom.htm.txt'),
|
289
300
|
'utf-8',
|
290
301
|
);
|
291
302
|
|
292
|
-
const
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
const inputStream = fs.createReadStream(inputFilePath, {
|
299
|
-
encoding: 'utf-8',
|
303
|
+
const scanDetailsReadStream = fs.createReadStream(scanDetailsFilePath, {
|
304
|
+
encoding: 'utf8',
|
305
|
+
highWaterMark: BUFFER_LIMIT,
|
306
|
+
});
|
307
|
+
const scanItemsReadStream = fs.createReadStream(scanItemsFilePath, {
|
308
|
+
encoding: 'utf8',
|
300
309
|
highWaterMark: BUFFER_LIMIT,
|
301
310
|
});
|
302
311
|
|
303
|
-
|
304
|
-
|
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
|
-
}
|
312
|
+
const outputFilePath = `${storagePath}/${htmlFilename}.html`;
|
313
|
+
const outputStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
|
315
314
|
|
316
315
|
const cleanupFiles = async () => {
|
317
316
|
try {
|
@@ -321,75 +320,54 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report') => {
|
|
321
320
|
}
|
322
321
|
};
|
323
322
|
|
324
|
-
|
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
|
-
}
|
323
|
+
outputStream.write(prefixData);
|
362
324
|
|
363
|
-
|
364
|
-
|
325
|
+
// outputStream.write("scanData = decompressJsonObject('");
|
326
|
+
outputStream.write(
|
327
|
+
"let scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse('",
|
328
|
+
);
|
329
|
+
scanDetailsReadStream.pipe(outputStream, { end: false });
|
330
|
+
|
331
|
+
scanDetailsReadStream.on('end', () => {
|
332
|
+
// outputStream.write("')\n\n");
|
333
|
+
outputStream.write("'); })();\n\n");
|
334
|
+
// outputStream.write("(scanItems = decompressJsonObject('");
|
335
|
+
outputStream.write(
|
336
|
+
"let scanItemsPromise = (async () => { console.log('Loading scanItems...'); scanItems = await decodeUnzipParse('",
|
337
|
+
);
|
338
|
+
scanItemsReadStream.pipe(outputStream, { end: false });
|
365
339
|
});
|
366
340
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
flushBuffer();
|
341
|
+
scanDetailsReadStream.on('error', err => {
|
342
|
+
console.error('Read stream error:', err);
|
343
|
+
outputStream.end();
|
344
|
+
});
|
372
345
|
|
346
|
+
scanItemsReadStream.on('end', () => {
|
347
|
+
// outputStream.write("')\n\n");
|
348
|
+
outputStream.write("'); })();\n\n");
|
373
349
|
outputStream.write(suffixData);
|
374
350
|
outputStream.end();
|
375
|
-
console.log('Content appended successfully.');
|
376
|
-
|
377
|
-
await cleanupFiles();
|
378
351
|
});
|
379
352
|
|
380
|
-
|
381
|
-
console.error('
|
353
|
+
scanItemsReadStream.on('error', err => {
|
354
|
+
console.error('Read stream error:', err);
|
382
355
|
outputStream.end();
|
383
|
-
|
384
|
-
await cleanupFiles();
|
385
356
|
});
|
386
357
|
|
358
|
+
consoleLogger.info('Content appended successfully.');
|
359
|
+
await cleanupFiles();
|
360
|
+
|
387
361
|
outputStream.on('error', err => {
|
388
|
-
|
362
|
+
consoleLogger.error('Error writing to output file:', err);
|
389
363
|
});
|
390
364
|
};
|
391
365
|
|
392
|
-
const writeSummaryHTML = async (
|
366
|
+
const writeSummaryHTML = async (
|
367
|
+
allIssues: AllIssues,
|
368
|
+
storagePath: string,
|
369
|
+
htmlFilename = 'summary',
|
370
|
+
) => {
|
393
371
|
const ejsString = fs.readFileSync(path.join(dirname, './static/ejs/summary.ejs'), 'utf-8');
|
394
372
|
const template = ejs.compile(ejsString, {
|
395
373
|
filename: path.join(dirname, './static/ejs/summary.ejs'),
|
@@ -398,47 +376,56 @@ const writeSummaryHTML = async (allIssues, storagePath, htmlFilename = 'summary'
|
|
398
376
|
fs.writeFileSync(`${storagePath}/${htmlFilename}.html`, html);
|
399
377
|
};
|
400
378
|
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
}
|
407
|
-
|
408
|
-
} else if (value === null) {
|
409
|
-
writeStream.write('null');
|
410
|
-
}
|
411
|
-
}
|
379
|
+
const cleanUpJsonFiles = async (filesToDelete: string[]) => {
|
380
|
+
consoleLogger.info('Cleaning up JSON files...');
|
381
|
+
filesToDelete.forEach(file => {
|
382
|
+
fs.unlinkSync(file);
|
383
|
+
consoleLogger.info(`Deleted ${file}`);
|
384
|
+
});
|
385
|
+
};
|
412
386
|
|
413
|
-
function serializeObject(obj
|
387
|
+
function* serializeObject(obj: any, depth = 0, indent = ' ') {
|
414
388
|
const currentIndent = indent.repeat(depth);
|
415
389
|
const nextIndent = indent.repeat(depth + 1);
|
416
390
|
|
417
391
|
if (obj instanceof Date) {
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
392
|
+
yield JSON.stringify(obj.toISOString());
|
393
|
+
return;
|
394
|
+
}
|
395
|
+
|
396
|
+
if (Array.isArray(obj)) {
|
397
|
+
yield '[\n';
|
398
|
+
for (let i = 0; i < obj.length; i++) {
|
399
|
+
if (i > 0) yield ',\n';
|
400
|
+
yield nextIndent;
|
401
|
+
yield* serializeObject(obj[i], depth + 1, indent);
|
402
|
+
}
|
403
|
+
yield `\n${currentIndent}]`;
|
404
|
+
return;
|
405
|
+
}
|
406
|
+
|
407
|
+
if (obj !== null && typeof obj === 'object') {
|
408
|
+
yield '{\n';
|
429
409
|
const keys = Object.keys(obj);
|
430
|
-
keys.
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
410
|
+
for (let i = 0; i < keys.length; i++) {
|
411
|
+
const key = keys[i];
|
412
|
+
if (i > 0) yield ',\n';
|
413
|
+
yield `${nextIndent}${JSON.stringify(key)}: `;
|
414
|
+
yield* serializeObject(obj[key], depth + 1, indent);
|
415
|
+
}
|
416
|
+
yield `\n${currentIndent}}`;
|
417
|
+
return;
|
438
418
|
}
|
419
|
+
|
420
|
+
if (obj === null || typeof obj === 'function' || typeof obj === 'undefined') {
|
421
|
+
yield 'null';
|
422
|
+
return;
|
423
|
+
}
|
424
|
+
|
425
|
+
yield JSON.stringify(obj);
|
439
426
|
}
|
440
427
|
|
441
|
-
function writeLargeJsonToFile(obj, filePath) {
|
428
|
+
function writeLargeJsonToFile(obj: object, filePath: string) {
|
442
429
|
return new Promise((resolve, reject) => {
|
443
430
|
const writeStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
444
431
|
|
@@ -448,61 +435,231 @@ function writeLargeJsonToFile(obj, filePath) {
|
|
448
435
|
});
|
449
436
|
|
450
437
|
writeStream.on('finish', () => {
|
451
|
-
consoleLogger.info(
|
438
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
452
439
|
resolve(true);
|
453
440
|
});
|
454
441
|
|
455
|
-
serializeObject(obj
|
456
|
-
|
442
|
+
const generator = serializeObject(obj);
|
443
|
+
|
444
|
+
function write() {
|
445
|
+
let next: any;
|
446
|
+
while (!(next = generator.next()).done) {
|
447
|
+
if (!writeStream.write(next.value)) {
|
448
|
+
writeStream.once('drain', write);
|
449
|
+
return;
|
450
|
+
}
|
451
|
+
}
|
452
|
+
writeStream.end();
|
453
|
+
}
|
454
|
+
|
455
|
+
write();
|
457
456
|
});
|
458
457
|
}
|
459
458
|
|
460
|
-
const
|
461
|
-
|
462
|
-
const
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
459
|
+
const writeLargeScanItemsJsonToFile = async (obj: object, filePath: string) => {
|
460
|
+
return new Promise((resolve, reject) => {
|
461
|
+
const writeStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
|
462
|
+
const writeQueue: string[] = [];
|
463
|
+
let isWriting = false;
|
464
|
+
|
465
|
+
const processNextWrite = async () => {
|
466
|
+
if (isWriting || writeQueue.length === 0) return;
|
467
|
+
|
468
|
+
isWriting = true;
|
469
|
+
const data = writeQueue.shift()!;
|
470
|
+
|
471
|
+
try {
|
472
|
+
if (!writeStream.write(data)) {
|
473
|
+
await new Promise<void>(resolve => {
|
474
|
+
writeStream.once('drain', () => {
|
475
|
+
resolve();
|
476
|
+
});
|
477
|
+
});
|
478
|
+
}
|
479
|
+
} catch (error) {
|
480
|
+
writeStream.destroy(error as Error);
|
481
|
+
return;
|
482
|
+
}
|
483
|
+
|
484
|
+
isWriting = false;
|
485
|
+
processNextWrite();
|
486
|
+
};
|
487
|
+
|
488
|
+
const queueWrite = (data: string) => {
|
489
|
+
writeQueue.push(data);
|
490
|
+
processNextWrite();
|
491
|
+
};
|
469
492
|
|
470
|
-
|
493
|
+
writeStream.on('error', error => {
|
494
|
+
consoleLogger.error(`Error writing object to JSON file: ${error}`);
|
495
|
+
reject(error);
|
496
|
+
});
|
471
497
|
|
472
|
-
|
473
|
-
|
498
|
+
writeStream.on('finish', () => {
|
499
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
500
|
+
resolve(true);
|
501
|
+
});
|
474
502
|
|
475
503
|
try {
|
476
|
-
|
477
|
-
const
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
504
|
+
queueWrite('{\n');
|
505
|
+
const keys = Object.keys(obj);
|
506
|
+
|
507
|
+
keys.forEach((key, i) => {
|
508
|
+
const value = obj[key];
|
509
|
+
queueWrite(` "${key}": {\n`);
|
510
|
+
|
511
|
+
const { rules, ...otherProperties } = value;
|
512
|
+
|
513
|
+
// Write other properties
|
514
|
+
Object.entries(otherProperties).forEach(([propKey, propValue], j) => {
|
515
|
+
const propValueString =
|
516
|
+
propValue === null ||
|
517
|
+
typeof propValue === 'function' ||
|
518
|
+
typeof propValue === 'undefined'
|
519
|
+
? 'null'
|
520
|
+
: JSON.stringify(propValue);
|
521
|
+
queueWrite(` "${propKey}": ${propValueString}`);
|
522
|
+
if (j < Object.keys(otherProperties).length - 1 || (rules && rules.length >= 0)) {
|
523
|
+
queueWrite(',\n');
|
524
|
+
} else {
|
525
|
+
queueWrite('\n');
|
526
|
+
}
|
527
|
+
});
|
482
528
|
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
529
|
+
if (rules && Array.isArray(rules)) {
|
530
|
+
queueWrite(' "rules": [\n');
|
531
|
+
|
532
|
+
rules.forEach((rule, j) => {
|
533
|
+
queueWrite(' {\n');
|
534
|
+
const { pagesAffected, ...otherRuleProperties } = rule;
|
535
|
+
|
536
|
+
Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
|
537
|
+
const ruleValueString =
|
538
|
+
ruleValue === null ||
|
539
|
+
typeof ruleValue === 'function' ||
|
540
|
+
typeof ruleValue === 'undefined'
|
541
|
+
? 'null'
|
542
|
+
: JSON.stringify(ruleValue);
|
543
|
+
queueWrite(` "${ruleKey}": ${ruleValueString}`);
|
544
|
+
if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
|
545
|
+
queueWrite(',\n');
|
546
|
+
} else {
|
547
|
+
queueWrite('\n');
|
548
|
+
}
|
549
|
+
});
|
550
|
+
|
551
|
+
if (pagesAffected && Array.isArray(pagesAffected)) {
|
552
|
+
queueWrite(' "pagesAffected": [\n');
|
553
|
+
|
554
|
+
pagesAffected.forEach((page, p) => {
|
555
|
+
const pageJson = JSON.stringify(page, null, 2)
|
556
|
+
.split('\n')
|
557
|
+
.map((line, idx) => (idx === 0 ? ` ${line}` : ` ${line}`))
|
558
|
+
.join('\n');
|
559
|
+
|
560
|
+
queueWrite(pageJson);
|
561
|
+
|
562
|
+
if (p < pagesAffected.length - 1) {
|
563
|
+
queueWrite(',\n');
|
564
|
+
} else {
|
565
|
+
queueWrite('\n');
|
566
|
+
}
|
567
|
+
});
|
568
|
+
|
569
|
+
queueWrite(' ]');
|
570
|
+
}
|
487
571
|
|
488
|
-
|
489
|
-
|
490
|
-
|
572
|
+
queueWrite('\n }');
|
573
|
+
if (j < rules.length - 1) {
|
574
|
+
queueWrite(',\n');
|
575
|
+
} else {
|
576
|
+
queueWrite('\n');
|
577
|
+
}
|
578
|
+
});
|
579
|
+
|
580
|
+
queueWrite(' ]');
|
581
|
+
}
|
582
|
+
|
583
|
+
queueWrite('\n }');
|
584
|
+
if (i < keys.length - 1) {
|
585
|
+
queueWrite(',\n');
|
586
|
+
} else {
|
587
|
+
queueWrite('\n');
|
588
|
+
}
|
491
589
|
});
|
492
590
|
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
591
|
+
queueWrite('}\n');
|
592
|
+
|
593
|
+
// Ensure all queued writes are processed before ending
|
594
|
+
const checkQueueAndEnd = () => {
|
595
|
+
if (writeQueue.length === 0 && !isWriting) {
|
596
|
+
writeStream.end();
|
597
|
+
} else {
|
598
|
+
setTimeout(checkQueueAndEnd, 100);
|
599
|
+
}
|
600
|
+
};
|
601
|
+
|
602
|
+
checkQueueAndEnd();
|
603
|
+
} catch (err) {
|
604
|
+
writeStream.destroy(err as Error);
|
605
|
+
reject(err);
|
606
|
+
}
|
607
|
+
});
|
608
|
+
};
|
609
|
+
|
610
|
+
async function compressJsonFileStreaming(inputPath: string, outputPath: string) {
|
611
|
+
// Create the read and write streams
|
612
|
+
const readStream = fs.createReadStream(inputPath);
|
613
|
+
const writeStream = fs.createWriteStream(outputPath);
|
614
|
+
|
615
|
+
// Create a gzip transform stream
|
616
|
+
const gzip = zlib.createGzip();
|
617
|
+
|
618
|
+
// Create a Base64 transform stream
|
619
|
+
const base64Encode = new Base64Encode();
|
620
|
+
|
621
|
+
// Pipe the streams:
|
622
|
+
// read -> gzip -> base64 -> write
|
623
|
+
await pipeline(readStream, gzip, base64Encode, writeStream);
|
624
|
+
console.log(`File successfully compressed and saved to ${outputPath}`);
|
625
|
+
}
|
626
|
+
|
627
|
+
const writeJsonFileAndCompressedJsonFile = async (
|
628
|
+
data: object,
|
629
|
+
storagePath: string,
|
630
|
+
filename: string,
|
631
|
+
): Promise<{ jsonFilePath: string; base64FilePath: string }> => {
|
632
|
+
try {
|
633
|
+
consoleLogger.info(`Writing JSON to ${filename}.json`);
|
634
|
+
const jsonFilePath = path.join(storagePath, `${filename}.json`);
|
635
|
+
if (filename === 'scanItems') {
|
636
|
+
await writeLargeScanItemsJsonToFile(data, jsonFilePath);
|
637
|
+
} else {
|
638
|
+
await writeLargeJsonToFile(data, jsonFilePath);
|
498
639
|
}
|
640
|
+
|
641
|
+
consoleLogger.info(
|
642
|
+
`Reading ${filename}.json, gzipping and base64 encoding it into ${filename}.json.gz.b64`,
|
643
|
+
);
|
644
|
+
const base64FilePath = path.join(storagePath, `${filename}.json.gz.b64`);
|
645
|
+
await compressJsonFileStreaming(jsonFilePath, base64FilePath);
|
646
|
+
|
647
|
+
consoleLogger.info(`Finished compression and base64 encoding for ${filename}`);
|
648
|
+
return {
|
649
|
+
jsonFilePath,
|
650
|
+
base64FilePath,
|
651
|
+
};
|
499
652
|
} catch (error) {
|
500
|
-
|
653
|
+
consoleLogger.error(`Error compressing and encoding ${filename}`);
|
501
654
|
throw error;
|
502
655
|
}
|
503
656
|
};
|
504
657
|
|
505
|
-
const streamEncodedDataToFile = async (
|
658
|
+
const streamEncodedDataToFile = async (
|
659
|
+
inputFilePath: string,
|
660
|
+
writeStream: fs.WriteStream,
|
661
|
+
appendComma: boolean,
|
662
|
+
) => {
|
506
663
|
const readStream = fs.createReadStream(inputFilePath, { encoding: 'utf8' });
|
507
664
|
let isFirstChunk = true;
|
508
665
|
|
@@ -520,35 +677,120 @@ const streamEncodedDataToFile = async (inputFilePath, writeStream, appendComma)
|
|
520
677
|
}
|
521
678
|
};
|
522
679
|
|
523
|
-
const
|
680
|
+
const writeJsonAndBase64Files = async (
|
681
|
+
allIssues: AllIssues,
|
682
|
+
storagePath: string,
|
683
|
+
): Promise<{
|
684
|
+
scanDataJsonFilePath: string;
|
685
|
+
scanDataBase64FilePath: string;
|
686
|
+
scanItemsJsonFilePath: string;
|
687
|
+
scanItemsBase64FilePath: string;
|
688
|
+
scanItemsSummaryJsonFilePath: string;
|
689
|
+
scanItemsSummaryBase64FilePath: string;
|
690
|
+
scanDataJsonFileSize: number;
|
691
|
+
scanItemsJsonFileSize: number;
|
692
|
+
}> => {
|
524
693
|
const { items, ...rest } = allIssues;
|
525
|
-
const
|
526
|
-
|
694
|
+
const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } =
|
695
|
+
await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
|
696
|
+
const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } =
|
697
|
+
await writeJsonFileAndCompressedJsonFile(items, storagePath, 'scanItems');
|
698
|
+
|
699
|
+
// scanItemsSummary
|
700
|
+
// the below mutates the original items object, since it is expensive to clone
|
701
|
+
items.mustFix.rules.forEach(rule => {
|
702
|
+
rule.pagesAffected.forEach(page => {
|
703
|
+
page.itemsCount = page.items.length;
|
704
|
+
page.items = [];
|
705
|
+
});
|
706
|
+
});
|
707
|
+
items.goodToFix.rules.forEach(rule => {
|
708
|
+
rule.pagesAffected.forEach(page => {
|
709
|
+
page.itemsCount = page.items.length;
|
710
|
+
page.items = [];
|
711
|
+
});
|
712
|
+
});
|
713
|
+
items.needsReview.rules.forEach(rule => {
|
714
|
+
rule.pagesAffected.forEach(page => {
|
715
|
+
page.itemsCount = page.items.length;
|
716
|
+
page.items = [];
|
717
|
+
});
|
718
|
+
});
|
719
|
+
items.passed.rules.forEach(rule => {
|
720
|
+
rule.pagesAffected.forEach(page => {
|
721
|
+
page.itemsCount = page.items.length;
|
722
|
+
page.items = [];
|
723
|
+
});
|
724
|
+
});
|
725
|
+
|
726
|
+
items.mustFix.totalRuleIssues = items.mustFix.rules.length;
|
727
|
+
items.goodToFix.totalRuleIssues = items.goodToFix.rules.length;
|
728
|
+
items.needsReview.totalRuleIssues = items.needsReview.rules.length;
|
729
|
+
items.passed.totalRuleIssues = items.passed.rules.length;
|
730
|
+
|
731
|
+
const {
|
732
|
+
pagesScanned,
|
733
|
+
topTenPagesWithMostIssues,
|
734
|
+
pagesNotScanned,
|
735
|
+
wcagLinks,
|
736
|
+
wcagPassPercentage,
|
737
|
+
totalPagesScanned,
|
738
|
+
totalPagesNotScanned,
|
739
|
+
topTenIssues,
|
740
|
+
} = rest;
|
741
|
+
|
742
|
+
const summaryItems = {
|
743
|
+
...items,
|
744
|
+
pagesScanned,
|
745
|
+
topTenPagesWithMostIssues,
|
746
|
+
pagesNotScanned,
|
747
|
+
wcagLinks,
|
748
|
+
wcagPassPercentage,
|
749
|
+
totalPagesScanned,
|
750
|
+
totalPagesNotScanned,
|
751
|
+
topTenIssues,
|
752
|
+
};
|
753
|
+
|
754
|
+
const {
|
755
|
+
jsonFilePath: scanItemsSummaryJsonFilePath,
|
756
|
+
base64FilePath: scanItemsSummaryBase64FilePath,
|
757
|
+
} = await writeJsonFileAndCompressedJsonFile(summaryItems, storagePath, 'scanItemsSummary');
|
758
|
+
|
759
|
+
return {
|
760
|
+
scanDataJsonFilePath,
|
761
|
+
scanDataBase64FilePath,
|
762
|
+
scanItemsJsonFilePath,
|
763
|
+
scanItemsBase64FilePath,
|
764
|
+
scanItemsSummaryJsonFilePath,
|
765
|
+
scanItemsSummaryBase64FilePath,
|
766
|
+
scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
|
767
|
+
scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
|
768
|
+
};
|
769
|
+
};
|
527
770
|
|
771
|
+
const writeScanDetailsCsv = async (
|
772
|
+
scanDataFilePath: string,
|
773
|
+
scanItemsFilePath: string,
|
774
|
+
scanItemsSummaryFilePath: string,
|
775
|
+
storagePath: string,
|
776
|
+
) => {
|
528
777
|
const filePath = path.join(storagePath, 'scanDetails.csv');
|
778
|
+
const csvWriteStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
529
779
|
const directoryPath = path.dirname(filePath);
|
530
780
|
|
531
781
|
if (!fs.existsSync(directoryPath)) {
|
532
782
|
fs.mkdirSync(directoryPath, { recursive: true });
|
533
783
|
}
|
534
784
|
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
await streamEncodedDataToFile(
|
539
|
-
await streamEncodedDataToFile(encodedScanItemsPath, csvWriteStream, false);
|
785
|
+
csvWriteStream.write('scanData_base64,scanItems_base64,scanItemsSummary_base64\n');
|
786
|
+
await streamEncodedDataToFile(scanDataFilePath, csvWriteStream, true);
|
787
|
+
await streamEncodedDataToFile(scanItemsFilePath, csvWriteStream, true);
|
788
|
+
await streamEncodedDataToFile(scanItemsSummaryFilePath, csvWriteStream, false);
|
540
789
|
|
541
790
|
await new Promise((resolve, reject) => {
|
542
791
|
csvWriteStream.end(resolve);
|
543
792
|
csvWriteStream.on('error', reject);
|
544
793
|
});
|
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
794
|
};
|
553
795
|
|
554
796
|
let browserChannel = 'chrome';
|
@@ -561,12 +803,13 @@ if (os.platform() === 'linux') {
|
|
561
803
|
browserChannel = 'chromium';
|
562
804
|
}
|
563
805
|
|
564
|
-
const writeSummaryPdf = async (storagePath, pagesScanned, filename = 'summary') => {
|
806
|
+
const writeSummaryPdf = async (storagePath: string, pagesScanned: number, filename = 'summary') => {
|
565
807
|
const htmlFilePath = `${storagePath}/${filename}.html`;
|
566
808
|
const fileDestinationPath = `${storagePath}/${filename}.pdf`;
|
567
809
|
const browser = await chromium.launch({
|
568
|
-
headless:
|
810
|
+
headless: false,
|
569
811
|
channel: browserChannel,
|
812
|
+
args: ['--headless=new', '--no-sandbox'],
|
570
813
|
});
|
571
814
|
|
572
815
|
const context = await browser.newContext({
|
@@ -613,7 +856,12 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
613
856
|
Object.keys(pageResults.goodToFix.rules).forEach(k => totalIssuesInPage.add(k));
|
614
857
|
Object.keys(pageResults.needsReview.rules).forEach(k => totalIssuesInPage.add(k));
|
615
858
|
|
616
|
-
allIssues.topFiveMostIssues.push({
|
859
|
+
allIssues.topFiveMostIssues.push({
|
860
|
+
url,
|
861
|
+
pageTitle,
|
862
|
+
totalIssues: totalIssuesInPage.size,
|
863
|
+
totalOccurrences: 0,
|
864
|
+
});
|
617
865
|
|
618
866
|
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
619
867
|
if (!pageResults[category]) return;
|
@@ -695,9 +943,47 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
695
943
|
});
|
696
944
|
};
|
697
945
|
|
946
|
+
const getTopTenIssues = allIssues => {
|
947
|
+
const categories = ['mustFix', 'goodToFix'];
|
948
|
+
const rulesWithCounts = [];
|
949
|
+
|
950
|
+
const conformanceLevels = {
|
951
|
+
wcag2a: 'A',
|
952
|
+
wcag2aa: 'AA',
|
953
|
+
wcag21aa: 'AA',
|
954
|
+
wcag22aa: 'AA',
|
955
|
+
wcag2aaa: 'AAA',
|
956
|
+
};
|
957
|
+
|
958
|
+
categories.forEach(category => {
|
959
|
+
const rules = allIssues.items[category]?.rules || [];
|
960
|
+
|
961
|
+
rules.forEach(rule => {
|
962
|
+
const wcagLevel = rule.conformance[0];
|
963
|
+
const aLevel = conformanceLevels[wcagLevel] || wcagLevel;
|
964
|
+
|
965
|
+
rulesWithCounts.push({
|
966
|
+
category,
|
967
|
+
ruleId: rule.rule,
|
968
|
+
description: rule.description,
|
969
|
+
axeImpact: rule.axeImpact,
|
970
|
+
conformance: aLevel,
|
971
|
+
totalItems: rule.totalItems,
|
972
|
+
});
|
973
|
+
});
|
974
|
+
});
|
975
|
+
|
976
|
+
rulesWithCounts.sort((a, b) => b.totalItems - a.totalItems);
|
977
|
+
|
978
|
+
return rulesWithCounts.slice(0, 10);
|
979
|
+
};
|
980
|
+
|
698
981
|
const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
982
|
+
const urlOccurrencesMap = new Map<string, number>();
|
983
|
+
|
699
984
|
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
700
985
|
allIssues.totalItems += allIssues.items[category].totalItems;
|
986
|
+
|
701
987
|
allIssues.items[category].rules = Object.entries(allIssues.items[category].rules)
|
702
988
|
.map(ruleEntry => {
|
703
989
|
const [rule, ruleInfo] = ruleEntry as [string, RuleInfo];
|
@@ -705,9 +991,14 @@ const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
|
705
991
|
.map(pageEntry => {
|
706
992
|
if (isCustomFlow) {
|
707
993
|
const [pageIndex, pageInfo] = pageEntry as unknown as [number, PageInfo];
|
994
|
+
urlOccurrencesMap.set(
|
995
|
+
pageInfo.url!,
|
996
|
+
(urlOccurrencesMap.get(pageInfo.url!) || 0) + pageInfo.items.length,
|
997
|
+
);
|
708
998
|
return { pageIndex, ...pageInfo };
|
709
999
|
}
|
710
1000
|
const [url, pageInfo] = pageEntry as unknown as [string, PageInfo];
|
1001
|
+
urlOccurrencesMap.set(url, (urlOccurrencesMap.get(url) || 0) + pageInfo.items.length);
|
711
1002
|
return { url, ...pageInfo };
|
712
1003
|
})
|
713
1004
|
.sort((page1, page2) => page2.items.length - page1.items.length);
|
@@ -715,8 +1006,19 @@ const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
|
715
1006
|
})
|
716
1007
|
.sort((rule1, rule2) => rule2.totalItems - rule1.totalItems);
|
717
1008
|
});
|
1009
|
+
|
1010
|
+
const updateIssuesWithOccurrences = (issuesList: Array<any>) => {
|
1011
|
+
issuesList.forEach(issue => {
|
1012
|
+
issue.totalOccurrences = urlOccurrencesMap.get(issue.url) || 0;
|
1013
|
+
});
|
1014
|
+
};
|
1015
|
+
|
718
1016
|
allIssues.topFiveMostIssues.sort((page1, page2) => page2.totalIssues - page1.totalIssues);
|
719
1017
|
allIssues.topFiveMostIssues = allIssues.topFiveMostIssues.slice(0, 5);
|
1018
|
+
allIssues.topTenPagesWithMostIssues = allIssues.topFiveMostIssues.slice(0, 10);
|
1019
|
+
updateIssuesWithOccurrences(allIssues.topTenPagesWithMostIssues);
|
1020
|
+
const topTenIssues = getTopTenIssues(allIssues);
|
1021
|
+
allIssues.topTenIssues = topTenIssues;
|
720
1022
|
};
|
721
1023
|
|
722
1024
|
const createRuleIdJson = allIssues => {
|
@@ -766,6 +1068,7 @@ const generateArtifacts = async (
|
|
766
1068
|
cypressScanAboutMetadata,
|
767
1069
|
scanDetails,
|
768
1070
|
zip = undefined, // optional
|
1071
|
+
generateJsonFiles = false,
|
769
1072
|
) => {
|
770
1073
|
const intermediateDatasetsPath = `${randomToken}/datasets/${randomToken}`;
|
771
1074
|
const phAppVersion = getVersion();
|
@@ -815,6 +1118,7 @@ const generateArtifacts = async (
|
|
815
1118
|
endTime: scanDetails.endTime ? scanDetails.endTime : new Date(),
|
816
1119
|
urlScanned,
|
817
1120
|
scanType,
|
1121
|
+
deviceChosen: scanDetails.deviceChosen || 'Desktop',
|
818
1122
|
formatAboutStartTime,
|
819
1123
|
isCustomFlow,
|
820
1124
|
viewport,
|
@@ -824,17 +1128,50 @@ const generateArtifacts = async (
|
|
824
1128
|
totalPagesNotScanned: pagesNotScanned.length,
|
825
1129
|
totalItems: 0,
|
826
1130
|
topFiveMostIssues: [],
|
1131
|
+
topTenPagesWithMostIssues: [],
|
1132
|
+
topTenIssues: [],
|
827
1133
|
wcagViolations: [],
|
828
1134
|
customFlowLabel,
|
829
1135
|
phAppVersion,
|
830
1136
|
items: {
|
831
|
-
mustFix: {
|
832
|
-
|
833
|
-
|
834
|
-
|
1137
|
+
mustFix: {
|
1138
|
+
description: itemTypeDescription.mustFix,
|
1139
|
+
totalItems: 0,
|
1140
|
+
totalRuleIssues: 0,
|
1141
|
+
rules: [],
|
1142
|
+
},
|
1143
|
+
goodToFix: {
|
1144
|
+
description: itemTypeDescription.goodToFix,
|
1145
|
+
totalItems: 0,
|
1146
|
+
totalRuleIssues: 0,
|
1147
|
+
rules: [],
|
1148
|
+
},
|
1149
|
+
needsReview: {
|
1150
|
+
description: itemTypeDescription.needsReview,
|
1151
|
+
totalItems: 0,
|
1152
|
+
totalRuleIssues: 0,
|
1153
|
+
rules: [],
|
1154
|
+
},
|
1155
|
+
passed: {
|
1156
|
+
description: itemTypeDescription.passed,
|
1157
|
+
totalItems: 0,
|
1158
|
+
totalRuleIssues: 0,
|
1159
|
+
rules: [],
|
1160
|
+
},
|
835
1161
|
},
|
836
1162
|
cypressScanAboutMetadata,
|
837
1163
|
wcagLinks: constants.wcagLinks,
|
1164
|
+
// Populate boolean values for id="advancedScanOptionsSummary"
|
1165
|
+
advancedScanOptionsSummaryItems: {
|
1166
|
+
showIncludeScreenshots: [true].includes(scanDetails.isIncludeScreenshots),
|
1167
|
+
showAllowSubdomains: ['same-domain'].includes(scanDetails.isAllowSubdomains),
|
1168
|
+
showEnableCustomChecks: ['default', 'enable-wcag-aaa'].includes(
|
1169
|
+
scanDetails.isEnableCustomChecks?.[0],
|
1170
|
+
),
|
1171
|
+
showEnableWcagAaa: (scanDetails.isEnableWcagAaa || []).includes('enable-wcag-aaa'),
|
1172
|
+
showSlowScanMode: [1].includes(scanDetails.isSlowScanMode),
|
1173
|
+
showAdhereRobots: [true].includes(scanDetails.isAdhereRobots),
|
1174
|
+
},
|
838
1175
|
};
|
839
1176
|
|
840
1177
|
const allFiles = await extractFileNames(intermediateDatasetsPath);
|
@@ -870,6 +1207,9 @@ const generateArtifacts = async (
|
|
870
1207
|
}
|
871
1208
|
|
872
1209
|
allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations);
|
1210
|
+
consoleLogger.info(
|
1211
|
+
`advancedScanOptionsSummaryItems is ${allIssues.advancedScanOptionsSummaryItems}`,
|
1212
|
+
);
|
873
1213
|
|
874
1214
|
const getAxeImpactCount = (allIssues: AllIssues) => {
|
875
1215
|
const impactCount = {
|
@@ -908,9 +1248,45 @@ const generateArtifacts = async (
|
|
908
1248
|
}
|
909
1249
|
|
910
1250
|
await writeCsv(allIssues, storagePath);
|
911
|
-
|
1251
|
+
const {
|
1252
|
+
scanDataJsonFilePath,
|
1253
|
+
scanDataBase64FilePath,
|
1254
|
+
scanItemsJsonFilePath,
|
1255
|
+
scanItemsBase64FilePath,
|
1256
|
+
scanItemsSummaryJsonFilePath,
|
1257
|
+
scanItemsSummaryBase64FilePath,
|
1258
|
+
scanDataJsonFileSize,
|
1259
|
+
scanItemsJsonFileSize,
|
1260
|
+
} = await writeJsonAndBase64Files(allIssues, storagePath);
|
1261
|
+
const BIG_RESULTS_THRESHOLD = 500 * 1024 * 1024; // 500 MB
|
1262
|
+
const resultsTooBig = scanDataJsonFileSize + scanItemsJsonFileSize > BIG_RESULTS_THRESHOLD;
|
1263
|
+
|
1264
|
+
await writeScanDetailsCsv(
|
1265
|
+
scanDataBase64FilePath,
|
1266
|
+
scanItemsBase64FilePath,
|
1267
|
+
scanItemsSummaryBase64FilePath,
|
1268
|
+
storagePath,
|
1269
|
+
);
|
912
1270
|
await writeSummaryHTML(allIssues, storagePath);
|
913
|
-
await writeHTML(
|
1271
|
+
await writeHTML(
|
1272
|
+
allIssues,
|
1273
|
+
storagePath,
|
1274
|
+
'report',
|
1275
|
+
scanDataBase64FilePath,
|
1276
|
+
resultsTooBig ? scanItemsSummaryBase64FilePath : scanItemsBase64FilePath,
|
1277
|
+
);
|
1278
|
+
|
1279
|
+
if (!generateJsonFiles) {
|
1280
|
+
await cleanUpJsonFiles([
|
1281
|
+
scanDataJsonFilePath,
|
1282
|
+
scanDataBase64FilePath,
|
1283
|
+
scanItemsJsonFilePath,
|
1284
|
+
scanItemsBase64FilePath,
|
1285
|
+
scanItemsSummaryJsonFilePath,
|
1286
|
+
scanItemsSummaryBase64FilePath,
|
1287
|
+
]);
|
1288
|
+
}
|
1289
|
+
|
914
1290
|
await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length), 1);
|
915
1291
|
|
916
1292
|
// Take option if set
|