@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.
- package/README.md +6 -1
- package/dist/constants/common.js +13 -1
- package/dist/crawlers/crawlDomain.js +220 -120
- package/dist/crawlers/crawlIntelligentSitemap.js +22 -7
- package/dist/crawlers/runCustom.js +8 -2
- package/dist/mergeAxeResults/itemReferences.js +55 -0
- package/dist/mergeAxeResults/jsonArtifacts.js +335 -0
- package/dist/mergeAxeResults/scanPages.js +159 -0
- package/dist/mergeAxeResults/sentryTelemetry.js +152 -0
- package/dist/mergeAxeResults/types.js +1 -0
- package/dist/mergeAxeResults/writeCsv.js +125 -0
- package/dist/mergeAxeResults/writeScanDetailsCsv.js +35 -0
- package/dist/mergeAxeResults/writeSitemap.js +10 -0
- package/dist/mergeAxeResults.js +24 -929
- package/dist/proxyService.js +90 -5
- package/dist/utils.js +20 -7
- package/package.json +6 -6
- package/src/constants/common.ts +13 -1
- package/src/crawlers/crawlDomain.ts +248 -137
- package/src/crawlers/crawlIntelligentSitemap.ts +22 -8
- package/src/crawlers/runCustom.ts +10 -2
- package/src/mergeAxeResults/itemReferences.ts +62 -0
- package/src/mergeAxeResults/jsonArtifacts.ts +451 -0
- package/src/mergeAxeResults/scanPages.ts +207 -0
- package/src/mergeAxeResults/sentryTelemetry.ts +183 -0
- package/src/mergeAxeResults/types.ts +99 -0
- package/src/mergeAxeResults/writeCsv.ts +145 -0
- package/src/mergeAxeResults/writeScanDetailsCsv.ts +51 -0
- package/src/mergeAxeResults/writeSitemap.ts +13 -0
- package/src/mergeAxeResults.ts +82 -1318
- package/src/proxyService.ts +96 -4
- package/src/utils.ts +19 -7
package/src/mergeAxeResults.ts
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/* eslint-disable consistent-return */
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
|
-
import os from 'os';
|
|
4
3
|
import fs, { ensureDirSync } from 'fs-extra';
|
|
5
4
|
import printMessage from 'print-message';
|
|
6
5
|
import path from 'path';
|
|
7
6
|
import ejs from 'ejs';
|
|
8
7
|
import { fileURLToPath } from 'url';
|
|
9
|
-
import { chromium } from 'playwright';
|
|
10
|
-
import { createWriteStream } from 'fs';
|
|
11
|
-
import { AsyncParser, ParserOptions } from '@json2csv/node';
|
|
12
|
-
import zlib from 'zlib';
|
|
13
|
-
import { Base64Encode } from 'base64-stream';
|
|
14
|
-
import { pipeline } from 'stream/promises';
|
|
15
|
-
import * as Sentry from '@sentry/node';
|
|
16
8
|
import constants, {
|
|
17
9
|
BrowserTypes,
|
|
18
10
|
ScannerTypes,
|
|
19
|
-
sentryConfig,
|
|
20
|
-
setSentryUser,
|
|
21
11
|
WCAGclauses,
|
|
22
12
|
a11yRuleShortDescriptionMap,
|
|
23
13
|
disabilityBadgesMap,
|
|
@@ -34,114 +24,32 @@ import {
|
|
|
34
24
|
retryFunction,
|
|
35
25
|
zipResults,
|
|
36
26
|
getIssuesPercentage,
|
|
37
|
-
getWcagCriteriaMap,
|
|
38
|
-
categorizeWcagCriteria,
|
|
39
|
-
getUserDataTxt,
|
|
40
27
|
register,
|
|
41
28
|
} from './utils.js';
|
|
42
|
-
import { consoleLogger
|
|
29
|
+
import { consoleLogger } from './logs.js';
|
|
43
30
|
import itemTypeDescription from './constants/itemTypeDescription.js';
|
|
44
31
|
import { oobeeAiHtmlETL, oobeeAiRules } from './constants/oobeeAi.js';
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export type HtmlGroupItem = {
|
|
67
|
-
html: string;
|
|
68
|
-
xpath: string;
|
|
69
|
-
message: string;
|
|
70
|
-
screenshotPath: string;
|
|
71
|
-
displayNeedsReview?: boolean;
|
|
72
|
-
pageUrls: string[];
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export type HtmlGroups = {
|
|
76
|
-
[htmlKey: string]: HtmlGroupItem;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export type RuleInfo = {
|
|
80
|
-
totalItems: number;
|
|
81
|
-
pagesAffected: PageInfo[];
|
|
82
|
-
pagesAffectedCount: number;
|
|
83
|
-
rule: string;
|
|
84
|
-
description: string;
|
|
85
|
-
axeImpact: string;
|
|
86
|
-
conformance: string[];
|
|
87
|
-
helpUrl: string;
|
|
88
|
-
htmlGroups?: HtmlGroups;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
type Category = {
|
|
92
|
-
description: string;
|
|
93
|
-
totalItems: number;
|
|
94
|
-
totalRuleIssues: number;
|
|
95
|
-
rules: RuleInfo[];
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
type AllIssues = {
|
|
99
|
-
storagePath: string;
|
|
100
|
-
oobeeAi: {
|
|
101
|
-
htmlETL: any;
|
|
102
|
-
rules: string[];
|
|
103
|
-
};
|
|
104
|
-
siteName: string;
|
|
105
|
-
startTime: Date;
|
|
106
|
-
endTime: Date;
|
|
107
|
-
urlScanned: string;
|
|
108
|
-
scanType: string;
|
|
109
|
-
deviceChosen: string;
|
|
110
|
-
formatAboutStartTime: (dateString: any) => string;
|
|
111
|
-
isCustomFlow: boolean;
|
|
112
|
-
pagesScanned: PageInfo[];
|
|
113
|
-
pagesNotScanned: PageInfo[];
|
|
114
|
-
totalPagesScanned: number;
|
|
115
|
-
totalPagesNotScanned: number;
|
|
116
|
-
totalItems: number;
|
|
117
|
-
topFiveMostIssues: Array<any>;
|
|
118
|
-
topTenPagesWithMostIssues: Array<any>;
|
|
119
|
-
topTenIssues: Array<any>;
|
|
120
|
-
wcagViolations: string[];
|
|
121
|
-
customFlowLabel: string;
|
|
122
|
-
oobeeAppVersion: string;
|
|
123
|
-
items: {
|
|
124
|
-
mustFix: Category;
|
|
125
|
-
goodToFix: Category;
|
|
126
|
-
needsReview: Category;
|
|
127
|
-
passed: Category;
|
|
128
|
-
};
|
|
129
|
-
cypressScanAboutMetadata: {
|
|
130
|
-
browser?: string;
|
|
131
|
-
viewport?: { width: number; height: number };
|
|
132
|
-
};
|
|
133
|
-
wcagLinks: { [key: string]: string };
|
|
134
|
-
wcagClauses: { [key: string]: string };
|
|
135
|
-
[key: string]: any;
|
|
136
|
-
advancedScanOptionsSummaryItems: { [key: string]: boolean };
|
|
137
|
-
scanPagesDetail: {
|
|
138
|
-
pagesAffected: any[];
|
|
139
|
-
pagesNotAffected: any[];
|
|
140
|
-
scannedPagesCount: number;
|
|
141
|
-
pagesNotScanned: any[];
|
|
142
|
-
pagesNotScannedCount: number;
|
|
143
|
-
};
|
|
144
|
-
};
|
|
32
|
+
import { buildHtmlGroups, convertItemsToReferences } from './mergeAxeResults/itemReferences.js';
|
|
33
|
+
import {
|
|
34
|
+
compressJsonFileStreaming,
|
|
35
|
+
writeJsonAndBase64Files,
|
|
36
|
+
writeJsonFileAndCompressedJsonFile,
|
|
37
|
+
} from './mergeAxeResults/jsonArtifacts.js';
|
|
38
|
+
import writeCsv from './mergeAxeResults/writeCsv.js';
|
|
39
|
+
import writeScanDetailsCsv from './mergeAxeResults/writeScanDetailsCsv.js';
|
|
40
|
+
import writeSitemap from './mergeAxeResults/writeSitemap.js';
|
|
41
|
+
import populateScanPagesDetail from './mergeAxeResults/scanPages.js';
|
|
42
|
+
import sendWcagBreakdownToSentry from './mergeAxeResults/sentryTelemetry.js';
|
|
43
|
+
import type { AllIssues, PageInfo, RuleInfo } from './mergeAxeResults/types.js';
|
|
44
|
+
|
|
45
|
+
export type {
|
|
46
|
+
AllIssues,
|
|
47
|
+
HtmlGroupItem,
|
|
48
|
+
HtmlGroups,
|
|
49
|
+
ItemsInfo,
|
|
50
|
+
PageInfo,
|
|
51
|
+
RuleInfo,
|
|
52
|
+
} from './mergeAxeResults/types.js';
|
|
145
53
|
|
|
146
54
|
const filename = fileURLToPath(import.meta.url);
|
|
147
55
|
const dirname = path.dirname(filename);
|
|
@@ -182,7 +90,9 @@ const parseContentToJson = async (rPath: string) => {
|
|
|
182
90
|
}
|
|
183
91
|
|
|
184
92
|
consoleLogger.error(`[parseContentToJson] Failed to parse file: ${rPath}`);
|
|
185
|
-
consoleLogger.error(
|
|
93
|
+
consoleLogger.error(
|
|
94
|
+
`[parseContentToJson] ${parseError?.name || 'Error'}: ${parseError?.message || parseError}`,
|
|
95
|
+
);
|
|
186
96
|
if (position !== null) {
|
|
187
97
|
consoleLogger.error(`[parseContentToJson] JSON parse position: ${position}`);
|
|
188
98
|
}
|
|
@@ -195,144 +105,6 @@ const parseContentToJson = async (rPath: string) => {
|
|
|
195
105
|
}
|
|
196
106
|
};
|
|
197
107
|
|
|
198
|
-
const writeCsv = async (allIssues, storagePath) => {
|
|
199
|
-
const csvOutput = createWriteStream(`${storagePath}/report.csv`, { encoding: 'utf8' });
|
|
200
|
-
const formatPageViolation = pageNum => {
|
|
201
|
-
if (pageNum < 0) return 'Document';
|
|
202
|
-
return `Page ${pageNum}`;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// transform allIssues into the form:
|
|
206
|
-
// [['mustFix', rule1], ['mustFix', rule2], ['goodToFix', rule3], ...]
|
|
207
|
-
const getRulesByCategory = (allIssues: AllIssues) => {
|
|
208
|
-
return Object.entries(allIssues.items)
|
|
209
|
-
.filter(([category]) => category !== 'passed')
|
|
210
|
-
.reduce((prev: [string, RuleInfo][], [category, value]) => {
|
|
211
|
-
const rulesEntries = Object.entries(value.rules);
|
|
212
|
-
rulesEntries.forEach(([, ruleInfo]) => {
|
|
213
|
-
prev.push([category, ruleInfo]);
|
|
214
|
-
});
|
|
215
|
-
return prev;
|
|
216
|
-
}, [])
|
|
217
|
-
.sort((a, b) => {
|
|
218
|
-
// sort rules according to severity, then ruleId
|
|
219
|
-
const compareCategory = -a[0].localeCompare(b[0]);
|
|
220
|
-
return compareCategory === 0 ? a[1].rule.localeCompare(b[1].rule) : compareCategory;
|
|
221
|
-
});
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const flattenRule = catAndRule => {
|
|
225
|
-
const [severity, rule] = catAndRule;
|
|
226
|
-
const results = [];
|
|
227
|
-
const {
|
|
228
|
-
rule: issueId,
|
|
229
|
-
description: issueDescription,
|
|
230
|
-
axeImpact,
|
|
231
|
-
conformance,
|
|
232
|
-
pagesAffected,
|
|
233
|
-
helpUrl: learnMore,
|
|
234
|
-
} = rule;
|
|
235
|
-
|
|
236
|
-
// format clauses as a string
|
|
237
|
-
const wcagConformance = conformance.join(',');
|
|
238
|
-
|
|
239
|
-
pagesAffected.sort((a, b) => a.url.localeCompare(b.url));
|
|
240
|
-
|
|
241
|
-
pagesAffected.forEach(affectedPage => {
|
|
242
|
-
const { url, items } = affectedPage;
|
|
243
|
-
items.forEach(item => {
|
|
244
|
-
const { html, page, message, xpath } = item;
|
|
245
|
-
const howToFix = message.replace(/(\r\n|\n|\r)/g, '\\n'); // preserve newlines as \n
|
|
246
|
-
const violation = html || formatPageViolation(page); // page is a number, not a string
|
|
247
|
-
const context = violation.replace(/(\r\n|\n|\r)/g, ''); // remove newlines
|
|
248
|
-
|
|
249
|
-
results.push({
|
|
250
|
-
customFlowLabel: allIssues.customFlowLabel || '',
|
|
251
|
-
deviceChosen: allIssues.deviceChosen || '',
|
|
252
|
-
scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
253
|
-
severity: severity || '',
|
|
254
|
-
issueId: issueId || '',
|
|
255
|
-
issueDescription: a11yRuleShortDescriptionMap[issueId] || issueDescription || '',
|
|
256
|
-
wcagConformance: wcagConformance || '',
|
|
257
|
-
url: url || '',
|
|
258
|
-
pageTitle: affectedPage.pageTitle || 'No page title',
|
|
259
|
-
context: context || '',
|
|
260
|
-
howToFix: howToFix || '',
|
|
261
|
-
axeImpact: axeImpact || '',
|
|
262
|
-
xpath: xpath || '',
|
|
263
|
-
learnMore: learnMore || '',
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
if (results.length === 0) return {};
|
|
268
|
-
return results;
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const opts: ParserOptions<any, any> = {
|
|
272
|
-
transforms: [getRulesByCategory, flattenRule],
|
|
273
|
-
fields: [
|
|
274
|
-
'customFlowLabel',
|
|
275
|
-
'deviceChosen',
|
|
276
|
-
'scanCompletedAt',
|
|
277
|
-
'severity',
|
|
278
|
-
'issueId',
|
|
279
|
-
'issueDescription',
|
|
280
|
-
'wcagConformance',
|
|
281
|
-
'url',
|
|
282
|
-
'pageTitle',
|
|
283
|
-
'context',
|
|
284
|
-
'howToFix',
|
|
285
|
-
'axeImpact',
|
|
286
|
-
'xpath',
|
|
287
|
-
'learnMore',
|
|
288
|
-
],
|
|
289
|
-
includeEmptyRows: true,
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
// Create the parse stream (it's asynchronous)
|
|
293
|
-
const parser = new AsyncParser(opts);
|
|
294
|
-
const parseStream = parser.parse(allIssues);
|
|
295
|
-
|
|
296
|
-
// Pipe JSON2CSV output into the file, but don't end automatically
|
|
297
|
-
parseStream.pipe(csvOutput, { end: false });
|
|
298
|
-
|
|
299
|
-
// Once JSON2CSV is done writing all normal rows, append any "pagesNotScanned"
|
|
300
|
-
parseStream.on('end', () => {
|
|
301
|
-
if (allIssues.pagesNotScanned && allIssues.pagesNotScanned.length > 0) {
|
|
302
|
-
csvOutput.write('\n');
|
|
303
|
-
allIssues.pagesNotScanned.forEach(page => {
|
|
304
|
-
const skippedPage = {
|
|
305
|
-
customFlowLabel: allIssues.customFlowLabel || '',
|
|
306
|
-
deviceChosen: allIssues.deviceChosen || '',
|
|
307
|
-
scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
308
|
-
severity: 'error',
|
|
309
|
-
issueId: 'error-pages-skipped',
|
|
310
|
-
issueDescription: page.metadata
|
|
311
|
-
? page.metadata
|
|
312
|
-
: 'An unknown error caused the page to be skipped',
|
|
313
|
-
wcagConformance: '',
|
|
314
|
-
url: page.url || page || '',
|
|
315
|
-
pageTitle: 'Error',
|
|
316
|
-
context: '',
|
|
317
|
-
howToFix: '',
|
|
318
|
-
axeImpact: '',
|
|
319
|
-
xpath: '',
|
|
320
|
-
learnMore: '',
|
|
321
|
-
};
|
|
322
|
-
csvOutput.write(`${Object.values(skippedPage).join(',')}\n`);
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Now close the CSV file
|
|
327
|
-
csvOutput.end();
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
parseStream.on('error', err => {
|
|
331
|
-
console.error('Error parsing CSV:', err);
|
|
332
|
-
csvOutput.end();
|
|
333
|
-
});
|
|
334
|
-
};
|
|
335
|
-
|
|
336
108
|
const compileHtmlWithEJS = async (
|
|
337
109
|
allIssues: AllIssues,
|
|
338
110
|
storagePath: string,
|
|
@@ -416,12 +188,14 @@ const writeHTML = async (
|
|
|
416
188
|
const scanItemsWithHtmlGroupRefs = convertItemsToReferences(allIssues);
|
|
417
189
|
|
|
418
190
|
// Write the lighter items to a file and get the base64 path
|
|
419
|
-
const {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
191
|
+
const {
|
|
192
|
+
jsonFilePath: scanItemsWithHtmlGroupRefsJsonFilePath,
|
|
193
|
+
base64FilePath: scanItemsWithHtmlGroupRefsBase64FilePath,
|
|
194
|
+
} = await writeJsonFileAndCompressedJsonFile(
|
|
195
|
+
scanItemsWithHtmlGroupRefs.items,
|
|
196
|
+
storagePath,
|
|
197
|
+
'scanItems-light',
|
|
198
|
+
);
|
|
425
199
|
|
|
426
200
|
return new Promise<void>((resolve, reject) => {
|
|
427
201
|
const scanDetailsReadStream = fs.createReadStream(scanDetailsFilePath, {
|
|
@@ -448,7 +222,7 @@ const writeHTML = async (
|
|
|
448
222
|
outputStream.write(prefixData);
|
|
449
223
|
|
|
450
224
|
// For Proxied AI environments only
|
|
451
|
-
outputStream.write(`let proxyUrl = "${process.env.PROXY_API_BASE_URL ||
|
|
225
|
+
outputStream.write(`let proxyUrl = "${process.env.PROXY_API_BASE_URL || ''}"\n`);
|
|
452
226
|
|
|
453
227
|
// Initialize GenAI feature flag
|
|
454
228
|
outputStream.write(`
|
|
@@ -478,17 +252,15 @@ const writeHTML = async (
|
|
|
478
252
|
}
|
|
479
253
|
\n`);
|
|
480
254
|
|
|
481
|
-
outputStream.write(
|
|
482
|
-
"</script>\n<script type=\"text/plain\" id=\"scanDataRaw\">"
|
|
483
|
-
);
|
|
255
|
+
outputStream.write('</script>\n<script type="text/plain" id="scanDataRaw">');
|
|
484
256
|
scanDetailsReadStream.pipe(outputStream, { end: false });
|
|
485
257
|
|
|
486
258
|
scanDetailsReadStream.on('end', async () => {
|
|
487
|
-
outputStream.write(
|
|
259
|
+
outputStream.write('</script>\n<script>\n');
|
|
488
260
|
outputStream.write(
|
|
489
|
-
"var scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse(document.getElementById('scanDataRaw').textContent); })();\n"
|
|
261
|
+
"var scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse(document.getElementById('scanDataRaw').textContent); })();\n",
|
|
490
262
|
);
|
|
491
|
-
outputStream.write(
|
|
263
|
+
outputStream.write('</script>\n');
|
|
492
264
|
|
|
493
265
|
// Write scanItems in 2MB chunks using a stream to avoid loading entire file into memory
|
|
494
266
|
try {
|
|
@@ -499,11 +271,13 @@ const writeHTML = async (
|
|
|
499
271
|
});
|
|
500
272
|
|
|
501
273
|
for await (const chunk of scanItemsStream) {
|
|
502
|
-
outputStream.write(
|
|
274
|
+
outputStream.write(
|
|
275
|
+
`<script type="text/plain" id="scanItemsRaw${chunkIndex}">${chunk}</script>\n`,
|
|
276
|
+
);
|
|
503
277
|
chunkIndex++;
|
|
504
278
|
}
|
|
505
279
|
|
|
506
|
-
outputStream.write(
|
|
280
|
+
outputStream.write('<script>\n');
|
|
507
281
|
outputStream.write(`
|
|
508
282
|
var scanItemsPromise = (async () => {
|
|
509
283
|
console.log('Loading scanItems...');
|
|
@@ -560,13 +334,6 @@ const writeSummaryHTML = async (
|
|
|
560
334
|
fs.writeFileSync(`${storagePath}/${htmlFilename}.html`, html);
|
|
561
335
|
};
|
|
562
336
|
|
|
563
|
-
const writeSitemap = async (pagesScanned: PageInfo[], storagePath: string) => {
|
|
564
|
-
const sitemapPath = path.join(storagePath, 'sitemap.txt');
|
|
565
|
-
const content = pagesScanned.map(p => p.url).join('\n');
|
|
566
|
-
await fs.writeFile(sitemapPath, content, { encoding: 'utf-8' });
|
|
567
|
-
consoleLogger.info(`Sitemap written to ${sitemapPath}`);
|
|
568
|
-
};
|
|
569
|
-
|
|
570
337
|
const cleanUpJsonFiles = async (filesToDelete: string[]) => {
|
|
571
338
|
consoleLogger.info('Cleaning up JSON files...');
|
|
572
339
|
filesToDelete.forEach(file => {
|
|
@@ -575,513 +342,6 @@ const cleanUpJsonFiles = async (filesToDelete: string[]) => {
|
|
|
575
342
|
});
|
|
576
343
|
};
|
|
577
344
|
|
|
578
|
-
function* serializeObject(obj: any, depth = 0, indent = ' ') {
|
|
579
|
-
const currentIndent = indent.repeat(depth);
|
|
580
|
-
const nextIndent = indent.repeat(depth + 1);
|
|
581
|
-
|
|
582
|
-
if (obj instanceof Date) {
|
|
583
|
-
yield JSON.stringify(obj.toISOString());
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (Array.isArray(obj)) {
|
|
588
|
-
yield '[\n';
|
|
589
|
-
for (let i = 0; i < obj.length; i++) {
|
|
590
|
-
if (i > 0) yield ',\n';
|
|
591
|
-
yield nextIndent;
|
|
592
|
-
yield* serializeObject(obj[i], depth + 1, indent);
|
|
593
|
-
}
|
|
594
|
-
yield `\n${currentIndent}]`;
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
if (obj !== null && typeof obj === 'object') {
|
|
599
|
-
yield '{\n';
|
|
600
|
-
const keys = Object.keys(obj);
|
|
601
|
-
for (let i = 0; i < keys.length; i++) {
|
|
602
|
-
const key = keys[i];
|
|
603
|
-
if (i > 0) yield ',\n';
|
|
604
|
-
yield `${nextIndent}${JSON.stringify(key)}: `;
|
|
605
|
-
yield* serializeObject(obj[key], depth + 1, indent);
|
|
606
|
-
}
|
|
607
|
-
yield `\n${currentIndent}}`;
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (obj === null || typeof obj === 'function' || typeof obj === 'undefined') {
|
|
612
|
-
yield 'null';
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
yield JSON.stringify(obj);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function writeLargeJsonToFile(obj: object, filePath: string) {
|
|
620
|
-
return new Promise((resolve, reject) => {
|
|
621
|
-
const writeStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
|
622
|
-
|
|
623
|
-
writeStream.on('error', error => {
|
|
624
|
-
consoleLogger.error('Stream error:', error);
|
|
625
|
-
reject(error);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
writeStream.on('finish', () => {
|
|
629
|
-
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
630
|
-
resolve(true);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
const generator = serializeObject(obj);
|
|
634
|
-
|
|
635
|
-
function write() {
|
|
636
|
-
let next: any;
|
|
637
|
-
while (!(next = generator.next()).done) {
|
|
638
|
-
if (!writeStream.write(next.value)) {
|
|
639
|
-
writeStream.once('drain', write);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
writeStream.end();
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
write();
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const writeLargeScanItemsJsonToFile = async (obj: object, filePath: string) => {
|
|
651
|
-
return new Promise((resolve, reject) => {
|
|
652
|
-
const writeStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
|
|
653
|
-
const writeQueue: string[] = [];
|
|
654
|
-
let isWriting = false;
|
|
655
|
-
|
|
656
|
-
const processNextWrite = async () => {
|
|
657
|
-
if (isWriting || writeQueue.length === 0) return;
|
|
658
|
-
|
|
659
|
-
isWriting = true;
|
|
660
|
-
const data = writeQueue.shift()!;
|
|
661
|
-
|
|
662
|
-
try {
|
|
663
|
-
if (!writeStream.write(data)) {
|
|
664
|
-
await new Promise<void>(resolve => {
|
|
665
|
-
writeStream.once('drain', () => {
|
|
666
|
-
resolve();
|
|
667
|
-
});
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
} catch (error) {
|
|
671
|
-
writeStream.destroy(error as Error);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
isWriting = false;
|
|
676
|
-
processNextWrite();
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
const queueWrite = (data: string) => {
|
|
680
|
-
writeQueue.push(data);
|
|
681
|
-
processNextWrite();
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
writeStream.on('error', error => {
|
|
685
|
-
consoleLogger.error(`Error writing object to JSON file: ${error}`);
|
|
686
|
-
reject(error);
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
writeStream.on('finish', () => {
|
|
690
|
-
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
691
|
-
resolve(true);
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
try {
|
|
695
|
-
queueWrite('{\n');
|
|
696
|
-
const keys = Object.keys(obj);
|
|
697
|
-
|
|
698
|
-
keys.forEach((key, i) => {
|
|
699
|
-
const value = obj[key];
|
|
700
|
-
|
|
701
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
702
|
-
queueWrite(` "${key}": ${JSON.stringify(value)}`);
|
|
703
|
-
} else {
|
|
704
|
-
queueWrite(` "${key}": {\n`);
|
|
705
|
-
|
|
706
|
-
const { rules, ...otherProperties } = value;
|
|
707
|
-
|
|
708
|
-
// Write other properties
|
|
709
|
-
Object.entries(otherProperties).forEach(([propKey, propValue], j) => {
|
|
710
|
-
const propValueString =
|
|
711
|
-
propValue === null ||
|
|
712
|
-
typeof propValue === 'function' ||
|
|
713
|
-
typeof propValue === 'undefined'
|
|
714
|
-
? 'null'
|
|
715
|
-
: JSON.stringify(propValue);
|
|
716
|
-
queueWrite(` "${propKey}": ${propValueString}`);
|
|
717
|
-
if (j < Object.keys(otherProperties).length - 1 || (rules && rules.length >= 0)) {
|
|
718
|
-
queueWrite(',\n');
|
|
719
|
-
} else {
|
|
720
|
-
queueWrite('\n');
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
if (rules && Array.isArray(rules)) {
|
|
725
|
-
queueWrite(' "rules": [\n');
|
|
726
|
-
|
|
727
|
-
rules.forEach((rule, j) => {
|
|
728
|
-
queueWrite(' {\n');
|
|
729
|
-
const { pagesAffected, ...otherRuleProperties } = rule;
|
|
730
|
-
|
|
731
|
-
Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
|
|
732
|
-
const ruleValueString =
|
|
733
|
-
ruleValue === null ||
|
|
734
|
-
typeof ruleValue === 'function' ||
|
|
735
|
-
typeof ruleValue === 'undefined'
|
|
736
|
-
? 'null'
|
|
737
|
-
: JSON.stringify(ruleValue);
|
|
738
|
-
queueWrite(` "${ruleKey}": ${ruleValueString}`);
|
|
739
|
-
if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
|
|
740
|
-
queueWrite(',\n');
|
|
741
|
-
} else {
|
|
742
|
-
queueWrite('\n');
|
|
743
|
-
}
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
if (pagesAffected && Array.isArray(pagesAffected)) {
|
|
747
|
-
queueWrite(' "pagesAffected": [\n');
|
|
748
|
-
|
|
749
|
-
pagesAffected.forEach((page, p) => {
|
|
750
|
-
const pageJson = JSON.stringify(page, null, 2)
|
|
751
|
-
.split('\n')
|
|
752
|
-
.map((line, idx) => (idx === 0 ? ` ${line}` : ` ${line}`))
|
|
753
|
-
.join('\n');
|
|
754
|
-
|
|
755
|
-
queueWrite(pageJson);
|
|
756
|
-
|
|
757
|
-
if (p < pagesAffected.length - 1) {
|
|
758
|
-
queueWrite(',\n');
|
|
759
|
-
} else {
|
|
760
|
-
queueWrite('\n');
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
queueWrite(' ]');
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
queueWrite('\n }');
|
|
768
|
-
if (j < rules.length - 1) {
|
|
769
|
-
queueWrite(',\n');
|
|
770
|
-
} else {
|
|
771
|
-
queueWrite('\n');
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
queueWrite(' ]');
|
|
776
|
-
}
|
|
777
|
-
queueWrite('\n }');
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (i < keys.length - 1) {
|
|
781
|
-
queueWrite(',\n');
|
|
782
|
-
} else {
|
|
783
|
-
queueWrite('\n');
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
queueWrite('}\n');
|
|
788
|
-
|
|
789
|
-
// Ensure all queued writes are processed before ending
|
|
790
|
-
const checkQueueAndEnd = () => {
|
|
791
|
-
if (writeQueue.length === 0 && !isWriting) {
|
|
792
|
-
writeStream.end();
|
|
793
|
-
} else {
|
|
794
|
-
setTimeout(checkQueueAndEnd, 100);
|
|
795
|
-
}
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
checkQueueAndEnd();
|
|
799
|
-
} catch (err) {
|
|
800
|
-
writeStream.destroy(err as Error);
|
|
801
|
-
reject(err);
|
|
802
|
-
}
|
|
803
|
-
});
|
|
804
|
-
};
|
|
805
|
-
|
|
806
|
-
async function compressJsonFileStreaming(inputPath: string, outputPath: string) {
|
|
807
|
-
// Create the read and write streams
|
|
808
|
-
const readStream = fs.createReadStream(inputPath);
|
|
809
|
-
const writeStream = fs.createWriteStream(outputPath);
|
|
810
|
-
|
|
811
|
-
// Create a gzip transform stream
|
|
812
|
-
const gzip = zlib.createGzip();
|
|
813
|
-
|
|
814
|
-
// Create a Base64 transform stream
|
|
815
|
-
const base64Encode = new Base64Encode();
|
|
816
|
-
|
|
817
|
-
// Pipe the streams:
|
|
818
|
-
// read -> gzip -> base64 -> write
|
|
819
|
-
await pipeline(readStream, gzip, base64Encode, writeStream);
|
|
820
|
-
consoleLogger.info(`File successfully compressed and saved to ${outputPath}`);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const writeJsonFileAndCompressedJsonFile = async (
|
|
824
|
-
data: object,
|
|
825
|
-
storagePath: string,
|
|
826
|
-
filename: string,
|
|
827
|
-
): Promise<{ jsonFilePath: string; base64FilePath: string }> => {
|
|
828
|
-
try {
|
|
829
|
-
consoleLogger.info(`Writing JSON to ${filename}.json`);
|
|
830
|
-
const jsonFilePath = path.join(storagePath, `${filename}.json`);
|
|
831
|
-
if (filename === 'scanItems') {
|
|
832
|
-
await writeLargeScanItemsJsonToFile(data, jsonFilePath);
|
|
833
|
-
} else {
|
|
834
|
-
await writeLargeJsonToFile(data, jsonFilePath);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
consoleLogger.info(
|
|
838
|
-
`Reading ${filename}.json, gzipping and base64 encoding it into ${filename}.json.gz.b64`,
|
|
839
|
-
);
|
|
840
|
-
const base64FilePath = path.join(storagePath, `${filename}.json.gz.b64`);
|
|
841
|
-
await compressJsonFileStreaming(jsonFilePath, base64FilePath);
|
|
842
|
-
|
|
843
|
-
consoleLogger.info(`Finished compression and base64 encoding for ${filename}`);
|
|
844
|
-
return {
|
|
845
|
-
jsonFilePath,
|
|
846
|
-
base64FilePath,
|
|
847
|
-
};
|
|
848
|
-
} catch (error) {
|
|
849
|
-
consoleLogger.error(`Error compressing and encoding ${filename}`);
|
|
850
|
-
throw error;
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
|
|
854
|
-
const streamEncodedDataToFile = async (
|
|
855
|
-
inputFilePath: string,
|
|
856
|
-
writeStream: fs.WriteStream,
|
|
857
|
-
appendComma: boolean,
|
|
858
|
-
) => {
|
|
859
|
-
const readStream = fs.createReadStream(inputFilePath, { encoding: 'utf8' });
|
|
860
|
-
let isFirstChunk = true;
|
|
861
|
-
|
|
862
|
-
for await (const chunk of readStream) {
|
|
863
|
-
if (isFirstChunk) {
|
|
864
|
-
isFirstChunk = false;
|
|
865
|
-
writeStream.write(chunk);
|
|
866
|
-
} else {
|
|
867
|
-
writeStream.write(chunk);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (appendComma) {
|
|
872
|
-
writeStream.write(',');
|
|
873
|
-
}
|
|
874
|
-
};
|
|
875
|
-
|
|
876
|
-
const writeJsonAndBase64Files = async (
|
|
877
|
-
allIssues: AllIssues,
|
|
878
|
-
storagePath: string,
|
|
879
|
-
): Promise<{
|
|
880
|
-
scanDataJsonFilePath: string;
|
|
881
|
-
scanDataBase64FilePath: string;
|
|
882
|
-
scanItemsJsonFilePath: string;
|
|
883
|
-
scanItemsBase64FilePath: string;
|
|
884
|
-
scanItemsSummaryJsonFilePath: string;
|
|
885
|
-
scanItemsSummaryBase64FilePath: string;
|
|
886
|
-
scanIssuesSummaryJsonFilePath: string;
|
|
887
|
-
scanIssuesSummaryBase64FilePath: string;
|
|
888
|
-
scanPagesDetailJsonFilePath: string;
|
|
889
|
-
scanPagesDetailBase64FilePath: string;
|
|
890
|
-
scanPagesSummaryJsonFilePath: string;
|
|
891
|
-
scanPagesSummaryBase64FilePath: string;
|
|
892
|
-
scanDataJsonFileSize: number;
|
|
893
|
-
scanItemsJsonFileSize: number;
|
|
894
|
-
}> => {
|
|
895
|
-
const { items, ...rest } = allIssues;
|
|
896
|
-
const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } =
|
|
897
|
-
await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
|
|
898
|
-
const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } =
|
|
899
|
-
await writeJsonFileAndCompressedJsonFile(
|
|
900
|
-
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...items },
|
|
901
|
-
storagePath,
|
|
902
|
-
'scanItems',
|
|
903
|
-
);
|
|
904
|
-
|
|
905
|
-
// Add pagesAffectedCount to each rule in items and sort them in descending order of pagesAffectedCount
|
|
906
|
-
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
907
|
-
if (items[category].rules && Array.isArray(items[category].rules)) {
|
|
908
|
-
items[category].rules.forEach(rule => {
|
|
909
|
-
rule.pagesAffectedCount = Array.isArray(rule.pagesAffected) ? rule.pagesAffected.length : 0;
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
// Sort in descending order of pagesAffectedCount
|
|
913
|
-
items[category].rules.sort(
|
|
914
|
-
(a, b) => (b.pagesAffectedCount || 0) - (a.pagesAffectedCount || 0),
|
|
915
|
-
);
|
|
916
|
-
}
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
// Refactor scanIssuesSummary to reuse the structure by stripping out pagesAffected
|
|
920
|
-
const scanIssuesSummary = {
|
|
921
|
-
|
|
922
|
-
// Replace rule descriptions with short descriptions from the map
|
|
923
|
-
mustFix: items.mustFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
924
|
-
...ruleInfo,
|
|
925
|
-
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
926
|
-
})),
|
|
927
|
-
goodToFix: items.goodToFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
928
|
-
...ruleInfo,
|
|
929
|
-
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
930
|
-
})),
|
|
931
|
-
needsReview: items.needsReview.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
932
|
-
...ruleInfo,
|
|
933
|
-
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
934
|
-
})),
|
|
935
|
-
passed: items.passed.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
936
|
-
...ruleInfo,
|
|
937
|
-
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
938
|
-
})),
|
|
939
|
-
};
|
|
940
|
-
|
|
941
|
-
// Write out the scanIssuesSummary JSON using the new structure
|
|
942
|
-
const {
|
|
943
|
-
jsonFilePath: scanIssuesSummaryJsonFilePath,
|
|
944
|
-
base64FilePath: scanIssuesSummaryBase64FilePath,
|
|
945
|
-
} = await writeJsonFileAndCompressedJsonFile(
|
|
946
|
-
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanIssuesSummary },
|
|
947
|
-
storagePath,
|
|
948
|
-
'scanIssuesSummary',
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
// scanItemsSummary
|
|
952
|
-
// the below mutates the original items object, since it is expensive to clone
|
|
953
|
-
items.mustFix.rules.forEach(rule => {
|
|
954
|
-
rule.pagesAffected.forEach(page => {
|
|
955
|
-
page.itemsCount = page.items.length;
|
|
956
|
-
});
|
|
957
|
-
});
|
|
958
|
-
items.goodToFix.rules.forEach(rule => {
|
|
959
|
-
rule.pagesAffected.forEach(page => {
|
|
960
|
-
page.itemsCount = page.items.length;
|
|
961
|
-
});
|
|
962
|
-
});
|
|
963
|
-
items.needsReview.rules.forEach(rule => {
|
|
964
|
-
rule.pagesAffected.forEach(page => {
|
|
965
|
-
page.itemsCount = page.items.length;
|
|
966
|
-
});
|
|
967
|
-
});
|
|
968
|
-
items.passed.rules.forEach(rule => {
|
|
969
|
-
rule.pagesAffected.forEach(page => {
|
|
970
|
-
page.itemsCount = page.items.length;
|
|
971
|
-
});
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
items.mustFix.totalRuleIssues = items.mustFix.rules.length;
|
|
975
|
-
items.goodToFix.totalRuleIssues = items.goodToFix.rules.length;
|
|
976
|
-
items.needsReview.totalRuleIssues = items.needsReview.rules.length;
|
|
977
|
-
items.passed.totalRuleIssues = items.passed.rules.length;
|
|
978
|
-
|
|
979
|
-
const {
|
|
980
|
-
pagesScanned,
|
|
981
|
-
topTenPagesWithMostIssues,
|
|
982
|
-
pagesNotScanned,
|
|
983
|
-
wcagLinks,
|
|
984
|
-
wcagPassPercentage,
|
|
985
|
-
progressPercentage,
|
|
986
|
-
issuesPercentage,
|
|
987
|
-
totalPagesScanned,
|
|
988
|
-
totalPagesNotScanned,
|
|
989
|
-
topTenIssues,
|
|
990
|
-
} = rest;
|
|
991
|
-
|
|
992
|
-
const summaryItems = {
|
|
993
|
-
mustFix: {
|
|
994
|
-
totalItems: items.mustFix?.totalItems || 0,
|
|
995
|
-
totalRuleIssues: items.mustFix?.totalRuleIssues || 0,
|
|
996
|
-
},
|
|
997
|
-
goodToFix: {
|
|
998
|
-
totalItems: items.goodToFix?.totalItems || 0,
|
|
999
|
-
totalRuleIssues: items.goodToFix?.totalRuleIssues || 0,
|
|
1000
|
-
},
|
|
1001
|
-
needsReview: {
|
|
1002
|
-
totalItems: items.needsReview?.totalItems || 0,
|
|
1003
|
-
totalRuleIssues: items.needsReview?.totalRuleIssues || 0,
|
|
1004
|
-
},
|
|
1005
|
-
topTenPagesWithMostIssues,
|
|
1006
|
-
wcagLinks,
|
|
1007
|
-
wcagPassPercentage,
|
|
1008
|
-
progressPercentage,
|
|
1009
|
-
issuesPercentage,
|
|
1010
|
-
totalPagesScanned,
|
|
1011
|
-
totalPagesNotScanned,
|
|
1012
|
-
topTenIssues,
|
|
1013
|
-
};
|
|
1014
|
-
|
|
1015
|
-
const {
|
|
1016
|
-
jsonFilePath: scanItemsSummaryJsonFilePath,
|
|
1017
|
-
base64FilePath: scanItemsSummaryBase64FilePath,
|
|
1018
|
-
} = await writeJsonFileAndCompressedJsonFile(
|
|
1019
|
-
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItems },
|
|
1020
|
-
storagePath,
|
|
1021
|
-
'scanItemsSummary',
|
|
1022
|
-
);
|
|
1023
|
-
|
|
1024
|
-
const {
|
|
1025
|
-
jsonFilePath: scanPagesDetailJsonFilePath,
|
|
1026
|
-
base64FilePath: scanPagesDetailBase64FilePath,
|
|
1027
|
-
} = await writeJsonFileAndCompressedJsonFile(
|
|
1028
|
-
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesDetail },
|
|
1029
|
-
storagePath,
|
|
1030
|
-
'scanPagesDetail',
|
|
1031
|
-
);
|
|
1032
|
-
|
|
1033
|
-
const {
|
|
1034
|
-
jsonFilePath: scanPagesSummaryJsonFilePath,
|
|
1035
|
-
base64FilePath: scanPagesSummaryBase64FilePath,
|
|
1036
|
-
} = await writeJsonFileAndCompressedJsonFile(
|
|
1037
|
-
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesSummary },
|
|
1038
|
-
storagePath,
|
|
1039
|
-
'scanPagesSummary',
|
|
1040
|
-
);
|
|
1041
|
-
|
|
1042
|
-
return {
|
|
1043
|
-
scanDataJsonFilePath,
|
|
1044
|
-
scanDataBase64FilePath,
|
|
1045
|
-
scanItemsJsonFilePath,
|
|
1046
|
-
scanItemsBase64FilePath,
|
|
1047
|
-
scanItemsSummaryJsonFilePath,
|
|
1048
|
-
scanItemsSummaryBase64FilePath,
|
|
1049
|
-
scanIssuesSummaryJsonFilePath,
|
|
1050
|
-
scanIssuesSummaryBase64FilePath,
|
|
1051
|
-
scanPagesDetailJsonFilePath,
|
|
1052
|
-
scanPagesDetailBase64FilePath,
|
|
1053
|
-
scanPagesSummaryJsonFilePath,
|
|
1054
|
-
scanPagesSummaryBase64FilePath,
|
|
1055
|
-
scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
|
|
1056
|
-
scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
|
|
1057
|
-
};
|
|
1058
|
-
};
|
|
1059
|
-
|
|
1060
|
-
const writeScanDetailsCsv = async (
|
|
1061
|
-
scanDataFilePath: string,
|
|
1062
|
-
scanItemsFilePath: string,
|
|
1063
|
-
scanItemsSummaryFilePath: string,
|
|
1064
|
-
storagePath: string,
|
|
1065
|
-
) => {
|
|
1066
|
-
const filePath = path.join(storagePath, 'scanDetails.csv');
|
|
1067
|
-
const csvWriteStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
|
1068
|
-
const directoryPath = path.dirname(filePath);
|
|
1069
|
-
|
|
1070
|
-
if (!fs.existsSync(directoryPath)) {
|
|
1071
|
-
fs.mkdirSync(directoryPath, { recursive: true });
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
csvWriteStream.write('scanData_base64,scanItems_base64,scanItemsSummary_base64\n');
|
|
1075
|
-
await streamEncodedDataToFile(scanDataFilePath, csvWriteStream, true);
|
|
1076
|
-
await streamEncodedDataToFile(scanItemsFilePath, csvWriteStream, true);
|
|
1077
|
-
await streamEncodedDataToFile(scanItemsSummaryFilePath, csvWriteStream, false);
|
|
1078
|
-
|
|
1079
|
-
await new Promise((resolve, reject) => {
|
|
1080
|
-
csvWriteStream.end(resolve);
|
|
1081
|
-
csvWriteStream.on('error', reject);
|
|
1082
|
-
});
|
|
1083
|
-
};
|
|
1084
|
-
|
|
1085
345
|
const writeSummaryPdf = async (
|
|
1086
346
|
storagePath: string,
|
|
1087
347
|
pagesScanned: number,
|
|
@@ -1133,18 +393,6 @@ const writeSummaryPdf = async (
|
|
|
1133
393
|
// Tracking WCAG occurrences
|
|
1134
394
|
const wcagOccurrencesMap = new Map<string, number>();
|
|
1135
395
|
|
|
1136
|
-
// Format WCAG tag in requested format: wcag111a_Occurrences
|
|
1137
|
-
const formatWcagTag = async (wcagId: string): Promise<string | null> => {
|
|
1138
|
-
// Get dynamic WCAG criteria map
|
|
1139
|
-
const wcagCriteriaMap = await getWcagCriteriaMap();
|
|
1140
|
-
|
|
1141
|
-
if (wcagCriteriaMap[wcagId]) {
|
|
1142
|
-
const { level } = wcagCriteriaMap[wcagId];
|
|
1143
|
-
return `${wcagId}${level}_Occurrences`;
|
|
1144
|
-
}
|
|
1145
|
-
return null;
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1148
396
|
const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
1149
397
|
const { url, pageTitle, filePath } = pageResults;
|
|
1150
398
|
|
|
@@ -1206,10 +454,10 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
1206
454
|
const currRuleFromAllIssues = currCategoryFromAllIssues.rules[rule];
|
|
1207
455
|
|
|
1208
456
|
currRuleFromAllIssues.totalItems += count;
|
|
1209
|
-
|
|
457
|
+
|
|
1210
458
|
// Build htmlGroups for pre-computed Group by HTML Element
|
|
1211
459
|
buildHtmlGroups(currRuleFromAllIssues, items, url);
|
|
1212
|
-
|
|
460
|
+
|
|
1213
461
|
if (isCustomFlow) {
|
|
1214
462
|
const { pageIndex, pageImagePath, metadata } = pageResults;
|
|
1215
463
|
currRuleFromAllIssues.pagesAffected[pageIndex] = {
|
|
@@ -1219,85 +467,17 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
1219
467
|
metadata,
|
|
1220
468
|
items: [...items],
|
|
1221
469
|
};
|
|
1222
|
-
} else {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
|
|
470
|
+
} else if (!(url in currRuleFromAllIssues.pagesAffected)) {
|
|
471
|
+
currRuleFromAllIssues.pagesAffected[url] = {
|
|
472
|
+
pageTitle,
|
|
473
|
+
items: [...items],
|
|
474
|
+
...(filePath && { filePath }),
|
|
475
|
+
};
|
|
1231
476
|
}
|
|
1232
477
|
});
|
|
1233
478
|
});
|
|
1234
479
|
};
|
|
1235
480
|
|
|
1236
|
-
/**
|
|
1237
|
-
* Builds pre-computed HTML groups to optimize Group by HTML Element functionality.
|
|
1238
|
-
* Keys are composite "html\x00xpath" strings to ensure unique matching per element instance.
|
|
1239
|
-
*/
|
|
1240
|
-
const buildHtmlGroups = (
|
|
1241
|
-
rule: RuleInfo,
|
|
1242
|
-
items: ItemsInfo[],
|
|
1243
|
-
pageUrl: string
|
|
1244
|
-
) => {
|
|
1245
|
-
if (!rule.htmlGroups) {
|
|
1246
|
-
rule.htmlGroups = {};
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
items.forEach(item => {
|
|
1250
|
-
// Use composite key of html + xpath for precise matching
|
|
1251
|
-
const htmlKey = `${item.html || 'No HTML element'}\x00${item.xpath || ''}`;
|
|
1252
|
-
|
|
1253
|
-
if (!rule.htmlGroups![htmlKey]) {
|
|
1254
|
-
// Create new group with the first occurrence
|
|
1255
|
-
rule.htmlGroups![htmlKey] = {
|
|
1256
|
-
html: item.html || '',
|
|
1257
|
-
xpath: item.xpath || '',
|
|
1258
|
-
message: item.message || '',
|
|
1259
|
-
screenshotPath: item.screenshotPath || '',
|
|
1260
|
-
displayNeedsReview: item.displayNeedsReview,
|
|
1261
|
-
pageUrls: [],
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
if (!rule.htmlGroups![htmlKey].pageUrls.includes(pageUrl)) {
|
|
1266
|
-
rule.htmlGroups![htmlKey].pageUrls.push(pageUrl);
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
/**
|
|
1272
|
-
* Converts items in pagesAffected to references (html\x00xpath composite keys) for embedding in HTML report.
|
|
1273
|
-
* Additionally, it deep-clones allIssues, replaces page.items objects with composite reference keys.
|
|
1274
|
-
* Those refs are specifically for htmlGroups lookup (html + xpath).
|
|
1275
|
-
*/
|
|
1276
|
-
export const convertItemsToReferences = (allIssues: AllIssues): AllIssues => {
|
|
1277
|
-
const cloned = JSON.parse(JSON.stringify(allIssues));
|
|
1278
|
-
|
|
1279
|
-
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
1280
|
-
if (!cloned.items[category]?.rules) return;
|
|
1281
|
-
|
|
1282
|
-
cloned.items[category].rules.forEach((rule: any) => {
|
|
1283
|
-
if (!rule.pagesAffected || !rule.htmlGroups) return;
|
|
1284
|
-
|
|
1285
|
-
rule.pagesAffected.forEach((page: any) => {
|
|
1286
|
-
if (!page.items) return;
|
|
1287
|
-
|
|
1288
|
-
page.items = page.items.map((item: any) => {
|
|
1289
|
-
if (typeof item === 'string') return item; // Already a reference
|
|
1290
|
-
// Use composite key matching buildHtmlGroups
|
|
1291
|
-
const htmlKey = `${item.html || 'No HTML element'}\x00${item.xpath || ''}`;
|
|
1292
|
-
return htmlKey;
|
|
1293
|
-
});
|
|
1294
|
-
});
|
|
1295
|
-
});
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
return cloned;
|
|
1299
|
-
};
|
|
1300
|
-
|
|
1301
481
|
const getTopTenIssues = allIssues => {
|
|
1302
482
|
const categories = ['mustFix', 'goodToFix'];
|
|
1303
483
|
const rulesWithCounts = [];
|
|
@@ -1401,7 +581,12 @@ function updateIssuesWithOccurrences(issuesList: any[], urlOccurrencesMap: Map<s
|
|
|
1401
581
|
});
|
|
1402
582
|
}
|
|
1403
583
|
|
|
1404
|
-
const extractRuleAiData = (
|
|
584
|
+
const extractRuleAiData = (
|
|
585
|
+
ruleId: string,
|
|
586
|
+
totalItems: number,
|
|
587
|
+
items: any[],
|
|
588
|
+
callback?: () => void,
|
|
589
|
+
) => {
|
|
1405
590
|
let snippets = [];
|
|
1406
591
|
|
|
1407
592
|
if (oobeeAiRules.includes(ruleId)) {
|
|
@@ -1421,8 +606,7 @@ const extractRuleAiData = (ruleId: string, totalItems: number, items: any[], cal
|
|
|
1421
606
|
};
|
|
1422
607
|
|
|
1423
608
|
// This is for telemetry purposes called within mergeAxeResults.ts
|
|
1424
|
-
export
|
|
1425
|
-
const createRuleIdJson = allIssues => {
|
|
609
|
+
export const createRuleIdJson = allIssues => {
|
|
1426
610
|
const compiledRuleJson = {};
|
|
1427
611
|
|
|
1428
612
|
['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
|
|
@@ -1445,9 +629,11 @@ export const createBasicFormHTMLSnippet = filteredResults => {
|
|
|
1445
629
|
|
|
1446
630
|
['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
|
|
1447
631
|
if (filteredResults[category] && filteredResults[category].rules) {
|
|
1448
|
-
Object.entries(filteredResults[category].rules).forEach(
|
|
1449
|
-
|
|
1450
|
-
|
|
632
|
+
Object.entries(filteredResults[category].rules).forEach(
|
|
633
|
+
([ruleId, ruleVal]: [string, any]) => {
|
|
634
|
+
compiledRuleJson[ruleId] = extractRuleAiData(ruleId, ruleVal.totalItems, ruleVal.items);
|
|
635
|
+
},
|
|
636
|
+
);
|
|
1451
637
|
}
|
|
1452
638
|
});
|
|
1453
639
|
|
|
@@ -1462,429 +648,6 @@ const moveElemScreenshots = (randomToken: string, storagePath: string) => {
|
|
|
1462
648
|
}
|
|
1463
649
|
};
|
|
1464
650
|
|
|
1465
|
-
/**
|
|
1466
|
-
* Build allIssues.scanPagesDetail and allIssues.scanPagesSummary
|
|
1467
|
-
* by analyzing pagesScanned (including mustFix/goodToFix/etc.).
|
|
1468
|
-
*/
|
|
1469
|
-
function populateScanPagesDetail(allIssues: AllIssues): void {
|
|
1470
|
-
// --------------------------------------------
|
|
1471
|
-
// 1) Gather your "scanned" pages from allIssues
|
|
1472
|
-
// --------------------------------------------
|
|
1473
|
-
const allScannedPages = Array.isArray(allIssues.pagesScanned) ? allIssues.pagesScanned : [];
|
|
1474
|
-
|
|
1475
|
-
// --------------------------------------------
|
|
1476
|
-
// 2) Define category constants (optional, just for clarity)
|
|
1477
|
-
// --------------------------------------------
|
|
1478
|
-
const mustFixCategory = 'mustFix';
|
|
1479
|
-
const goodToFixCategory = 'goodToFix';
|
|
1480
|
-
const needsReviewCategory = 'needsReview';
|
|
1481
|
-
const passedCategory = 'passed';
|
|
1482
|
-
|
|
1483
|
-
// --------------------------------------------
|
|
1484
|
-
// 3) Set up type declarations (if you want them local to this function)
|
|
1485
|
-
// --------------------------------------------
|
|
1486
|
-
type RuleData = {
|
|
1487
|
-
ruleId: string;
|
|
1488
|
-
wcagConformance: string[];
|
|
1489
|
-
occurrencesMustFix: number;
|
|
1490
|
-
occurrencesGoodToFix: number;
|
|
1491
|
-
occurrencesNeedsReview: number;
|
|
1492
|
-
occurrencesPassed: number;
|
|
1493
|
-
};
|
|
1494
|
-
|
|
1495
|
-
type PageData = {
|
|
1496
|
-
pageTitle: string;
|
|
1497
|
-
url: string;
|
|
1498
|
-
// Summaries
|
|
1499
|
-
totalOccurrencesFailedIncludingNeedsReview: number; // mustFix + goodToFix + needsReview
|
|
1500
|
-
totalOccurrencesFailedExcludingNeedsReview: number; // mustFix + goodToFix
|
|
1501
|
-
totalOccurrencesNeedsReview: number; // needsReview
|
|
1502
|
-
totalOccurrencesPassed: number; // passed only
|
|
1503
|
-
typesOfIssues: Record<string, RuleData>;
|
|
1504
|
-
};
|
|
1505
|
-
|
|
1506
|
-
// --------------------------------------------
|
|
1507
|
-
// 4) We'll accumulate pages in a map keyed by URL
|
|
1508
|
-
// --------------------------------------------
|
|
1509
|
-
const pagesMap: Record<string, PageData> = {};
|
|
1510
|
-
|
|
1511
|
-
// --------------------------------------------
|
|
1512
|
-
// 5) Build pagesMap by iterating over each category in allIssues.items
|
|
1513
|
-
// --------------------------------------------
|
|
1514
|
-
Object.entries(allIssues.items).forEach(([categoryName, categoryData]) => {
|
|
1515
|
-
if (!categoryData?.rules) return; // no rules in this category? skip
|
|
1516
|
-
|
|
1517
|
-
categoryData.rules.forEach(rule => {
|
|
1518
|
-
const { rule: ruleId, conformance = [] } = rule;
|
|
1519
|
-
|
|
1520
|
-
rule.pagesAffected.forEach(p => {
|
|
1521
|
-
const { url, pageTitle, items = [] } = p;
|
|
1522
|
-
const itemsCount = items.length;
|
|
1523
|
-
|
|
1524
|
-
// Ensure the page is in pagesMap
|
|
1525
|
-
if (!pagesMap[url]) {
|
|
1526
|
-
pagesMap[url] = {
|
|
1527
|
-
pageTitle,
|
|
1528
|
-
url,
|
|
1529
|
-
totalOccurrencesFailedIncludingNeedsReview: 0,
|
|
1530
|
-
totalOccurrencesFailedExcludingNeedsReview: 0,
|
|
1531
|
-
totalOccurrencesNeedsReview: 0,
|
|
1532
|
-
totalOccurrencesPassed: 0,
|
|
1533
|
-
typesOfIssues: {},
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
// Ensure the rule is present for this page
|
|
1538
|
-
if (!pagesMap[url].typesOfIssues[ruleId]) {
|
|
1539
|
-
pagesMap[url].typesOfIssues[ruleId] = {
|
|
1540
|
-
ruleId,
|
|
1541
|
-
wcagConformance: conformance,
|
|
1542
|
-
occurrencesMustFix: 0,
|
|
1543
|
-
occurrencesGoodToFix: 0,
|
|
1544
|
-
occurrencesNeedsReview: 0,
|
|
1545
|
-
occurrencesPassed: 0,
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// Depending on the category, increment the relevant occurrence counts
|
|
1550
|
-
if (categoryName === mustFixCategory) {
|
|
1551
|
-
pagesMap[url].typesOfIssues[ruleId].occurrencesMustFix += itemsCount;
|
|
1552
|
-
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
1553
|
-
pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
|
|
1554
|
-
} else if (categoryName === goodToFixCategory) {
|
|
1555
|
-
pagesMap[url].typesOfIssues[ruleId].occurrencesGoodToFix += itemsCount;
|
|
1556
|
-
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
1557
|
-
pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
|
|
1558
|
-
} else if (categoryName === needsReviewCategory) {
|
|
1559
|
-
pagesMap[url].typesOfIssues[ruleId].occurrencesNeedsReview += itemsCount;
|
|
1560
|
-
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
1561
|
-
pagesMap[url].totalOccurrencesNeedsReview += itemsCount;
|
|
1562
|
-
} else if (categoryName === passedCategory) {
|
|
1563
|
-
pagesMap[url].typesOfIssues[ruleId].occurrencesPassed += itemsCount;
|
|
1564
|
-
pagesMap[url].totalOccurrencesPassed += itemsCount;
|
|
1565
|
-
}
|
|
1566
|
-
});
|
|
1567
|
-
});
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
|
-
// --------------------------------------------
|
|
1571
|
-
// 6) Separate scanned pages into “affected” vs. “notAffected”
|
|
1572
|
-
// --------------------------------------------
|
|
1573
|
-
const pagesInMap = Object.values(pagesMap); // All pages that have some record in pagesMap
|
|
1574
|
-
const pagesInMapUrls = new Set(Object.keys(pagesMap));
|
|
1575
|
-
|
|
1576
|
-
// (a) Pages with only passed (no mustFix/goodToFix/needsReview)
|
|
1577
|
-
const pagesAllPassed = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview === 0);
|
|
1578
|
-
|
|
1579
|
-
// (b) Pages that do NOT appear in pagesMap at all => scanned but no items found
|
|
1580
|
-
const pagesNoEntries = allScannedPages
|
|
1581
|
-
.filter(sp => !pagesInMapUrls.has(sp.url))
|
|
1582
|
-
.map(sp => ({
|
|
1583
|
-
pageTitle: sp.pageTitle,
|
|
1584
|
-
url: sp.url,
|
|
1585
|
-
totalOccurrencesFailedIncludingNeedsReview: 0,
|
|
1586
|
-
totalOccurrencesFailedExcludingNeedsReview: 0,
|
|
1587
|
-
totalOccurrencesNeedsReview: 0,
|
|
1588
|
-
totalOccurrencesPassed: 0,
|
|
1589
|
-
typesOfIssues: {},
|
|
1590
|
-
}));
|
|
1591
|
-
|
|
1592
|
-
// Combine these into "notAffected"
|
|
1593
|
-
const pagesNotAffectedRaw = [...pagesAllPassed, ...pagesNoEntries];
|
|
1594
|
-
|
|
1595
|
-
// "affected" pages => have at least 1 mustFix/goodToFix/needsReview
|
|
1596
|
-
const pagesAffectedRaw = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview > 0);
|
|
1597
|
-
|
|
1598
|
-
// --------------------------------------------
|
|
1599
|
-
// 7) Transform both arrays to the final shape
|
|
1600
|
-
// --------------------------------------------
|
|
1601
|
-
function transformPageData(page: PageData) {
|
|
1602
|
-
const typesOfIssuesArray = Object.values(page.typesOfIssues);
|
|
1603
|
-
|
|
1604
|
-
// Compute sums for each failing category
|
|
1605
|
-
const mustFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesMustFix, 0);
|
|
1606
|
-
const goodToFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesGoodToFix, 0);
|
|
1607
|
-
const needsReviewSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesNeedsReview, 0);
|
|
1608
|
-
|
|
1609
|
-
// Build categoriesPresent based on nonzero failing counts
|
|
1610
|
-
const categoriesPresent: string[] = [];
|
|
1611
|
-
if (mustFixSum > 0) categoriesPresent.push('mustFix');
|
|
1612
|
-
if (goodToFixSum > 0) categoriesPresent.push('goodToFix');
|
|
1613
|
-
if (needsReviewSum > 0) categoriesPresent.push('needsReview');
|
|
1614
|
-
|
|
1615
|
-
// Count how many rules have failing issues
|
|
1616
|
-
const failedRuleIds = new Set<string>();
|
|
1617
|
-
typesOfIssuesArray.forEach(r => {
|
|
1618
|
-
if (
|
|
1619
|
-
(r.occurrencesMustFix || 0) > 0 ||
|
|
1620
|
-
(r.occurrencesGoodToFix || 0) > 0 ||
|
|
1621
|
-
(r.occurrencesNeedsReview || 0) > 0
|
|
1622
|
-
) {
|
|
1623
|
-
failedRuleIds.add(r.ruleId); // Ensure ruleId is unique
|
|
1624
|
-
}
|
|
1625
|
-
});
|
|
1626
|
-
const failedRuleCount = failedRuleIds.size;
|
|
1627
|
-
|
|
1628
|
-
// Possibly these two for future convenience
|
|
1629
|
-
const typesOfIssuesExcludingNeedsReviewCount = typesOfIssuesArray.filter(
|
|
1630
|
-
r => (r.occurrencesMustFix || 0) + (r.occurrencesGoodToFix || 0) > 0,
|
|
1631
|
-
).length;
|
|
1632
|
-
|
|
1633
|
-
const typesOfIssuesExclusiveToNeedsReviewCount = typesOfIssuesArray.filter(
|
|
1634
|
-
r =>
|
|
1635
|
-
(r.occurrencesNeedsReview || 0) > 0 &&
|
|
1636
|
-
(r.occurrencesMustFix || 0) === 0 &&
|
|
1637
|
-
(r.occurrencesGoodToFix || 0) === 0,
|
|
1638
|
-
).length;
|
|
1639
|
-
|
|
1640
|
-
// Aggregate wcagConformance for rules that actually fail
|
|
1641
|
-
const allConformance = typesOfIssuesArray.reduce((acc, curr) => {
|
|
1642
|
-
const nonPassedCount =
|
|
1643
|
-
(curr.occurrencesMustFix || 0) +
|
|
1644
|
-
(curr.occurrencesGoodToFix || 0) +
|
|
1645
|
-
(curr.occurrencesNeedsReview || 0);
|
|
1646
|
-
|
|
1647
|
-
if (nonPassedCount > 0) {
|
|
1648
|
-
return acc.concat(curr.wcagConformance || []);
|
|
1649
|
-
}
|
|
1650
|
-
return acc;
|
|
1651
|
-
}, [] as string[]);
|
|
1652
|
-
// Remove duplicates
|
|
1653
|
-
const conformance = Array.from(new Set(allConformance));
|
|
1654
|
-
|
|
1655
|
-
return {
|
|
1656
|
-
pageTitle: page.pageTitle,
|
|
1657
|
-
url: page.url,
|
|
1658
|
-
totalOccurrencesFailedIncludingNeedsReview: page.totalOccurrencesFailedIncludingNeedsReview,
|
|
1659
|
-
totalOccurrencesFailedExcludingNeedsReview: page.totalOccurrencesFailedExcludingNeedsReview,
|
|
1660
|
-
totalOccurrencesMustFix: mustFixSum,
|
|
1661
|
-
totalOccurrencesGoodToFix: goodToFixSum,
|
|
1662
|
-
totalOccurrencesNeedsReview: needsReviewSum,
|
|
1663
|
-
totalOccurrencesPassed: page.totalOccurrencesPassed,
|
|
1664
|
-
typesOfIssuesExclusiveToNeedsReviewCount,
|
|
1665
|
-
typesOfIssuesCount: failedRuleCount,
|
|
1666
|
-
typesOfIssuesExcludingNeedsReviewCount,
|
|
1667
|
-
categoriesPresent,
|
|
1668
|
-
conformance,
|
|
1669
|
-
// Keep full detail for "scanPagesDetail"
|
|
1670
|
-
typesOfIssues: typesOfIssuesArray,
|
|
1671
|
-
};
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
// Transform raw pages
|
|
1675
|
-
const pagesAffected = pagesAffectedRaw.map(transformPageData);
|
|
1676
|
-
const pagesNotAffected = pagesNotAffectedRaw.map(transformPageData);
|
|
1677
|
-
|
|
1678
|
-
// --------------------------------------------
|
|
1679
|
-
// 8) Sort pages by typesOfIssuesCount (descending) for both arrays
|
|
1680
|
-
// --------------------------------------------
|
|
1681
|
-
pagesAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
|
|
1682
|
-
pagesNotAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
|
|
1683
|
-
|
|
1684
|
-
// --------------------------------------------
|
|
1685
|
-
// 9) Compute scanned/ skipped counts
|
|
1686
|
-
// --------------------------------------------
|
|
1687
|
-
const scannedPagesCount = pagesAffected.length + pagesNotAffected.length;
|
|
1688
|
-
const pagesNotScannedCount = Array.isArray(allIssues.pagesNotScanned)
|
|
1689
|
-
? allIssues.pagesNotScanned.length
|
|
1690
|
-
: 0;
|
|
1691
|
-
|
|
1692
|
-
// --------------------------------------------
|
|
1693
|
-
// 10) Build scanPagesDetail (with full "typesOfIssues")
|
|
1694
|
-
// --------------------------------------------
|
|
1695
|
-
allIssues.scanPagesDetail = {
|
|
1696
|
-
pagesAffected,
|
|
1697
|
-
pagesNotAffected,
|
|
1698
|
-
scannedPagesCount,
|
|
1699
|
-
pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
|
|
1700
|
-
pagesNotScannedCount,
|
|
1701
|
-
};
|
|
1702
|
-
|
|
1703
|
-
// --------------------------------------------
|
|
1704
|
-
// 11) Build scanPagesSummary (strip out "typesOfIssues")
|
|
1705
|
-
// --------------------------------------------
|
|
1706
|
-
function stripTypesOfIssues(page: ReturnType<typeof transformPageData>) {
|
|
1707
|
-
const { typesOfIssues, ...rest } = page;
|
|
1708
|
-
return rest;
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
const summaryPagesAffected = pagesAffected.map(stripTypesOfIssues);
|
|
1712
|
-
const summaryPagesNotAffected = pagesNotAffected.map(stripTypesOfIssues);
|
|
1713
|
-
|
|
1714
|
-
allIssues.scanPagesSummary = {
|
|
1715
|
-
pagesAffected: summaryPagesAffected,
|
|
1716
|
-
pagesNotAffected: summaryPagesNotAffected,
|
|
1717
|
-
scannedPagesCount,
|
|
1718
|
-
pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
|
|
1719
|
-
pagesNotScannedCount,
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
// Send WCAG criteria breakdown to Sentry
|
|
1724
|
-
export const sendWcagBreakdownToSentry = async (
|
|
1725
|
-
appVersion: string,
|
|
1726
|
-
wcagBreakdown: Map<string, number>,
|
|
1727
|
-
ruleIdJson: any,
|
|
1728
|
-
scanInfo: {
|
|
1729
|
-
entryUrl: string;
|
|
1730
|
-
scanType: string;
|
|
1731
|
-
browser: string;
|
|
1732
|
-
email?: string;
|
|
1733
|
-
name?: string;
|
|
1734
|
-
},
|
|
1735
|
-
allIssues?: AllIssues,
|
|
1736
|
-
pagesScannedCount: number = 0,
|
|
1737
|
-
) => {
|
|
1738
|
-
try {
|
|
1739
|
-
// Initialize Sentry
|
|
1740
|
-
Sentry.init(sentryConfig);
|
|
1741
|
-
// Set user ID for Sentry tracking
|
|
1742
|
-
const userData = getUserDataTxt();
|
|
1743
|
-
if (userData && userData.userId) {
|
|
1744
|
-
setSentryUser(userData.userId);
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// Prepare tags for the event
|
|
1748
|
-
const tags: Record<string, string> = {};
|
|
1749
|
-
const wcagCriteriaBreakdown: Record<string, any> = {};
|
|
1750
|
-
|
|
1751
|
-
// Tag app version
|
|
1752
|
-
tags.version = appVersion;
|
|
1753
|
-
|
|
1754
|
-
// Get dynamic WCAG criteria map once
|
|
1755
|
-
const wcagCriteriaMap = await getWcagCriteriaMap();
|
|
1756
|
-
|
|
1757
|
-
// Categorize all WCAG criteria for reporting
|
|
1758
|
-
const wcagIds = Array.from(
|
|
1759
|
-
new Set([...Object.keys(wcagCriteriaMap), ...Array.from(wcagBreakdown.keys())]),
|
|
1760
|
-
);
|
|
1761
|
-
const categorizedWcag = await categorizeWcagCriteria(wcagIds);
|
|
1762
|
-
|
|
1763
|
-
// First ensure all WCAG criteria are included in the tags with a value of 0
|
|
1764
|
-
// This ensures criteria with no violations are still reported
|
|
1765
|
-
for (const [wcagId, info] of Object.entries(wcagCriteriaMap)) {
|
|
1766
|
-
const formattedTag = await formatWcagTag(wcagId);
|
|
1767
|
-
if (formattedTag) {
|
|
1768
|
-
// Initialize with zero
|
|
1769
|
-
tags[formattedTag] = '0';
|
|
1770
|
-
|
|
1771
|
-
// Store in breakdown object with category information
|
|
1772
|
-
wcagCriteriaBreakdown[formattedTag] = {
|
|
1773
|
-
count: 0,
|
|
1774
|
-
category: categorizedWcag[wcagId] || 'mustFix', // Default to mustFix if not found
|
|
1775
|
-
};
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
// Now override with actual counts from the scan
|
|
1780
|
-
for (const [wcagId, count] of wcagBreakdown.entries()) {
|
|
1781
|
-
const formattedTag = await formatWcagTag(wcagId);
|
|
1782
|
-
if (formattedTag) {
|
|
1783
|
-
// Add as a tag with the count as value
|
|
1784
|
-
tags[formattedTag] = String(count);
|
|
1785
|
-
|
|
1786
|
-
// Update count in breakdown object
|
|
1787
|
-
if (wcagCriteriaBreakdown[formattedTag]) {
|
|
1788
|
-
wcagCriteriaBreakdown[formattedTag].count = count;
|
|
1789
|
-
} else {
|
|
1790
|
-
// If somehow this wasn't in our initial map
|
|
1791
|
-
wcagCriteriaBreakdown[formattedTag] = {
|
|
1792
|
-
count,
|
|
1793
|
-
category: categorizedWcag[wcagId] || 'mustFix',
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
// Calculate category counts based on actual issue counts from the report
|
|
1800
|
-
// rather than occurrence counts from wcagBreakdown
|
|
1801
|
-
const categoryCounts = {
|
|
1802
|
-
mustFix: 0,
|
|
1803
|
-
goodToFix: 0,
|
|
1804
|
-
needsReview: 0,
|
|
1805
|
-
};
|
|
1806
|
-
|
|
1807
|
-
if (allIssues) {
|
|
1808
|
-
// Use the actual report data for the counts
|
|
1809
|
-
categoryCounts.mustFix = allIssues.items.mustFix.rules.length;
|
|
1810
|
-
categoryCounts.goodToFix = allIssues.items.goodToFix.rules.length;
|
|
1811
|
-
categoryCounts.needsReview = allIssues.items.needsReview.rules.length;
|
|
1812
|
-
} else {
|
|
1813
|
-
// Fallback to the old way if allIssues not provided
|
|
1814
|
-
Object.values(wcagCriteriaBreakdown).forEach(item => {
|
|
1815
|
-
if (item.count > 0 && categoryCounts[item.category] !== undefined) {
|
|
1816
|
-
categoryCounts[item.category] += 1; // Count rules, not occurrences
|
|
1817
|
-
}
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
// Add category counts as tags
|
|
1822
|
-
tags['WCAG-MustFix-Count'] = String(categoryCounts.mustFix);
|
|
1823
|
-
tags['WCAG-GoodToFix-Count'] = String(categoryCounts.goodToFix);
|
|
1824
|
-
tags['WCAG-NeedsReview-Count'] = String(categoryCounts.needsReview);
|
|
1825
|
-
|
|
1826
|
-
// Also add occurrence counts for reference
|
|
1827
|
-
if (allIssues) {
|
|
1828
|
-
tags['WCAG-MustFix-Occurrences'] = String(allIssues.items.mustFix.totalItems);
|
|
1829
|
-
tags['WCAG-GoodToFix-Occurrences'] = String(allIssues.items.goodToFix.totalItems);
|
|
1830
|
-
tags['WCAG-NeedsReview-Occurrences'] = String(allIssues.items.needsReview.totalItems);
|
|
1831
|
-
|
|
1832
|
-
// Add number of pages scanned tag
|
|
1833
|
-
tags['Pages-Scanned-Count'] = String(allIssues.totalPagesScanned);
|
|
1834
|
-
} else if (pagesScannedCount > 0) {
|
|
1835
|
-
// Still add the pages scanned count even if we don't have allIssues
|
|
1836
|
-
tags['Pages-Scanned-Count'] = String(pagesScannedCount);
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
// Send the event to Sentry
|
|
1840
|
-
await Sentry.captureEvent({
|
|
1841
|
-
message: 'Accessibility Scan Completed',
|
|
1842
|
-
level: 'info',
|
|
1843
|
-
tags: {
|
|
1844
|
-
...tags,
|
|
1845
|
-
event_type: 'accessibility_scan',
|
|
1846
|
-
scanType: scanInfo.scanType,
|
|
1847
|
-
browser: scanInfo.browser,
|
|
1848
|
-
entryUrl: scanInfo.entryUrl,
|
|
1849
|
-
},
|
|
1850
|
-
user: {
|
|
1851
|
-
...(scanInfo.email && scanInfo.name
|
|
1852
|
-
? {
|
|
1853
|
-
email: scanInfo.email,
|
|
1854
|
-
username: scanInfo.name,
|
|
1855
|
-
}
|
|
1856
|
-
: {}),
|
|
1857
|
-
...(userData && userData.userId ? { id: userData.userId } : {}),
|
|
1858
|
-
},
|
|
1859
|
-
extra: {
|
|
1860
|
-
additionalScanMetadata: ruleIdJson != null ? JSON.stringify(ruleIdJson) : '{}',
|
|
1861
|
-
wcagBreakdown: wcagCriteriaBreakdown,
|
|
1862
|
-
reportCounts: allIssues
|
|
1863
|
-
? {
|
|
1864
|
-
mustFix: {
|
|
1865
|
-
issues: allIssues.items.mustFix.rules?.length ?? 0,
|
|
1866
|
-
occurrences: allIssues.items.mustFix.totalItems ?? 0,
|
|
1867
|
-
},
|
|
1868
|
-
goodToFix: {
|
|
1869
|
-
issues: allIssues.items.goodToFix.rules?.length ?? 0,
|
|
1870
|
-
occurrences: allIssues.items.goodToFix.totalItems ?? 0,
|
|
1871
|
-
},
|
|
1872
|
-
needsReview: {
|
|
1873
|
-
issues: allIssues.items.needsReview.rules?.length ?? 0,
|
|
1874
|
-
occurrences: allIssues.items.needsReview.totalItems ?? 0,
|
|
1875
|
-
},
|
|
1876
|
-
}
|
|
1877
|
-
: undefined,
|
|
1878
|
-
},
|
|
1879
|
-
});
|
|
1880
|
-
|
|
1881
|
-
// Wait for events to be sent
|
|
1882
|
-
await Sentry.flush(2000);
|
|
1883
|
-
} catch (error) {
|
|
1884
|
-
console.error('Error sending WCAG breakdown to Sentry:', error);
|
|
1885
|
-
}
|
|
1886
|
-
};
|
|
1887
|
-
|
|
1888
651
|
const formatAboutStartTime = (dateString: string) => {
|
|
1889
652
|
const utcStartTimeDate = new Date(dateString);
|
|
1890
653
|
const formattedStartTime = utcStartTimeDate.toLocaleTimeString('en-GB', {
|
|
@@ -1939,7 +702,6 @@ const generateArtifacts = async (
|
|
|
1939
702
|
zip: string = undefined, // optional
|
|
1940
703
|
generateJsonFiles = false,
|
|
1941
704
|
) => {
|
|
1942
|
-
|
|
1943
705
|
consoleLogger.info('Generating report artifacts');
|
|
1944
706
|
|
|
1945
707
|
const storagePath = getStoragePath(randomToken);
|
|
@@ -2037,7 +799,7 @@ const generateArtifacts = async (
|
|
|
2037
799
|
await pushResults(pageResults, allIssues, isCustomFlow);
|
|
2038
800
|
}),
|
|
2039
801
|
).catch(flattenIssuesError => {
|
|
2040
|
-
|
|
802
|
+
consoleLogger.error(
|
|
2041
803
|
`[generateArtifacts] Error flattening issues: ${flattenIssuesError?.stack || flattenIssuesError}`,
|
|
2042
804
|
);
|
|
2043
805
|
});
|
|
@@ -2176,22 +938,22 @@ const generateArtifacts = async (
|
|
|
2176
938
|
scanItemsBase64FilePath,
|
|
2177
939
|
);
|
|
2178
940
|
|
|
2179
|
-
if (!generateJsonFiles) {
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
}
|
|
941
|
+
if (!generateJsonFiles) {
|
|
942
|
+
await cleanUpJsonFiles([
|
|
943
|
+
scanDataJsonFilePath,
|
|
944
|
+
scanDataBase64FilePath,
|
|
945
|
+
scanItemsJsonFilePath,
|
|
946
|
+
scanItemsBase64FilePath,
|
|
947
|
+
scanItemsSummaryJsonFilePath,
|
|
948
|
+
scanItemsSummaryBase64FilePath,
|
|
949
|
+
scanIssuesSummaryJsonFilePath,
|
|
950
|
+
scanIssuesSummaryBase64FilePath,
|
|
951
|
+
scanPagesDetailJsonFilePath,
|
|
952
|
+
scanPagesDetailBase64FilePath,
|
|
953
|
+
scanPagesSummaryJsonFilePath,
|
|
954
|
+
scanPagesSummaryBase64FilePath,
|
|
955
|
+
]);
|
|
956
|
+
}
|
|
2195
957
|
|
|
2196
958
|
const browserChannel = getBrowserToRun(randomToken, BrowserTypes.CHROME, false).browserToRun;
|
|
2197
959
|
|
|
@@ -2300,8 +1062,10 @@ if (!generateJsonFiles) {
|
|
|
2300
1062
|
export {
|
|
2301
1063
|
writeHTML,
|
|
2302
1064
|
compressJsonFileStreaming,
|
|
1065
|
+
convertItemsToReferences,
|
|
2303
1066
|
flattenAndSortResults,
|
|
2304
1067
|
populateScanPagesDetail,
|
|
1068
|
+
sendWcagBreakdownToSentry,
|
|
2305
1069
|
getWcagPassPercentage,
|
|
2306
1070
|
getProgressPercentage,
|
|
2307
1071
|
getIssuesPercentage,
|
|
@@ -2311,4 +1075,4 @@ export {
|
|
|
2311
1075
|
formatAboutStartTime,
|
|
2312
1076
|
};
|
|
2313
1077
|
|
|
2314
|
-
export default generateArtifacts;
|
|
1078
|
+
export default generateArtifacts;
|