@govtechsg/oobee 0.10.83 → 0.10.84

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.
@@ -1,20 +1,23 @@
1
+ /* eslint-disable consistent-return */
2
+ /* eslint-disable no-console */
1
3
  import fs, { ensureDirSync } from 'fs-extra';
2
4
  import printMessage from 'print-message';
3
5
  import path from 'path';
4
6
  import ejs from 'ejs';
5
7
  import { fileURLToPath } from 'url';
6
- import { createWriteStream } from 'fs';
7
- import { AsyncParser } from '@json2csv/node';
8
- import zlib from 'zlib';
9
- import { Base64Encode } from 'base64-stream';
10
- import { pipeline } from 'stream/promises';
11
- import * as Sentry from '@sentry/node';
12
- import constants, { BrowserTypes, ScannerTypes, sentryConfig, setSentryUser, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, } from './constants/constants.js';
8
+ import constants, { BrowserTypes, ScannerTypes, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, } from './constants/constants.js';
13
9
  import { getBrowserToRun, getPlaywrightLaunchOptions } from './constants/common.js';
14
- import { createScreenshotsFolder, getStoragePath, getVersion, getWcagPassPercentage, getProgressPercentage, retryFunction, zipResults, getIssuesPercentage, getWcagCriteriaMap, categorizeWcagCriteria, getUserDataTxt, register, } from './utils.js';
10
+ import { createScreenshotsFolder, getStoragePath, getVersion, getWcagPassPercentage, getProgressPercentage, retryFunction, zipResults, getIssuesPercentage, register, } from './utils.js';
15
11
  import { consoleLogger } from './logs.js';
16
12
  import itemTypeDescription from './constants/itemTypeDescription.js';
17
13
  import { oobeeAiHtmlETL, oobeeAiRules } from './constants/oobeeAi.js';
14
+ import { buildHtmlGroups, convertItemsToReferences } from './mergeAxeResults/itemReferences.js';
15
+ import { compressJsonFileStreaming, writeJsonAndBase64Files, writeJsonFileAndCompressedJsonFile, } from './mergeAxeResults/jsonArtifacts.js';
16
+ import writeCsv from './mergeAxeResults/writeCsv.js';
17
+ import writeScanDetailsCsv from './mergeAxeResults/writeScanDetailsCsv.js';
18
+ import writeSitemap from './mergeAxeResults/writeSitemap.js';
19
+ import populateScanPagesDetail from './mergeAxeResults/scanPages.js';
20
+ import sendWcagBreakdownToSentry from './mergeAxeResults/sentryTelemetry.js';
18
21
  const filename = fileURLToPath(import.meta.url);
19
22
  const dirname = path.dirname(filename);
20
23
  const BUFFER_LIMIT = 100 * 1024 * 1024; // 100MB size
@@ -64,126 +67,6 @@ const parseContentToJson = async (rPath) => {
64
67
  return undefined;
65
68
  }
66
69
  };
67
- const writeCsv = async (allIssues, storagePath) => {
68
- const csvOutput = createWriteStream(`${storagePath}/report.csv`, { encoding: 'utf8' });
69
- const formatPageViolation = pageNum => {
70
- if (pageNum < 0)
71
- return 'Document';
72
- return `Page ${pageNum}`;
73
- };
74
- // transform allIssues into the form:
75
- // [['mustFix', rule1], ['mustFix', rule2], ['goodToFix', rule3], ...]
76
- const getRulesByCategory = (allIssues) => {
77
- return Object.entries(allIssues.items)
78
- .filter(([category]) => category !== 'passed')
79
- .reduce((prev, [category, value]) => {
80
- const rulesEntries = Object.entries(value.rules);
81
- rulesEntries.forEach(([, ruleInfo]) => {
82
- prev.push([category, ruleInfo]);
83
- });
84
- return prev;
85
- }, [])
86
- .sort((a, b) => {
87
- // sort rules according to severity, then ruleId
88
- const compareCategory = -a[0].localeCompare(b[0]);
89
- return compareCategory === 0 ? a[1].rule.localeCompare(b[1].rule) : compareCategory;
90
- });
91
- };
92
- const flattenRule = catAndRule => {
93
- const [severity, rule] = catAndRule;
94
- const results = [];
95
- const { rule: issueId, description: issueDescription, axeImpact, conformance, pagesAffected, helpUrl: learnMore, } = rule;
96
- // format clauses as a string
97
- const wcagConformance = conformance.join(',');
98
- pagesAffected.sort((a, b) => a.url.localeCompare(b.url));
99
- pagesAffected.forEach(affectedPage => {
100
- const { url, items } = affectedPage;
101
- items.forEach(item => {
102
- const { html, page, message, xpath } = item;
103
- const howToFix = message.replace(/(\r\n|\n|\r)/g, '\\n'); // preserve newlines as \n
104
- const violation = html || formatPageViolation(page); // page is a number, not a string
105
- const context = violation.replace(/(\r\n|\n|\r)/g, ''); // remove newlines
106
- results.push({
107
- customFlowLabel: allIssues.customFlowLabel || '',
108
- deviceChosen: allIssues.deviceChosen || '',
109
- scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
110
- severity: severity || '',
111
- issueId: issueId || '',
112
- issueDescription: a11yRuleShortDescriptionMap[issueId] || issueDescription || '',
113
- wcagConformance: wcagConformance || '',
114
- url: url || '',
115
- pageTitle: affectedPage.pageTitle || 'No page title',
116
- context: context || '',
117
- howToFix: howToFix || '',
118
- axeImpact: axeImpact || '',
119
- xpath: xpath || '',
120
- learnMore: learnMore || '',
121
- });
122
- });
123
- });
124
- if (results.length === 0)
125
- return {};
126
- return results;
127
- };
128
- const opts = {
129
- transforms: [getRulesByCategory, flattenRule],
130
- fields: [
131
- 'customFlowLabel',
132
- 'deviceChosen',
133
- 'scanCompletedAt',
134
- 'severity',
135
- 'issueId',
136
- 'issueDescription',
137
- 'wcagConformance',
138
- 'url',
139
- 'pageTitle',
140
- 'context',
141
- 'howToFix',
142
- 'axeImpact',
143
- 'xpath',
144
- 'learnMore',
145
- ],
146
- includeEmptyRows: true,
147
- };
148
- // Create the parse stream (it's asynchronous)
149
- const parser = new AsyncParser(opts);
150
- const parseStream = parser.parse(allIssues);
151
- // Pipe JSON2CSV output into the file, but don't end automatically
152
- parseStream.pipe(csvOutput, { end: false });
153
- // Once JSON2CSV is done writing all normal rows, append any "pagesNotScanned"
154
- parseStream.on('end', () => {
155
- if (allIssues.pagesNotScanned && allIssues.pagesNotScanned.length > 0) {
156
- csvOutput.write('\n');
157
- allIssues.pagesNotScanned.forEach(page => {
158
- const skippedPage = {
159
- customFlowLabel: allIssues.customFlowLabel || '',
160
- deviceChosen: allIssues.deviceChosen || '',
161
- scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
162
- severity: 'error',
163
- issueId: 'error-pages-skipped',
164
- issueDescription: page.metadata
165
- ? page.metadata
166
- : 'An unknown error caused the page to be skipped',
167
- wcagConformance: '',
168
- url: page.url || page || '',
169
- pageTitle: 'Error',
170
- context: '',
171
- howToFix: '',
172
- axeImpact: '',
173
- xpath: '',
174
- learnMore: '',
175
- };
176
- csvOutput.write(`${Object.values(skippedPage).join(',')}\n`);
177
- });
178
- }
179
- // Now close the CSV file
180
- csvOutput.end();
181
- });
182
- parseStream.on('error', err => {
183
- console.error('Error parsing CSV:', err);
184
- csvOutput.end();
185
- });
186
- };
187
70
  const compileHtmlWithEJS = async (allIssues, storagePath, htmlFilename = 'report') => {
188
71
  const htmlFilePath = `${path.join(storagePath, htmlFilename)}.html`;
189
72
  const ejsString = fs.readFileSync(path.join(dirname, './static/ejs/report.ejs'), 'utf-8');
@@ -239,7 +122,7 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
239
122
  // Create lighter version with item references for embedding in HTML
240
123
  const scanItemsWithHtmlGroupRefs = convertItemsToReferences(allIssues);
241
124
  // Write the lighter items to a file and get the base64 path
242
- const { jsonFilePath: scanItemsWithHtmlGroupRefsJsonFilePath, base64FilePath: scanItemsWithHtmlGroupRefsBase64FilePath } = await writeJsonFileAndCompressedJsonFile(scanItemsWithHtmlGroupRefs.items, storagePath, 'scanItems-light');
125
+ const { jsonFilePath: scanItemsWithHtmlGroupRefsJsonFilePath, base64FilePath: scanItemsWithHtmlGroupRefsBase64FilePath, } = await writeJsonFileAndCompressedJsonFile(scanItemsWithHtmlGroupRefs.items, storagePath, 'scanItems-light');
243
126
  return new Promise((resolve, reject) => {
244
127
  const scanDetailsReadStream = fs.createReadStream(scanDetailsFilePath, {
245
128
  encoding: 'utf8',
@@ -262,7 +145,7 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
262
145
  };
263
146
  outputStream.write(prefixData);
264
147
  // For Proxied AI environments only
265
- outputStream.write(`let proxyUrl = "${process.env.PROXY_API_BASE_URL || ""}"\n`);
148
+ outputStream.write(`let proxyUrl = "${process.env.PROXY_API_BASE_URL || ''}"\n`);
266
149
  // Initialize GenAI feature flag
267
150
  outputStream.write(`
268
151
  // Fetch GenAI feature flag from backend
@@ -290,12 +173,12 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
290
173
  console.warn('Skipping fetch GenAI feature as it is local report');
291
174
  }
292
175
  \n`);
293
- outputStream.write("</script>\n<script type=\"text/plain\" id=\"scanDataRaw\">");
176
+ outputStream.write('</script>\n<script type="text/plain" id="scanDataRaw">');
294
177
  scanDetailsReadStream.pipe(outputStream, { end: false });
295
178
  scanDetailsReadStream.on('end', async () => {
296
- outputStream.write("</script>\n<script>\n");
179
+ outputStream.write('</script>\n<script>\n');
297
180
  outputStream.write("var scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse(document.getElementById('scanDataRaw').textContent); })();\n");
298
- outputStream.write("</script>\n");
181
+ outputStream.write('</script>\n');
299
182
  // Write scanItems in 2MB chunks using a stream to avoid loading entire file into memory
300
183
  try {
301
184
  let chunkIndex = 1;
@@ -307,7 +190,7 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
307
190
  outputStream.write(`<script type="text/plain" id="scanItemsRaw${chunkIndex}">${chunk}</script>\n`);
308
191
  chunkIndex++;
309
192
  }
310
- outputStream.write("<script>\n");
193
+ outputStream.write('<script>\n');
311
194
  outputStream.write(`
312
195
  var scanItemsPromise = (async () => {
313
196
  console.log('Loading scanItems...');
@@ -356,12 +239,6 @@ const writeSummaryHTML = async (allIssues, storagePath, htmlFilename = 'summary'
356
239
  const html = template(allIssues);
357
240
  fs.writeFileSync(`${storagePath}/${htmlFilename}.html`, html);
358
241
  };
359
- const writeSitemap = async (pagesScanned, storagePath) => {
360
- const sitemapPath = path.join(storagePath, 'sitemap.txt');
361
- const content = pagesScanned.map(p => p.url).join('\n');
362
- await fs.writeFile(sitemapPath, content, { encoding: 'utf-8' });
363
- consoleLogger.info(`Sitemap written to ${sitemapPath}`);
364
- };
365
242
  const cleanUpJsonFiles = async (filesToDelete) => {
366
243
  consoleLogger.info('Cleaning up JSON files...');
367
244
  filesToDelete.forEach(file => {
@@ -369,379 +246,6 @@ const cleanUpJsonFiles = async (filesToDelete) => {
369
246
  consoleLogger.info(`Deleted ${file}`);
370
247
  });
371
248
  };
372
- function* serializeObject(obj, depth = 0, indent = ' ') {
373
- const currentIndent = indent.repeat(depth);
374
- const nextIndent = indent.repeat(depth + 1);
375
- if (obj instanceof Date) {
376
- yield JSON.stringify(obj.toISOString());
377
- return;
378
- }
379
- if (Array.isArray(obj)) {
380
- yield '[\n';
381
- for (let i = 0; i < obj.length; i++) {
382
- if (i > 0)
383
- yield ',\n';
384
- yield nextIndent;
385
- yield* serializeObject(obj[i], depth + 1, indent);
386
- }
387
- yield `\n${currentIndent}]`;
388
- return;
389
- }
390
- if (obj !== null && typeof obj === 'object') {
391
- yield '{\n';
392
- const keys = Object.keys(obj);
393
- for (let i = 0; i < keys.length; i++) {
394
- const key = keys[i];
395
- if (i > 0)
396
- yield ',\n';
397
- yield `${nextIndent}${JSON.stringify(key)}: `;
398
- yield* serializeObject(obj[key], depth + 1, indent);
399
- }
400
- yield `\n${currentIndent}}`;
401
- return;
402
- }
403
- if (obj === null || typeof obj === 'function' || typeof obj === 'undefined') {
404
- yield 'null';
405
- return;
406
- }
407
- yield JSON.stringify(obj);
408
- }
409
- function writeLargeJsonToFile(obj, filePath) {
410
- return new Promise((resolve, reject) => {
411
- const writeStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
412
- writeStream.on('error', error => {
413
- consoleLogger.error('Stream error:', error);
414
- reject(error);
415
- });
416
- writeStream.on('finish', () => {
417
- consoleLogger.info(`JSON file written successfully: ${filePath}`);
418
- resolve(true);
419
- });
420
- const generator = serializeObject(obj);
421
- function write() {
422
- let next;
423
- while (!(next = generator.next()).done) {
424
- if (!writeStream.write(next.value)) {
425
- writeStream.once('drain', write);
426
- return;
427
- }
428
- }
429
- writeStream.end();
430
- }
431
- write();
432
- });
433
- }
434
- const writeLargeScanItemsJsonToFile = async (obj, filePath) => {
435
- return new Promise((resolve, reject) => {
436
- const writeStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
437
- const writeQueue = [];
438
- let isWriting = false;
439
- const processNextWrite = async () => {
440
- if (isWriting || writeQueue.length === 0)
441
- return;
442
- isWriting = true;
443
- const data = writeQueue.shift();
444
- try {
445
- if (!writeStream.write(data)) {
446
- await new Promise(resolve => {
447
- writeStream.once('drain', () => {
448
- resolve();
449
- });
450
- });
451
- }
452
- }
453
- catch (error) {
454
- writeStream.destroy(error);
455
- return;
456
- }
457
- isWriting = false;
458
- processNextWrite();
459
- };
460
- const queueWrite = (data) => {
461
- writeQueue.push(data);
462
- processNextWrite();
463
- };
464
- writeStream.on('error', error => {
465
- consoleLogger.error(`Error writing object to JSON file: ${error}`);
466
- reject(error);
467
- });
468
- writeStream.on('finish', () => {
469
- consoleLogger.info(`JSON file written successfully: ${filePath}`);
470
- resolve(true);
471
- });
472
- try {
473
- queueWrite('{\n');
474
- const keys = Object.keys(obj);
475
- keys.forEach((key, i) => {
476
- const value = obj[key];
477
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
478
- queueWrite(` "${key}": ${JSON.stringify(value)}`);
479
- }
480
- else {
481
- queueWrite(` "${key}": {\n`);
482
- const { rules, ...otherProperties } = value;
483
- // Write other properties
484
- Object.entries(otherProperties).forEach(([propKey, propValue], j) => {
485
- const propValueString = propValue === null ||
486
- typeof propValue === 'function' ||
487
- typeof propValue === 'undefined'
488
- ? 'null'
489
- : JSON.stringify(propValue);
490
- queueWrite(` "${propKey}": ${propValueString}`);
491
- if (j < Object.keys(otherProperties).length - 1 || (rules && rules.length >= 0)) {
492
- queueWrite(',\n');
493
- }
494
- else {
495
- queueWrite('\n');
496
- }
497
- });
498
- if (rules && Array.isArray(rules)) {
499
- queueWrite(' "rules": [\n');
500
- rules.forEach((rule, j) => {
501
- queueWrite(' {\n');
502
- const { pagesAffected, ...otherRuleProperties } = rule;
503
- Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
504
- const ruleValueString = ruleValue === null ||
505
- typeof ruleValue === 'function' ||
506
- typeof ruleValue === 'undefined'
507
- ? 'null'
508
- : JSON.stringify(ruleValue);
509
- queueWrite(` "${ruleKey}": ${ruleValueString}`);
510
- if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
511
- queueWrite(',\n');
512
- }
513
- else {
514
- queueWrite('\n');
515
- }
516
- });
517
- if (pagesAffected && Array.isArray(pagesAffected)) {
518
- queueWrite(' "pagesAffected": [\n');
519
- pagesAffected.forEach((page, p) => {
520
- const pageJson = JSON.stringify(page, null, 2)
521
- .split('\n')
522
- .map((line, idx) => (idx === 0 ? ` ${line}` : ` ${line}`))
523
- .join('\n');
524
- queueWrite(pageJson);
525
- if (p < pagesAffected.length - 1) {
526
- queueWrite(',\n');
527
- }
528
- else {
529
- queueWrite('\n');
530
- }
531
- });
532
- queueWrite(' ]');
533
- }
534
- queueWrite('\n }');
535
- if (j < rules.length - 1) {
536
- queueWrite(',\n');
537
- }
538
- else {
539
- queueWrite('\n');
540
- }
541
- });
542
- queueWrite(' ]');
543
- }
544
- queueWrite('\n }');
545
- }
546
- if (i < keys.length - 1) {
547
- queueWrite(',\n');
548
- }
549
- else {
550
- queueWrite('\n');
551
- }
552
- });
553
- queueWrite('}\n');
554
- // Ensure all queued writes are processed before ending
555
- const checkQueueAndEnd = () => {
556
- if (writeQueue.length === 0 && !isWriting) {
557
- writeStream.end();
558
- }
559
- else {
560
- setTimeout(checkQueueAndEnd, 100);
561
- }
562
- };
563
- checkQueueAndEnd();
564
- }
565
- catch (err) {
566
- writeStream.destroy(err);
567
- reject(err);
568
- }
569
- });
570
- };
571
- async function compressJsonFileStreaming(inputPath, outputPath) {
572
- // Create the read and write streams
573
- const readStream = fs.createReadStream(inputPath);
574
- const writeStream = fs.createWriteStream(outputPath);
575
- // Create a gzip transform stream
576
- const gzip = zlib.createGzip();
577
- // Create a Base64 transform stream
578
- const base64Encode = new Base64Encode();
579
- // Pipe the streams:
580
- // read -> gzip -> base64 -> write
581
- await pipeline(readStream, gzip, base64Encode, writeStream);
582
- consoleLogger.info(`File successfully compressed and saved to ${outputPath}`);
583
- }
584
- const writeJsonFileAndCompressedJsonFile = async (data, storagePath, filename) => {
585
- try {
586
- consoleLogger.info(`Writing JSON to ${filename}.json`);
587
- const jsonFilePath = path.join(storagePath, `${filename}.json`);
588
- if (filename === 'scanItems') {
589
- await writeLargeScanItemsJsonToFile(data, jsonFilePath);
590
- }
591
- else {
592
- await writeLargeJsonToFile(data, jsonFilePath);
593
- }
594
- consoleLogger.info(`Reading ${filename}.json, gzipping and base64 encoding it into ${filename}.json.gz.b64`);
595
- const base64FilePath = path.join(storagePath, `${filename}.json.gz.b64`);
596
- await compressJsonFileStreaming(jsonFilePath, base64FilePath);
597
- consoleLogger.info(`Finished compression and base64 encoding for ${filename}`);
598
- return {
599
- jsonFilePath,
600
- base64FilePath,
601
- };
602
- }
603
- catch (error) {
604
- consoleLogger.error(`Error compressing and encoding ${filename}`);
605
- throw error;
606
- }
607
- };
608
- const streamEncodedDataToFile = async (inputFilePath, writeStream, appendComma) => {
609
- const readStream = fs.createReadStream(inputFilePath, { encoding: 'utf8' });
610
- let isFirstChunk = true;
611
- for await (const chunk of readStream) {
612
- if (isFirstChunk) {
613
- isFirstChunk = false;
614
- writeStream.write(chunk);
615
- }
616
- else {
617
- writeStream.write(chunk);
618
- }
619
- }
620
- if (appendComma) {
621
- writeStream.write(',');
622
- }
623
- };
624
- const writeJsonAndBase64Files = async (allIssues, storagePath) => {
625
- const { items, ...rest } = allIssues;
626
- const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } = await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
627
- const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...items }, storagePath, 'scanItems');
628
- // Add pagesAffectedCount to each rule in items and sort them in descending order of pagesAffectedCount
629
- ['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
630
- if (items[category].rules && Array.isArray(items[category].rules)) {
631
- items[category].rules.forEach(rule => {
632
- rule.pagesAffectedCount = Array.isArray(rule.pagesAffected) ? rule.pagesAffected.length : 0;
633
- });
634
- // Sort in descending order of pagesAffectedCount
635
- items[category].rules.sort((a, b) => (b.pagesAffectedCount || 0) - (a.pagesAffectedCount || 0));
636
- }
637
- });
638
- // Refactor scanIssuesSummary to reuse the structure by stripping out pagesAffected
639
- const scanIssuesSummary = {
640
- // Replace rule descriptions with short descriptions from the map
641
- mustFix: items.mustFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
642
- ...ruleInfo,
643
- description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
644
- })),
645
- goodToFix: items.goodToFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
646
- ...ruleInfo,
647
- description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
648
- })),
649
- needsReview: items.needsReview.rules.map(({ pagesAffected, ...ruleInfo }) => ({
650
- ...ruleInfo,
651
- description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
652
- })),
653
- passed: items.passed.rules.map(({ pagesAffected, ...ruleInfo }) => ({
654
- ...ruleInfo,
655
- description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
656
- })),
657
- };
658
- // Write out the scanIssuesSummary JSON using the new structure
659
- const { jsonFilePath: scanIssuesSummaryJsonFilePath, base64FilePath: scanIssuesSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanIssuesSummary }, storagePath, 'scanIssuesSummary');
660
- // scanItemsSummary
661
- // the below mutates the original items object, since it is expensive to clone
662
- items.mustFix.rules.forEach(rule => {
663
- rule.pagesAffected.forEach(page => {
664
- page.itemsCount = page.items.length;
665
- });
666
- });
667
- items.goodToFix.rules.forEach(rule => {
668
- rule.pagesAffected.forEach(page => {
669
- page.itemsCount = page.items.length;
670
- });
671
- });
672
- items.needsReview.rules.forEach(rule => {
673
- rule.pagesAffected.forEach(page => {
674
- page.itemsCount = page.items.length;
675
- });
676
- });
677
- items.passed.rules.forEach(rule => {
678
- rule.pagesAffected.forEach(page => {
679
- page.itemsCount = page.items.length;
680
- });
681
- });
682
- items.mustFix.totalRuleIssues = items.mustFix.rules.length;
683
- items.goodToFix.totalRuleIssues = items.goodToFix.rules.length;
684
- items.needsReview.totalRuleIssues = items.needsReview.rules.length;
685
- items.passed.totalRuleIssues = items.passed.rules.length;
686
- const { pagesScanned, topTenPagesWithMostIssues, pagesNotScanned, wcagLinks, wcagPassPercentage, progressPercentage, issuesPercentage, totalPagesScanned, totalPagesNotScanned, topTenIssues, } = rest;
687
- const summaryItems = {
688
- mustFix: {
689
- totalItems: items.mustFix?.totalItems || 0,
690
- totalRuleIssues: items.mustFix?.totalRuleIssues || 0,
691
- },
692
- goodToFix: {
693
- totalItems: items.goodToFix?.totalItems || 0,
694
- totalRuleIssues: items.goodToFix?.totalRuleIssues || 0,
695
- },
696
- needsReview: {
697
- totalItems: items.needsReview?.totalItems || 0,
698
- totalRuleIssues: items.needsReview?.totalRuleIssues || 0,
699
- },
700
- topTenPagesWithMostIssues,
701
- wcagLinks,
702
- wcagPassPercentage,
703
- progressPercentage,
704
- issuesPercentage,
705
- totalPagesScanned,
706
- totalPagesNotScanned,
707
- topTenIssues,
708
- };
709
- const { jsonFilePath: scanItemsSummaryJsonFilePath, base64FilePath: scanItemsSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItems }, storagePath, 'scanItemsSummary');
710
- const { jsonFilePath: scanPagesDetailJsonFilePath, base64FilePath: scanPagesDetailBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesDetail }, storagePath, 'scanPagesDetail');
711
- const { jsonFilePath: scanPagesSummaryJsonFilePath, base64FilePath: scanPagesSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesSummary }, storagePath, 'scanPagesSummary');
712
- return {
713
- scanDataJsonFilePath,
714
- scanDataBase64FilePath,
715
- scanItemsJsonFilePath,
716
- scanItemsBase64FilePath,
717
- scanItemsSummaryJsonFilePath,
718
- scanItemsSummaryBase64FilePath,
719
- scanIssuesSummaryJsonFilePath,
720
- scanIssuesSummaryBase64FilePath,
721
- scanPagesDetailJsonFilePath,
722
- scanPagesDetailBase64FilePath,
723
- scanPagesSummaryJsonFilePath,
724
- scanPagesSummaryBase64FilePath,
725
- scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
726
- scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
727
- };
728
- };
729
- const writeScanDetailsCsv = async (scanDataFilePath, scanItemsFilePath, scanItemsSummaryFilePath, storagePath) => {
730
- const filePath = path.join(storagePath, 'scanDetails.csv');
731
- const csvWriteStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
732
- const directoryPath = path.dirname(filePath);
733
- if (!fs.existsSync(directoryPath)) {
734
- fs.mkdirSync(directoryPath, { recursive: true });
735
- }
736
- csvWriteStream.write('scanData_base64,scanItems_base64,scanItemsSummary_base64\n');
737
- await streamEncodedDataToFile(scanDataFilePath, csvWriteStream, true);
738
- await streamEncodedDataToFile(scanItemsFilePath, csvWriteStream, true);
739
- await streamEncodedDataToFile(scanItemsSummaryFilePath, csvWriteStream, false);
740
- await new Promise((resolve, reject) => {
741
- csvWriteStream.end(resolve);
742
- csvWriteStream.on('error', reject);
743
- });
744
- };
745
249
  const writeSummaryPdf = async (storagePath, pagesScanned, filename = 'summary', browser, _userDataDirectory) => {
746
250
  const htmlFilePath = `${storagePath}/${filename}.html`;
747
251
  const fileDestinationPath = `${storagePath}/${filename}.pdf`;
@@ -776,16 +280,6 @@ const writeSummaryPdf = async (storagePath, pagesScanned, filename = 'summary',
776
280
  };
777
281
  // Tracking WCAG occurrences
778
282
  const wcagOccurrencesMap = new Map();
779
- // Format WCAG tag in requested format: wcag111a_Occurrences
780
- const formatWcagTag = async (wcagId) => {
781
- // Get dynamic WCAG criteria map
782
- const wcagCriteriaMap = await getWcagCriteriaMap();
783
- if (wcagCriteriaMap[wcagId]) {
784
- const { level } = wcagCriteriaMap[wcagId];
785
- return `${wcagId}${level}_Occurrences`;
786
- }
787
- return null;
788
- };
789
283
  const pushResults = async (pageResults, allIssues, isCustomFlow) => {
790
284
  const { url, pageTitle, filePath } = pageResults;
791
285
  const totalIssuesInPage = new Set();
@@ -843,73 +337,16 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
843
337
  items: [...items],
844
338
  };
845
339
  }
846
- else {
847
- if (!(url in currRuleFromAllIssues.pagesAffected)) {
848
- currRuleFromAllIssues.pagesAffected[url] = {
849
- pageTitle,
850
- items: [...items],
851
- ...(filePath && { filePath }),
852
- };
853
- }
340
+ else if (!(url in currRuleFromAllIssues.pagesAffected)) {
341
+ currRuleFromAllIssues.pagesAffected[url] = {
342
+ pageTitle,
343
+ items: [...items],
344
+ ...(filePath && { filePath }),
345
+ };
854
346
  }
855
347
  });
856
348
  });
857
349
  };
858
- /**
859
- * Builds pre-computed HTML groups to optimize Group by HTML Element functionality.
860
- * Keys are composite "html\x00xpath" strings to ensure unique matching per element instance.
861
- */
862
- const buildHtmlGroups = (rule, items, pageUrl) => {
863
- if (!rule.htmlGroups) {
864
- rule.htmlGroups = {};
865
- }
866
- items.forEach(item => {
867
- // Use composite key of html + xpath for precise matching
868
- const htmlKey = `${item.html || 'No HTML element'}\x00${item.xpath || ''}`;
869
- if (!rule.htmlGroups[htmlKey]) {
870
- // Create new group with the first occurrence
871
- rule.htmlGroups[htmlKey] = {
872
- html: item.html || '',
873
- xpath: item.xpath || '',
874
- message: item.message || '',
875
- screenshotPath: item.screenshotPath || '',
876
- displayNeedsReview: item.displayNeedsReview,
877
- pageUrls: [],
878
- };
879
- }
880
- if (!rule.htmlGroups[htmlKey].pageUrls.includes(pageUrl)) {
881
- rule.htmlGroups[htmlKey].pageUrls.push(pageUrl);
882
- }
883
- });
884
- };
885
- /**
886
- * Converts items in pagesAffected to references (html\x00xpath composite keys) for embedding in HTML report.
887
- * Additionally, it deep-clones allIssues, replaces page.items objects with composite reference keys.
888
- * Those refs are specifically for htmlGroups lookup (html + xpath).
889
- */
890
- export const convertItemsToReferences = (allIssues) => {
891
- const cloned = JSON.parse(JSON.stringify(allIssues));
892
- ['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
893
- if (!cloned.items[category]?.rules)
894
- return;
895
- cloned.items[category].rules.forEach((rule) => {
896
- if (!rule.pagesAffected || !rule.htmlGroups)
897
- return;
898
- rule.pagesAffected.forEach((page) => {
899
- if (!page.items)
900
- return;
901
- page.items = page.items.map((item) => {
902
- if (typeof item === 'string')
903
- return item; // Already a reference
904
- // Use composite key matching buildHtmlGroups
905
- const htmlKey = `${item.html || 'No HTML element'}\x00${item.xpath || ''}`;
906
- return htmlKey;
907
- });
908
- });
909
- });
910
- });
911
- return cloned;
912
- };
913
350
  const getTopTenIssues = allIssues => {
914
351
  const categories = ['mustFix', 'goodToFix'];
915
352
  const rulesWithCounts = [];
@@ -1048,348 +485,6 @@ const moveElemScreenshots = (randomToken, storagePath) => {
1048
485
  fs.moveSync(currentScreenshotsPath, resultsScreenshotsPath);
1049
486
  }
1050
487
  };
1051
- /**
1052
- * Build allIssues.scanPagesDetail and allIssues.scanPagesSummary
1053
- * by analyzing pagesScanned (including mustFix/goodToFix/etc.).
1054
- */
1055
- function populateScanPagesDetail(allIssues) {
1056
- // --------------------------------------------
1057
- // 1) Gather your "scanned" pages from allIssues
1058
- // --------------------------------------------
1059
- const allScannedPages = Array.isArray(allIssues.pagesScanned) ? allIssues.pagesScanned : [];
1060
- // --------------------------------------------
1061
- // 2) Define category constants (optional, just for clarity)
1062
- // --------------------------------------------
1063
- const mustFixCategory = 'mustFix';
1064
- const goodToFixCategory = 'goodToFix';
1065
- const needsReviewCategory = 'needsReview';
1066
- const passedCategory = 'passed';
1067
- // --------------------------------------------
1068
- // 4) We'll accumulate pages in a map keyed by URL
1069
- // --------------------------------------------
1070
- const pagesMap = {};
1071
- // --------------------------------------------
1072
- // 5) Build pagesMap by iterating over each category in allIssues.items
1073
- // --------------------------------------------
1074
- Object.entries(allIssues.items).forEach(([categoryName, categoryData]) => {
1075
- if (!categoryData?.rules)
1076
- return; // no rules in this category? skip
1077
- categoryData.rules.forEach(rule => {
1078
- const { rule: ruleId, conformance = [] } = rule;
1079
- rule.pagesAffected.forEach(p => {
1080
- const { url, pageTitle, items = [] } = p;
1081
- const itemsCount = items.length;
1082
- // Ensure the page is in pagesMap
1083
- if (!pagesMap[url]) {
1084
- pagesMap[url] = {
1085
- pageTitle,
1086
- url,
1087
- totalOccurrencesFailedIncludingNeedsReview: 0,
1088
- totalOccurrencesFailedExcludingNeedsReview: 0,
1089
- totalOccurrencesNeedsReview: 0,
1090
- totalOccurrencesPassed: 0,
1091
- typesOfIssues: {},
1092
- };
1093
- }
1094
- // Ensure the rule is present for this page
1095
- if (!pagesMap[url].typesOfIssues[ruleId]) {
1096
- pagesMap[url].typesOfIssues[ruleId] = {
1097
- ruleId,
1098
- wcagConformance: conformance,
1099
- occurrencesMustFix: 0,
1100
- occurrencesGoodToFix: 0,
1101
- occurrencesNeedsReview: 0,
1102
- occurrencesPassed: 0,
1103
- };
1104
- }
1105
- // Depending on the category, increment the relevant occurrence counts
1106
- if (categoryName === mustFixCategory) {
1107
- pagesMap[url].typesOfIssues[ruleId].occurrencesMustFix += itemsCount;
1108
- pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1109
- pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
1110
- }
1111
- else if (categoryName === goodToFixCategory) {
1112
- pagesMap[url].typesOfIssues[ruleId].occurrencesGoodToFix += itemsCount;
1113
- pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1114
- pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
1115
- }
1116
- else if (categoryName === needsReviewCategory) {
1117
- pagesMap[url].typesOfIssues[ruleId].occurrencesNeedsReview += itemsCount;
1118
- pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1119
- pagesMap[url].totalOccurrencesNeedsReview += itemsCount;
1120
- }
1121
- else if (categoryName === passedCategory) {
1122
- pagesMap[url].typesOfIssues[ruleId].occurrencesPassed += itemsCount;
1123
- pagesMap[url].totalOccurrencesPassed += itemsCount;
1124
- }
1125
- });
1126
- });
1127
- });
1128
- // --------------------------------------------
1129
- // 6) Separate scanned pages into “affected” vs. “notAffected”
1130
- // --------------------------------------------
1131
- const pagesInMap = Object.values(pagesMap); // All pages that have some record in pagesMap
1132
- const pagesInMapUrls = new Set(Object.keys(pagesMap));
1133
- // (a) Pages with only passed (no mustFix/goodToFix/needsReview)
1134
- const pagesAllPassed = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview === 0);
1135
- // (b) Pages that do NOT appear in pagesMap at all => scanned but no items found
1136
- const pagesNoEntries = allScannedPages
1137
- .filter(sp => !pagesInMapUrls.has(sp.url))
1138
- .map(sp => ({
1139
- pageTitle: sp.pageTitle,
1140
- url: sp.url,
1141
- totalOccurrencesFailedIncludingNeedsReview: 0,
1142
- totalOccurrencesFailedExcludingNeedsReview: 0,
1143
- totalOccurrencesNeedsReview: 0,
1144
- totalOccurrencesPassed: 0,
1145
- typesOfIssues: {},
1146
- }));
1147
- // Combine these into "notAffected"
1148
- const pagesNotAffectedRaw = [...pagesAllPassed, ...pagesNoEntries];
1149
- // "affected" pages => have at least 1 mustFix/goodToFix/needsReview
1150
- const pagesAffectedRaw = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview > 0);
1151
- // --------------------------------------------
1152
- // 7) Transform both arrays to the final shape
1153
- // --------------------------------------------
1154
- function transformPageData(page) {
1155
- const typesOfIssuesArray = Object.values(page.typesOfIssues);
1156
- // Compute sums for each failing category
1157
- const mustFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesMustFix, 0);
1158
- const goodToFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesGoodToFix, 0);
1159
- const needsReviewSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesNeedsReview, 0);
1160
- // Build categoriesPresent based on nonzero failing counts
1161
- const categoriesPresent = [];
1162
- if (mustFixSum > 0)
1163
- categoriesPresent.push('mustFix');
1164
- if (goodToFixSum > 0)
1165
- categoriesPresent.push('goodToFix');
1166
- if (needsReviewSum > 0)
1167
- categoriesPresent.push('needsReview');
1168
- // Count how many rules have failing issues
1169
- const failedRuleIds = new Set();
1170
- typesOfIssuesArray.forEach(r => {
1171
- if ((r.occurrencesMustFix || 0) > 0 ||
1172
- (r.occurrencesGoodToFix || 0) > 0 ||
1173
- (r.occurrencesNeedsReview || 0) > 0) {
1174
- failedRuleIds.add(r.ruleId); // Ensure ruleId is unique
1175
- }
1176
- });
1177
- const failedRuleCount = failedRuleIds.size;
1178
- // Possibly these two for future convenience
1179
- const typesOfIssuesExcludingNeedsReviewCount = typesOfIssuesArray.filter(r => (r.occurrencesMustFix || 0) + (r.occurrencesGoodToFix || 0) > 0).length;
1180
- const typesOfIssuesExclusiveToNeedsReviewCount = typesOfIssuesArray.filter(r => (r.occurrencesNeedsReview || 0) > 0 &&
1181
- (r.occurrencesMustFix || 0) === 0 &&
1182
- (r.occurrencesGoodToFix || 0) === 0).length;
1183
- // Aggregate wcagConformance for rules that actually fail
1184
- const allConformance = typesOfIssuesArray.reduce((acc, curr) => {
1185
- const nonPassedCount = (curr.occurrencesMustFix || 0) +
1186
- (curr.occurrencesGoodToFix || 0) +
1187
- (curr.occurrencesNeedsReview || 0);
1188
- if (nonPassedCount > 0) {
1189
- return acc.concat(curr.wcagConformance || []);
1190
- }
1191
- return acc;
1192
- }, []);
1193
- // Remove duplicates
1194
- const conformance = Array.from(new Set(allConformance));
1195
- return {
1196
- pageTitle: page.pageTitle,
1197
- url: page.url,
1198
- totalOccurrencesFailedIncludingNeedsReview: page.totalOccurrencesFailedIncludingNeedsReview,
1199
- totalOccurrencesFailedExcludingNeedsReview: page.totalOccurrencesFailedExcludingNeedsReview,
1200
- totalOccurrencesMustFix: mustFixSum,
1201
- totalOccurrencesGoodToFix: goodToFixSum,
1202
- totalOccurrencesNeedsReview: needsReviewSum,
1203
- totalOccurrencesPassed: page.totalOccurrencesPassed,
1204
- typesOfIssuesExclusiveToNeedsReviewCount,
1205
- typesOfIssuesCount: failedRuleCount,
1206
- typesOfIssuesExcludingNeedsReviewCount,
1207
- categoriesPresent,
1208
- conformance,
1209
- // Keep full detail for "scanPagesDetail"
1210
- typesOfIssues: typesOfIssuesArray,
1211
- };
1212
- }
1213
- // Transform raw pages
1214
- const pagesAffected = pagesAffectedRaw.map(transformPageData);
1215
- const pagesNotAffected = pagesNotAffectedRaw.map(transformPageData);
1216
- // --------------------------------------------
1217
- // 8) Sort pages by typesOfIssuesCount (descending) for both arrays
1218
- // --------------------------------------------
1219
- pagesAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
1220
- pagesNotAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
1221
- // --------------------------------------------
1222
- // 9) Compute scanned/ skipped counts
1223
- // --------------------------------------------
1224
- const scannedPagesCount = pagesAffected.length + pagesNotAffected.length;
1225
- const pagesNotScannedCount = Array.isArray(allIssues.pagesNotScanned)
1226
- ? allIssues.pagesNotScanned.length
1227
- : 0;
1228
- // --------------------------------------------
1229
- // 10) Build scanPagesDetail (with full "typesOfIssues")
1230
- // --------------------------------------------
1231
- allIssues.scanPagesDetail = {
1232
- pagesAffected,
1233
- pagesNotAffected,
1234
- scannedPagesCount,
1235
- pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
1236
- pagesNotScannedCount,
1237
- };
1238
- // --------------------------------------------
1239
- // 11) Build scanPagesSummary (strip out "typesOfIssues")
1240
- // --------------------------------------------
1241
- function stripTypesOfIssues(page) {
1242
- const { typesOfIssues, ...rest } = page;
1243
- return rest;
1244
- }
1245
- const summaryPagesAffected = pagesAffected.map(stripTypesOfIssues);
1246
- const summaryPagesNotAffected = pagesNotAffected.map(stripTypesOfIssues);
1247
- allIssues.scanPagesSummary = {
1248
- pagesAffected: summaryPagesAffected,
1249
- pagesNotAffected: summaryPagesNotAffected,
1250
- scannedPagesCount,
1251
- pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
1252
- pagesNotScannedCount,
1253
- };
1254
- }
1255
- // Send WCAG criteria breakdown to Sentry
1256
- export const sendWcagBreakdownToSentry = async (appVersion, wcagBreakdown, ruleIdJson, scanInfo, allIssues, pagesScannedCount = 0) => {
1257
- try {
1258
- // Initialize Sentry
1259
- Sentry.init(sentryConfig);
1260
- // Set user ID for Sentry tracking
1261
- const userData = getUserDataTxt();
1262
- if (userData && userData.userId) {
1263
- setSentryUser(userData.userId);
1264
- }
1265
- // Prepare tags for the event
1266
- const tags = {};
1267
- const wcagCriteriaBreakdown = {};
1268
- // Tag app version
1269
- tags.version = appVersion;
1270
- // Get dynamic WCAG criteria map once
1271
- const wcagCriteriaMap = await getWcagCriteriaMap();
1272
- // Categorize all WCAG criteria for reporting
1273
- const wcagIds = Array.from(new Set([...Object.keys(wcagCriteriaMap), ...Array.from(wcagBreakdown.keys())]));
1274
- const categorizedWcag = await categorizeWcagCriteria(wcagIds);
1275
- // First ensure all WCAG criteria are included in the tags with a value of 0
1276
- // This ensures criteria with no violations are still reported
1277
- for (const [wcagId, info] of Object.entries(wcagCriteriaMap)) {
1278
- const formattedTag = await formatWcagTag(wcagId);
1279
- if (formattedTag) {
1280
- // Initialize with zero
1281
- tags[formattedTag] = '0';
1282
- // Store in breakdown object with category information
1283
- wcagCriteriaBreakdown[formattedTag] = {
1284
- count: 0,
1285
- category: categorizedWcag[wcagId] || 'mustFix', // Default to mustFix if not found
1286
- };
1287
- }
1288
- }
1289
- // Now override with actual counts from the scan
1290
- for (const [wcagId, count] of wcagBreakdown.entries()) {
1291
- const formattedTag = await formatWcagTag(wcagId);
1292
- if (formattedTag) {
1293
- // Add as a tag with the count as value
1294
- tags[formattedTag] = String(count);
1295
- // Update count in breakdown object
1296
- if (wcagCriteriaBreakdown[formattedTag]) {
1297
- wcagCriteriaBreakdown[formattedTag].count = count;
1298
- }
1299
- else {
1300
- // If somehow this wasn't in our initial map
1301
- wcagCriteriaBreakdown[formattedTag] = {
1302
- count,
1303
- category: categorizedWcag[wcagId] || 'mustFix',
1304
- };
1305
- }
1306
- }
1307
- }
1308
- // Calculate category counts based on actual issue counts from the report
1309
- // rather than occurrence counts from wcagBreakdown
1310
- const categoryCounts = {
1311
- mustFix: 0,
1312
- goodToFix: 0,
1313
- needsReview: 0,
1314
- };
1315
- if (allIssues) {
1316
- // Use the actual report data for the counts
1317
- categoryCounts.mustFix = allIssues.items.mustFix.rules.length;
1318
- categoryCounts.goodToFix = allIssues.items.goodToFix.rules.length;
1319
- categoryCounts.needsReview = allIssues.items.needsReview.rules.length;
1320
- }
1321
- else {
1322
- // Fallback to the old way if allIssues not provided
1323
- Object.values(wcagCriteriaBreakdown).forEach(item => {
1324
- if (item.count > 0 && categoryCounts[item.category] !== undefined) {
1325
- categoryCounts[item.category] += 1; // Count rules, not occurrences
1326
- }
1327
- });
1328
- }
1329
- // Add category counts as tags
1330
- tags['WCAG-MustFix-Count'] = String(categoryCounts.mustFix);
1331
- tags['WCAG-GoodToFix-Count'] = String(categoryCounts.goodToFix);
1332
- tags['WCAG-NeedsReview-Count'] = String(categoryCounts.needsReview);
1333
- // Also add occurrence counts for reference
1334
- if (allIssues) {
1335
- tags['WCAG-MustFix-Occurrences'] = String(allIssues.items.mustFix.totalItems);
1336
- tags['WCAG-GoodToFix-Occurrences'] = String(allIssues.items.goodToFix.totalItems);
1337
- tags['WCAG-NeedsReview-Occurrences'] = String(allIssues.items.needsReview.totalItems);
1338
- // Add number of pages scanned tag
1339
- tags['Pages-Scanned-Count'] = String(allIssues.totalPagesScanned);
1340
- }
1341
- else if (pagesScannedCount > 0) {
1342
- // Still add the pages scanned count even if we don't have allIssues
1343
- tags['Pages-Scanned-Count'] = String(pagesScannedCount);
1344
- }
1345
- // Send the event to Sentry
1346
- await Sentry.captureEvent({
1347
- message: 'Accessibility Scan Completed',
1348
- level: 'info',
1349
- tags: {
1350
- ...tags,
1351
- event_type: 'accessibility_scan',
1352
- scanType: scanInfo.scanType,
1353
- browser: scanInfo.browser,
1354
- entryUrl: scanInfo.entryUrl,
1355
- },
1356
- user: {
1357
- ...(scanInfo.email && scanInfo.name
1358
- ? {
1359
- email: scanInfo.email,
1360
- username: scanInfo.name,
1361
- }
1362
- : {}),
1363
- ...(userData && userData.userId ? { id: userData.userId } : {}),
1364
- },
1365
- extra: {
1366
- additionalScanMetadata: ruleIdJson != null ? JSON.stringify(ruleIdJson) : '{}',
1367
- wcagBreakdown: wcagCriteriaBreakdown,
1368
- reportCounts: allIssues
1369
- ? {
1370
- mustFix: {
1371
- issues: allIssues.items.mustFix.rules?.length ?? 0,
1372
- occurrences: allIssues.items.mustFix.totalItems ?? 0,
1373
- },
1374
- goodToFix: {
1375
- issues: allIssues.items.goodToFix.rules?.length ?? 0,
1376
- occurrences: allIssues.items.goodToFix.totalItems ?? 0,
1377
- },
1378
- needsReview: {
1379
- issues: allIssues.items.needsReview.rules?.length ?? 0,
1380
- occurrences: allIssues.items.needsReview.totalItems ?? 0,
1381
- },
1382
- }
1383
- : undefined,
1384
- },
1385
- });
1386
- // Wait for events to be sent
1387
- await Sentry.flush(2000);
1388
- }
1389
- catch (error) {
1390
- console.error('Error sending WCAG breakdown to Sentry:', error);
1391
- }
1392
- };
1393
488
  const formatAboutStartTime = (dateString) => {
1394
489
  const utcStartTimeDate = new Date(dateString);
1395
490
  const formattedStartTime = utcStartTimeDate.toLocaleTimeString('en-GB', {
@@ -1675,5 +770,5 @@ generateJsonFiles = false) => {
1675
770
  console.log('Report generated successfully');
1676
771
  return ruleIdJson;
1677
772
  };
1678
- export { writeHTML, compressJsonFileStreaming, flattenAndSortResults, populateScanPagesDetail, getWcagPassPercentage, getProgressPercentage, getIssuesPercentage, itemTypeDescription, oobeeAiHtmlETL, oobeeAiRules, formatAboutStartTime, };
773
+ export { writeHTML, compressJsonFileStreaming, convertItemsToReferences, flattenAndSortResults, populateScanPagesDetail, sendWcagBreakdownToSentry, getWcagPassPercentage, getProgressPercentage, getIssuesPercentage, itemTypeDescription, oobeeAiHtmlETL, oobeeAiRules, formatAboutStartTime, };
1679
774
  export default generateArtifacts;