@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.
Files changed (42) hide show
  1. package/.github/workflows/docker-test.yml +1 -1
  2. package/DETAILS.md +40 -25
  3. package/Dockerfile +41 -47
  4. package/LICENSE-3RD-PARTY-REPORT.txt +448 -0
  5. package/LICENSE-3RD-PARTY.txt +19913 -0
  6. package/README.md +26 -0
  7. package/__mocks__/mock-report.html +1503 -1360
  8. package/package.json +9 -5
  9. package/scripts/decodeUnzipParse.js +29 -0
  10. package/scripts/install_oobee_dependencies.command +2 -2
  11. package/scripts/install_oobee_dependencies.ps1 +3 -3
  12. package/src/cli.ts +9 -7
  13. package/src/combine.ts +13 -5
  14. package/src/constants/cliFunctions.ts +38 -1
  15. package/src/constants/common.ts +31 -5
  16. package/src/constants/constants.ts +28 -26
  17. package/src/constants/questions.ts +4 -1
  18. package/src/crawlers/commonCrawlerFunc.ts +114 -152
  19. package/src/crawlers/crawlDomain.ts +25 -32
  20. package/src/crawlers/crawlIntelligentSitemap.ts +7 -1
  21. package/src/crawlers/crawlLocalFile.ts +1 -1
  22. package/src/crawlers/crawlSitemap.ts +1 -1
  23. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +546 -472
  24. package/src/crawlers/customAxeFunctions.ts +1 -1
  25. package/src/index.ts +2 -2
  26. package/src/mergeAxeResults.ts +590 -214
  27. package/src/screenshotFunc/pdfScreenshotFunc.ts +3 -3
  28. package/src/static/ejs/partials/components/scanAbout.ejs +65 -0
  29. package/src/static/ejs/partials/components/wcagCompliance.ejs +10 -29
  30. package/src/static/ejs/partials/footer.ejs +10 -13
  31. package/src/static/ejs/partials/scripts/categorySummary.ejs +2 -2
  32. package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  33. package/src/static/ejs/partials/scripts/reportSearch.ejs +1 -0
  34. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +54 -52
  35. package/src/static/ejs/partials/scripts/scanAboutScript.ejs +38 -0
  36. package/src/static/ejs/partials/styles/styles.ejs +26 -1
  37. package/src/static/ejs/partials/summaryMain.ejs +15 -42
  38. package/src/static/ejs/report.ejs +22 -12
  39. package/src/utils.ts +10 -2
  40. package/src/xPathToCss.ts +186 -0
  41. package/a11y-scan-results.zip +0 -0
  42. package/src/types/xpath-to-css.d.ts +0 -3
@@ -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 { v4 as uuidv4 } from 'uuid';
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: { 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[] };
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
- // seems to go into
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, ' '); // remove newlines
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
- severity,
167
- issueId,
168
- issueDescription,
169
- wcagConformance,
170
- url,
171
- context,
172
- howToFix,
173
- axeImpact,
174
- xpath,
175
- learnMore,
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 (allIssues, storagePath, htmlFilename = 'report') => {
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
- const html = template(allIssues);
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
- } catch (error) {
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) + '\n\n';
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 (allIssues, storagePath, htmlFilename = 'report') => {
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 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',
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
- 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
- }
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
- 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
- }
323
+ outputStream.write(prefixData);
362
324
 
363
- chunkIndex++;
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
- inputStream.on('end', async () => {
368
- if (!isFirstField) {
369
- buffer += "')\n";
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
- inputStream.on('error', async err => {
381
- console.error('Error reading input file:', err);
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
- console.error('Error writing to output file:', err);
362
+ consoleLogger.error('Error writing to output file:', err);
389
363
  });
390
364
  };
391
365
 
392
- const writeSummaryHTML = async (allIssues, storagePath, htmlFilename = 'summary') => {
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
- 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
- }
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, writeStream, depth = 0, indent = ' ') {
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
- 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');
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.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);
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('Temporary file written successfully:', filePath);
438
+ consoleLogger.info(`JSON file written successfully: ${filePath}`);
452
439
  resolve(true);
453
440
  });
454
441
 
455
- serializeObject(obj, writeStream);
456
- writeStream.end();
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 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);
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
- await writeLargeJsonToFile(data, tempFilePath);
493
+ writeStream.on('error', error => {
494
+ consoleLogger.error(`Error writing object to JSON file: ${error}`);
495
+ reject(error);
496
+ });
471
497
 
472
- const outputFilename = `encoded_${uuidv4()}.txt`;
473
- const outputFilePath = path.join(process.cwd(), outputFilename);
498
+ writeStream.on('finish', () => {
499
+ consoleLogger.info(`JSON file written successfully: ${filePath}`);
500
+ resolve(true);
501
+ });
474
502
 
475
503
  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' });
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
- for await (const chunk of readStream) {
484
- const encodedChunk = Buffer.from(chunk).toString('base64');
485
- writeStream.write(`${encodedChunk}.`);
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
- await new Promise((resolve, reject) => {
489
- writeStream.end(resolve);
490
- writeStream.on('error', reject);
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
- return outputFilePath;
494
- } finally {
495
- await fs.promises
496
- .unlink(tempFilePath)
497
- .catch(err => console.error('Temp file delete error:', err));
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
- console.error('Error encoding data to Base64:', error);
653
+ consoleLogger.error(`Error compressing and encoding ${filename}`);
501
654
  throw error;
502
655
  }
503
656
  };
504
657
 
505
- const streamEncodedDataToFile = async (inputFilePath, writeStream, appendComma) => {
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 writeBase64 = async (allIssues, storagePath) => {
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 encodedScanItemsPath = await base64Encode(items, 1);
526
- const encodedScanDataPath = await base64Encode(rest, 2);
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
- 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);
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: true,
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({ url, pageTitle, totalIssues: totalIssuesInPage.size });
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: { 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: [] },
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
- await writeBase64(allIssues, storagePath);
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(allIssues, storagePath);
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