@govtechsg/oobee 0.10.21 → 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 +10 -2
- package/__mocks__/mock-report.html +1503 -1360
- package/package.json +8 -4
- 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 +3 -2
- package/src/constants/cliFunctions.ts +16 -2
- package/src/constants/common.ts +29 -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 -25
- 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 +0 -2
- package/src/mergeAxeResults.ts +569 -219
- package/src/screenshotFunc/pdfScreenshotFunc.ts +3 -3
- 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/styles/styles.ejs +4 -0
- package/src/static/ejs/partials/summaryMain.ejs +15 -42
- package/src/static/ejs/report.ejs +21 -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,
|
@@ -34,6 +36,7 @@ export type ItemsInfo = {
|
|
34
36
|
|
35
37
|
type PageInfo = {
|
36
38
|
items: ItemsInfo[];
|
39
|
+
itemsCount?: number;
|
37
40
|
pageTitle: string;
|
38
41
|
url?: string;
|
39
42
|
pageImagePath?: string;
|
@@ -51,6 +54,13 @@ export type RuleInfo = {
|
|
51
54
|
helpUrl: string;
|
52
55
|
};
|
53
56
|
|
57
|
+
type Category = {
|
58
|
+
description: string;
|
59
|
+
totalItems: number;
|
60
|
+
totalRuleIssues: number;
|
61
|
+
rules: RuleInfo[];
|
62
|
+
};
|
63
|
+
|
54
64
|
type AllIssues = {
|
55
65
|
storagePath: string;
|
56
66
|
oobeeAi: {
|
@@ -61,6 +71,7 @@ type AllIssues = {
|
|
61
71
|
endTime: Date;
|
62
72
|
urlScanned: string;
|
63
73
|
scanType: string;
|
74
|
+
deviceChosen: string;
|
64
75
|
formatAboutStartTime: (dateString: any) => string;
|
65
76
|
isCustomFlow: boolean;
|
66
77
|
viewport: string;
|
@@ -70,14 +81,16 @@ type AllIssues = {
|
|
70
81
|
totalPagesNotScanned: number;
|
71
82
|
totalItems: number;
|
72
83
|
topFiveMostIssues: Array<any>;
|
84
|
+
topTenPagesWithMostIssues: Array<any>;
|
85
|
+
topTenIssues: Array<any>;
|
73
86
|
wcagViolations: string[];
|
74
87
|
customFlowLabel: string;
|
75
88
|
phAppVersion: string;
|
76
89
|
items: {
|
77
|
-
mustFix:
|
78
|
-
goodToFix:
|
79
|
-
needsReview:
|
80
|
-
passed:
|
90
|
+
mustFix: Category;
|
91
|
+
goodToFix: Category;
|
92
|
+
needsReview: Category;
|
93
|
+
passed: Category;
|
81
94
|
};
|
82
95
|
cypressScanAboutMetadata: string;
|
83
96
|
wcagLinks: { [key: string]: string };
|
@@ -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 = ${BUFFER_LIMIT}) => {
|
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
|
|
@@ -276,40 +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
|
-
const
|
297
|
-
encoding: '
|
303
|
+
const scanDetailsReadStream = fs.createReadStream(scanDetailsFilePath, {
|
304
|
+
encoding: 'utf8',
|
305
|
+
highWaterMark: BUFFER_LIMIT,
|
306
|
+
});
|
307
|
+
const scanItemsReadStream = fs.createReadStream(scanItemsFilePath, {
|
308
|
+
encoding: 'utf8',
|
298
309
|
highWaterMark: BUFFER_LIMIT,
|
299
310
|
});
|
300
311
|
|
301
|
-
|
302
|
-
|
303
|
-
let isFirstField = true;
|
304
|
-
let isWritingFirstDataLine = true;
|
305
|
-
let buffer = '';
|
306
|
-
|
307
|
-
function flushBuffer() {
|
308
|
-
if (buffer.length > 0) {
|
309
|
-
outputStream.write(buffer);
|
310
|
-
buffer = '';
|
311
|
-
}
|
312
|
-
}
|
312
|
+
const outputFilePath = `${storagePath}/${htmlFilename}.html`;
|
313
|
+
const outputStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
|
313
314
|
|
314
315
|
const cleanupFiles = async () => {
|
315
316
|
try {
|
@@ -319,75 +320,54 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report') => {
|
|
319
320
|
}
|
320
321
|
};
|
321
322
|
|
322
|
-
|
323
|
-
let chunkIndex = 0;
|
324
|
-
|
325
|
-
while (chunkIndex < chunk.length) {
|
326
|
-
const char = chunk[chunkIndex];
|
327
|
-
|
328
|
-
if (isFirstLine) {
|
329
|
-
if (char === '\n' || char === '\r') {
|
330
|
-
lineEndingDetected = true;
|
331
|
-
} else if (lineEndingDetected) {
|
332
|
-
if (char !== '\n' && char !== '\r') {
|
333
|
-
isFirstLine = false;
|
334
|
-
|
335
|
-
if (isWritingFirstDataLine) {
|
336
|
-
buffer += "scanData = base64DecodeChunkedWithDecoder('";
|
337
|
-
isWritingFirstDataLine = false;
|
338
|
-
}
|
339
|
-
buffer += char;
|
340
|
-
}
|
341
|
-
lineEndingDetected = false;
|
342
|
-
}
|
343
|
-
} else {
|
344
|
-
if (char === ',') {
|
345
|
-
buffer += "')\n\n";
|
346
|
-
buffer += "scanItems = base64DecodeChunkedWithDecoder('";
|
347
|
-
isFirstField = false;
|
348
|
-
} else if (char === '\n' || char === '\r') {
|
349
|
-
if (!isFirstField) {
|
350
|
-
buffer += "')\n";
|
351
|
-
}
|
352
|
-
} else {
|
353
|
-
buffer += char;
|
354
|
-
}
|
355
|
-
|
356
|
-
if (buffer.length >= BUFFER_LIMIT) {
|
357
|
-
flushBuffer();
|
358
|
-
}
|
359
|
-
}
|
323
|
+
outputStream.write(prefixData);
|
360
324
|
|
361
|
-
|
362
|
-
|
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 });
|
363
339
|
});
|
364
340
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
flushBuffer();
|
341
|
+
scanDetailsReadStream.on('error', err => {
|
342
|
+
console.error('Read stream error:', err);
|
343
|
+
outputStream.end();
|
344
|
+
});
|
370
345
|
|
346
|
+
scanItemsReadStream.on('end', () => {
|
347
|
+
// outputStream.write("')\n\n");
|
348
|
+
outputStream.write("'); })();\n\n");
|
371
349
|
outputStream.write(suffixData);
|
372
350
|
outputStream.end();
|
373
|
-
console.log('Content appended successfully.');
|
374
|
-
|
375
|
-
await cleanupFiles();
|
376
351
|
});
|
377
352
|
|
378
|
-
|
379
|
-
console.error('
|
353
|
+
scanItemsReadStream.on('error', err => {
|
354
|
+
console.error('Read stream error:', err);
|
380
355
|
outputStream.end();
|
381
|
-
|
382
|
-
await cleanupFiles();
|
383
356
|
});
|
384
357
|
|
358
|
+
consoleLogger.info('Content appended successfully.');
|
359
|
+
await cleanupFiles();
|
360
|
+
|
385
361
|
outputStream.on('error', err => {
|
386
|
-
|
362
|
+
consoleLogger.error('Error writing to output file:', err);
|
387
363
|
});
|
388
364
|
};
|
389
365
|
|
390
|
-
const writeSummaryHTML = async (
|
366
|
+
const writeSummaryHTML = async (
|
367
|
+
allIssues: AllIssues,
|
368
|
+
storagePath: string,
|
369
|
+
htmlFilename = 'summary',
|
370
|
+
) => {
|
391
371
|
const ejsString = fs.readFileSync(path.join(dirname, './static/ejs/summary.ejs'), 'utf-8');
|
392
372
|
const template = ejs.compile(ejsString, {
|
393
373
|
filename: path.join(dirname, './static/ejs/summary.ejs'),
|
@@ -396,47 +376,56 @@ const writeSummaryHTML = async (allIssues, storagePath, htmlFilename = 'summary'
|
|
396
376
|
fs.writeFileSync(`${storagePath}/${htmlFilename}.html`, html);
|
397
377
|
};
|
398
378
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
}
|
405
|
-
|
406
|
-
} else if (value === null) {
|
407
|
-
writeStream.write('null');
|
408
|
-
}
|
409
|
-
}
|
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
|
+
};
|
410
386
|
|
411
|
-
function serializeObject(obj
|
387
|
+
function* serializeObject(obj: any, depth = 0, indent = ' ') {
|
412
388
|
const currentIndent = indent.repeat(depth);
|
413
389
|
const nextIndent = indent.repeat(depth + 1);
|
414
390
|
|
415
391
|
if (obj instanceof Date) {
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
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';
|
427
409
|
const keys = Object.keys(obj);
|
428
|
-
keys.
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
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;
|
418
|
+
}
|
419
|
+
|
420
|
+
if (obj === null || typeof obj === 'function' || typeof obj === 'undefined') {
|
421
|
+
yield 'null';
|
422
|
+
return;
|
436
423
|
}
|
424
|
+
|
425
|
+
yield JSON.stringify(obj);
|
437
426
|
}
|
438
427
|
|
439
|
-
function writeLargeJsonToFile(obj, filePath) {
|
428
|
+
function writeLargeJsonToFile(obj: object, filePath: string) {
|
440
429
|
return new Promise((resolve, reject) => {
|
441
430
|
const writeStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
442
431
|
|
@@ -446,74 +435,231 @@ function writeLargeJsonToFile(obj, filePath) {
|
|
446
435
|
});
|
447
436
|
|
448
437
|
writeStream.on('finish', () => {
|
449
|
-
consoleLogger.info(
|
438
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
450
439
|
resolve(true);
|
451
440
|
});
|
452
441
|
|
453
|
-
serializeObject(obj
|
454
|
-
|
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();
|
455
456
|
});
|
456
457
|
}
|
457
458
|
|
458
|
-
const
|
459
|
-
|
460
|
-
const
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
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
|
+
};
|
466
487
|
|
467
|
-
|
488
|
+
const queueWrite = (data: string) => {
|
489
|
+
writeQueue.push(data);
|
490
|
+
processNextWrite();
|
491
|
+
};
|
492
|
+
|
493
|
+
writeStream.on('error', error => {
|
494
|
+
consoleLogger.error(`Error writing object to JSON file: ${error}`);
|
495
|
+
reject(error);
|
496
|
+
});
|
468
497
|
|
469
|
-
|
470
|
-
|
498
|
+
writeStream.on('finish', () => {
|
499
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
500
|
+
resolve(true);
|
501
|
+
});
|
471
502
|
|
472
503
|
try {
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
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
|
+
});
|
478
528
|
|
479
|
-
|
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
|
+
}
|
480
571
|
|
481
|
-
|
482
|
-
|
572
|
+
queueWrite('\n }');
|
573
|
+
if (j < rules.length - 1) {
|
574
|
+
queueWrite(',\n');
|
575
|
+
} else {
|
576
|
+
queueWrite('\n');
|
577
|
+
}
|
578
|
+
});
|
483
579
|
|
484
|
-
|
485
|
-
// Note: Notice the pipe symbol `|`, it is intended to be here as a delimiter
|
486
|
-
// for the scenario where there are chunking happens
|
487
|
-
writeStream.write(`${previousChunk}|`);
|
580
|
+
queueWrite(' ]');
|
488
581
|
}
|
489
582
|
|
490
|
-
|
491
|
-
|
583
|
+
queueWrite('\n }');
|
584
|
+
if (i < keys.length - 1) {
|
585
|
+
queueWrite(',\n');
|
586
|
+
} else {
|
587
|
+
queueWrite('\n');
|
588
|
+
}
|
589
|
+
});
|
492
590
|
|
493
|
-
|
494
|
-
writeStream.write(previousChunk);
|
495
|
-
}
|
591
|
+
queueWrite('}\n');
|
496
592
|
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
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
|
+
};
|
501
601
|
|
502
|
-
|
503
|
-
}
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
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);
|
509
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
|
+
};
|
510
652
|
} catch (error) {
|
511
|
-
|
653
|
+
consoleLogger.error(`Error compressing and encoding ${filename}`);
|
512
654
|
throw error;
|
513
655
|
}
|
514
656
|
};
|
515
657
|
|
516
|
-
const streamEncodedDataToFile = async (
|
658
|
+
const streamEncodedDataToFile = async (
|
659
|
+
inputFilePath: string,
|
660
|
+
writeStream: fs.WriteStream,
|
661
|
+
appendComma: boolean,
|
662
|
+
) => {
|
517
663
|
const readStream = fs.createReadStream(inputFilePath, { encoding: 'utf8' });
|
518
664
|
let isFirstChunk = true;
|
519
665
|
|
@@ -531,35 +677,120 @@ const streamEncodedDataToFile = async (inputFilePath, writeStream, appendComma)
|
|
531
677
|
}
|
532
678
|
};
|
533
679
|
|
534
|
-
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
|
+
}> => {
|
535
693
|
const { items, ...rest } = allIssues;
|
536
|
-
const
|
537
|
-
|
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
|
+
};
|
538
770
|
|
771
|
+
const writeScanDetailsCsv = async (
|
772
|
+
scanDataFilePath: string,
|
773
|
+
scanItemsFilePath: string,
|
774
|
+
scanItemsSummaryFilePath: string,
|
775
|
+
storagePath: string,
|
776
|
+
) => {
|
539
777
|
const filePath = path.join(storagePath, 'scanDetails.csv');
|
778
|
+
const csvWriteStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
540
779
|
const directoryPath = path.dirname(filePath);
|
541
780
|
|
542
781
|
if (!fs.existsSync(directoryPath)) {
|
543
782
|
fs.mkdirSync(directoryPath, { recursive: true });
|
544
783
|
}
|
545
784
|
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
await streamEncodedDataToFile(
|
550
|
-
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);
|
551
789
|
|
552
790
|
await new Promise((resolve, reject) => {
|
553
791
|
csvWriteStream.end(resolve);
|
554
792
|
csvWriteStream.on('error', reject);
|
555
793
|
});
|
556
|
-
|
557
|
-
await fs.promises
|
558
|
-
.unlink(encodedScanDataPath)
|
559
|
-
.catch(err => console.error('Encoded file delete error:', err));
|
560
|
-
await fs.promises
|
561
|
-
.unlink(encodedScanItemsPath)
|
562
|
-
.catch(err => console.error('Encoded file delete error:', err));
|
563
794
|
};
|
564
795
|
|
565
796
|
let browserChannel = 'chrome';
|
@@ -572,12 +803,13 @@ if (os.platform() === 'linux') {
|
|
572
803
|
browserChannel = 'chromium';
|
573
804
|
}
|
574
805
|
|
575
|
-
const writeSummaryPdf = async (storagePath, pagesScanned, filename = 'summary') => {
|
806
|
+
const writeSummaryPdf = async (storagePath: string, pagesScanned: number, filename = 'summary') => {
|
576
807
|
const htmlFilePath = `${storagePath}/${filename}.html`;
|
577
808
|
const fileDestinationPath = `${storagePath}/${filename}.pdf`;
|
578
809
|
const browser = await chromium.launch({
|
579
|
-
headless:
|
810
|
+
headless: false,
|
580
811
|
channel: browserChannel,
|
812
|
+
args: ['--headless=new', '--no-sandbox'],
|
581
813
|
});
|
582
814
|
|
583
815
|
const context = await browser.newContext({
|
@@ -624,7 +856,12 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
624
856
|
Object.keys(pageResults.goodToFix.rules).forEach(k => totalIssuesInPage.add(k));
|
625
857
|
Object.keys(pageResults.needsReview.rules).forEach(k => totalIssuesInPage.add(k));
|
626
858
|
|
627
|
-
allIssues.topFiveMostIssues.push({
|
859
|
+
allIssues.topFiveMostIssues.push({
|
860
|
+
url,
|
861
|
+
pageTitle,
|
862
|
+
totalIssues: totalIssuesInPage.size,
|
863
|
+
totalOccurrences: 0,
|
864
|
+
});
|
628
865
|
|
629
866
|
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
630
867
|
if (!pageResults[category]) return;
|
@@ -706,9 +943,47 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
706
943
|
});
|
707
944
|
};
|
708
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
|
+
|
709
981
|
const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
982
|
+
const urlOccurrencesMap = new Map<string, number>();
|
983
|
+
|
710
984
|
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
711
985
|
allIssues.totalItems += allIssues.items[category].totalItems;
|
986
|
+
|
712
987
|
allIssues.items[category].rules = Object.entries(allIssues.items[category].rules)
|
713
988
|
.map(ruleEntry => {
|
714
989
|
const [rule, ruleInfo] = ruleEntry as [string, RuleInfo];
|
@@ -716,9 +991,14 @@ const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
|
716
991
|
.map(pageEntry => {
|
717
992
|
if (isCustomFlow) {
|
718
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
|
+
);
|
719
998
|
return { pageIndex, ...pageInfo };
|
720
999
|
}
|
721
1000
|
const [url, pageInfo] = pageEntry as unknown as [string, PageInfo];
|
1001
|
+
urlOccurrencesMap.set(url, (urlOccurrencesMap.get(url) || 0) + pageInfo.items.length);
|
722
1002
|
return { url, ...pageInfo };
|
723
1003
|
})
|
724
1004
|
.sort((page1, page2) => page2.items.length - page1.items.length);
|
@@ -726,8 +1006,19 @@ const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
|
726
1006
|
})
|
727
1007
|
.sort((rule1, rule2) => rule2.totalItems - rule1.totalItems);
|
728
1008
|
});
|
1009
|
+
|
1010
|
+
const updateIssuesWithOccurrences = (issuesList: Array<any>) => {
|
1011
|
+
issuesList.forEach(issue => {
|
1012
|
+
issue.totalOccurrences = urlOccurrencesMap.get(issue.url) || 0;
|
1013
|
+
});
|
1014
|
+
};
|
1015
|
+
|
729
1016
|
allIssues.topFiveMostIssues.sort((page1, page2) => page2.totalIssues - page1.totalIssues);
|
730
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;
|
731
1022
|
};
|
732
1023
|
|
733
1024
|
const createRuleIdJson = allIssues => {
|
@@ -827,6 +1118,7 @@ const generateArtifacts = async (
|
|
827
1118
|
endTime: scanDetails.endTime ? scanDetails.endTime : new Date(),
|
828
1119
|
urlScanned,
|
829
1120
|
scanType,
|
1121
|
+
deviceChosen: scanDetails.deviceChosen || 'Desktop',
|
830
1122
|
formatAboutStartTime,
|
831
1123
|
isCustomFlow,
|
832
1124
|
viewport,
|
@@ -836,21 +1128,43 @@ const generateArtifacts = async (
|
|
836
1128
|
totalPagesNotScanned: pagesNotScanned.length,
|
837
1129
|
totalItems: 0,
|
838
1130
|
topFiveMostIssues: [],
|
1131
|
+
topTenPagesWithMostIssues: [],
|
1132
|
+
topTenIssues: [],
|
839
1133
|
wcagViolations: [],
|
840
1134
|
customFlowLabel,
|
841
1135
|
phAppVersion,
|
842
1136
|
items: {
|
843
|
-
mustFix: {
|
844
|
-
|
845
|
-
|
846
|
-
|
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
|
+
},
|
847
1161
|
},
|
848
1162
|
cypressScanAboutMetadata,
|
849
1163
|
wcagLinks: constants.wcagLinks,
|
850
1164
|
// Populate boolean values for id="advancedScanOptionsSummary"
|
851
1165
|
advancedScanOptionsSummaryItems: {
|
852
1166
|
showIncludeScreenshots: [true].includes(scanDetails.isIncludeScreenshots),
|
853
|
-
showAllowSubdomains: [
|
1167
|
+
showAllowSubdomains: ['same-domain'].includes(scanDetails.isAllowSubdomains),
|
854
1168
|
showEnableCustomChecks: ['default', 'enable-wcag-aaa'].includes(
|
855
1169
|
scanDetails.isEnableCustomChecks?.[0],
|
856
1170
|
),
|
@@ -934,9 +1248,45 @@ const generateArtifacts = async (
|
|
934
1248
|
}
|
935
1249
|
|
936
1250
|
await writeCsv(allIssues, storagePath);
|
937
|
-
|
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
|
+
);
|
938
1270
|
await writeSummaryHTML(allIssues, storagePath);
|
939
|
-
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
|
+
|
940
1290
|
await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length), 1);
|
941
1291
|
|
942
1292
|
// Take option if set
|