@govtechsg/oobee 0.10.83 → 0.10.85
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/cli.js +7 -6
- package/dist/constants/common.js +13 -1
- package/dist/crawlers/crawlDomain.js +220 -120
- package/dist/crawlers/crawlIntelligentSitemap.js +22 -7
- package/dist/crawlers/custom/utils.js +81 -40
- package/dist/crawlers/runCustom.js +13 -5
- 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 +64 -950
- package/dist/proxyService.js +90 -5
- package/dist/utils.js +20 -7
- package/package.json +6 -6
- package/src/cli.ts +20 -15
- package/src/constants/common.ts +13 -1
- package/src/crawlers/crawlDomain.ts +248 -137
- package/src/crawlers/crawlIntelligentSitemap.ts +22 -8
- package/src/crawlers/custom/utils.ts +103 -48
- package/src/crawlers/runCustom.ts +18 -5
- 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 +125 -1344
- package/src/proxyService.ts +96 -4
- package/src/utils.ts +19 -7
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import zlib from 'zlib';
|
|
4
|
+
import { Base64Encode } from 'base64-stream';
|
|
5
|
+
import { pipeline } from 'stream/promises';
|
|
6
|
+
import { a11yRuleShortDescriptionMap } from '../constants/constants.js';
|
|
7
|
+
import { consoleLogger } from '../logs.js';
|
|
8
|
+
function* serializeObject(obj, depth = 0, indent = ' ') {
|
|
9
|
+
const currentIndent = indent.repeat(depth);
|
|
10
|
+
const nextIndent = indent.repeat(depth + 1);
|
|
11
|
+
if (obj instanceof Date) {
|
|
12
|
+
yield JSON.stringify(obj.toISOString());
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(obj)) {
|
|
16
|
+
yield '[\n';
|
|
17
|
+
for (let i = 0; i < obj.length; i++) {
|
|
18
|
+
if (i > 0)
|
|
19
|
+
yield ',\n';
|
|
20
|
+
yield nextIndent;
|
|
21
|
+
yield* serializeObject(obj[i], depth + 1, indent);
|
|
22
|
+
}
|
|
23
|
+
yield `\n${currentIndent}]`;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (obj !== null && typeof obj === 'object') {
|
|
27
|
+
yield '{\n';
|
|
28
|
+
const keys = Object.keys(obj);
|
|
29
|
+
for (let i = 0; i < keys.length; i++) {
|
|
30
|
+
const key = keys[i];
|
|
31
|
+
if (i > 0)
|
|
32
|
+
yield ',\n';
|
|
33
|
+
yield `${nextIndent}${JSON.stringify(key)}: `;
|
|
34
|
+
yield* serializeObject(obj[key], depth + 1, indent);
|
|
35
|
+
}
|
|
36
|
+
yield `\n${currentIndent}}`;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (obj === null || typeof obj === 'function' || typeof obj === 'undefined') {
|
|
40
|
+
yield 'null';
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
yield JSON.stringify(obj);
|
|
44
|
+
}
|
|
45
|
+
function writeLargeJsonToFile(obj, filePath) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const writeStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
|
48
|
+
writeStream.on('error', error => {
|
|
49
|
+
consoleLogger.error('Stream error:', error);
|
|
50
|
+
reject(error);
|
|
51
|
+
});
|
|
52
|
+
writeStream.on('finish', () => {
|
|
53
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
54
|
+
resolve(true);
|
|
55
|
+
});
|
|
56
|
+
const generator = serializeObject(obj);
|
|
57
|
+
function write() {
|
|
58
|
+
let next;
|
|
59
|
+
while (!(next = generator.next()).done) {
|
|
60
|
+
if (!writeStream.write(next.value)) {
|
|
61
|
+
writeStream.once('drain', write);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
writeStream.end();
|
|
66
|
+
}
|
|
67
|
+
write();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const writeLargeScanItemsJsonToFile = async (obj, filePath) => {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const writeStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
|
|
73
|
+
const writeQueue = [];
|
|
74
|
+
let isWriting = false;
|
|
75
|
+
const processNextWrite = async () => {
|
|
76
|
+
if (isWriting || writeQueue.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
isWriting = true;
|
|
79
|
+
const data = writeQueue.shift();
|
|
80
|
+
try {
|
|
81
|
+
if (!writeStream.write(data)) {
|
|
82
|
+
await new Promise(resolve => {
|
|
83
|
+
writeStream.once('drain', () => {
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
writeStream.destroy(error);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
isWriting = false;
|
|
94
|
+
processNextWrite();
|
|
95
|
+
};
|
|
96
|
+
const queueWrite = (data) => {
|
|
97
|
+
writeQueue.push(data);
|
|
98
|
+
processNextWrite();
|
|
99
|
+
};
|
|
100
|
+
writeStream.on('error', error => {
|
|
101
|
+
consoleLogger.error(`Error writing object to JSON file: ${error}`);
|
|
102
|
+
reject(error);
|
|
103
|
+
});
|
|
104
|
+
writeStream.on('finish', () => {
|
|
105
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
106
|
+
resolve(true);
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
queueWrite('{\n');
|
|
110
|
+
const keys = Object.keys(obj);
|
|
111
|
+
keys.forEach((key, i) => {
|
|
112
|
+
const value = obj[key];
|
|
113
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
114
|
+
queueWrite(` "${key}": ${JSON.stringify(value)}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
queueWrite(` "${key}": {\n`);
|
|
118
|
+
const { rules, ...otherProperties } = value;
|
|
119
|
+
Object.entries(otherProperties).forEach(([propKey, propValue], j) => {
|
|
120
|
+
const propValueString = propValue === null ||
|
|
121
|
+
typeof propValue === 'function' ||
|
|
122
|
+
typeof propValue === 'undefined'
|
|
123
|
+
? 'null'
|
|
124
|
+
: JSON.stringify(propValue);
|
|
125
|
+
queueWrite(` "${propKey}": ${propValueString}`);
|
|
126
|
+
if (j < Object.keys(otherProperties).length - 1 || (rules && rules.length >= 0)) {
|
|
127
|
+
queueWrite(',\n');
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
queueWrite('\n');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
if (rules && Array.isArray(rules)) {
|
|
134
|
+
queueWrite(' "rules": [\n');
|
|
135
|
+
rules.forEach((rule, j) => {
|
|
136
|
+
queueWrite(' {\n');
|
|
137
|
+
const { pagesAffected, ...otherRuleProperties } = rule;
|
|
138
|
+
Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
|
|
139
|
+
const ruleValueString = ruleValue === null ||
|
|
140
|
+
typeof ruleValue === 'function' ||
|
|
141
|
+
typeof ruleValue === 'undefined'
|
|
142
|
+
? 'null'
|
|
143
|
+
: JSON.stringify(ruleValue);
|
|
144
|
+
queueWrite(` "${ruleKey}": ${ruleValueString}`);
|
|
145
|
+
if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
|
|
146
|
+
queueWrite(',\n');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
queueWrite('\n');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (pagesAffected && Array.isArray(pagesAffected)) {
|
|
153
|
+
queueWrite(' "pagesAffected": [\n');
|
|
154
|
+
pagesAffected.forEach((page, p) => {
|
|
155
|
+
const pageJson = JSON.stringify(page, null, 2)
|
|
156
|
+
.split('\n')
|
|
157
|
+
.map(line => ` ${line}`)
|
|
158
|
+
.join('\n');
|
|
159
|
+
queueWrite(pageJson);
|
|
160
|
+
if (p < pagesAffected.length - 1) {
|
|
161
|
+
queueWrite(',\n');
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
queueWrite('\n');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
queueWrite(' ]');
|
|
168
|
+
}
|
|
169
|
+
queueWrite('\n }');
|
|
170
|
+
if (j < rules.length - 1) {
|
|
171
|
+
queueWrite(',\n');
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
queueWrite('\n');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
queueWrite(' ]');
|
|
178
|
+
}
|
|
179
|
+
queueWrite('\n }');
|
|
180
|
+
}
|
|
181
|
+
if (i < keys.length - 1) {
|
|
182
|
+
queueWrite(',\n');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
queueWrite('\n');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
queueWrite('}\n');
|
|
189
|
+
const checkQueueAndEnd = () => {
|
|
190
|
+
if (writeQueue.length === 0 && !isWriting) {
|
|
191
|
+
writeStream.end();
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
setTimeout(checkQueueAndEnd, 100);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
checkQueueAndEnd();
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
writeStream.destroy(err);
|
|
201
|
+
reject(err);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
async function compressJsonFileStreaming(inputPath, outputPath) {
|
|
206
|
+
const readStream = fs.createReadStream(inputPath);
|
|
207
|
+
const writeStream = fs.createWriteStream(outputPath);
|
|
208
|
+
const gzip = zlib.createGzip();
|
|
209
|
+
const base64Encode = new Base64Encode();
|
|
210
|
+
await pipeline(readStream, gzip, base64Encode, writeStream);
|
|
211
|
+
consoleLogger.info(`File successfully compressed and saved to ${outputPath}`);
|
|
212
|
+
}
|
|
213
|
+
const writeJsonFileAndCompressedJsonFile = async (data, storagePath, filename) => {
|
|
214
|
+
try {
|
|
215
|
+
consoleLogger.info(`Writing JSON to ${filename}.json`);
|
|
216
|
+
const jsonFilePath = path.join(storagePath, `${filename}.json`);
|
|
217
|
+
if (filename === 'scanItems') {
|
|
218
|
+
await writeLargeScanItemsJsonToFile(data, jsonFilePath);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
await writeLargeJsonToFile(data, jsonFilePath);
|
|
222
|
+
}
|
|
223
|
+
consoleLogger.info(`Reading ${filename}.json, gzipping and base64 encoding it into ${filename}.json.gz.b64`);
|
|
224
|
+
const base64FilePath = path.join(storagePath, `${filename}.json.gz.b64`);
|
|
225
|
+
await compressJsonFileStreaming(jsonFilePath, base64FilePath);
|
|
226
|
+
consoleLogger.info(`Finished compression and base64 encoding for ${filename}`);
|
|
227
|
+
return {
|
|
228
|
+
jsonFilePath,
|
|
229
|
+
base64FilePath,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
consoleLogger.error(`Error compressing and encoding ${filename}`);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const writeJsonAndBase64Files = async (allIssues, storagePath) => {
|
|
238
|
+
const { items, ...rest } = allIssues;
|
|
239
|
+
const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } = await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
|
|
240
|
+
const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...items }, storagePath, 'scanItems');
|
|
241
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
242
|
+
if (items[category].rules && Array.isArray(items[category].rules)) {
|
|
243
|
+
items[category].rules.forEach(rule => {
|
|
244
|
+
rule.pagesAffectedCount = Array.isArray(rule.pagesAffected) ? rule.pagesAffected.length : 0;
|
|
245
|
+
});
|
|
246
|
+
items[category].rules.sort((a, b) => (b.pagesAffectedCount || 0) - (a.pagesAffectedCount || 0));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const scanIssuesSummary = {
|
|
250
|
+
mustFix: items.mustFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
251
|
+
...ruleInfo,
|
|
252
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
253
|
+
})),
|
|
254
|
+
goodToFix: items.goodToFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
255
|
+
...ruleInfo,
|
|
256
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
257
|
+
})),
|
|
258
|
+
needsReview: items.needsReview.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
259
|
+
...ruleInfo,
|
|
260
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
261
|
+
})),
|
|
262
|
+
passed: items.passed.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
263
|
+
...ruleInfo,
|
|
264
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
265
|
+
})),
|
|
266
|
+
};
|
|
267
|
+
const { jsonFilePath: scanIssuesSummaryJsonFilePath, base64FilePath: scanIssuesSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanIssuesSummary }, storagePath, 'scanIssuesSummary');
|
|
268
|
+
items.mustFix.rules.forEach(rule => {
|
|
269
|
+
rule.pagesAffected.forEach(page => {
|
|
270
|
+
page.itemsCount = page.items.length;
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
items.goodToFix.rules.forEach(rule => {
|
|
274
|
+
rule.pagesAffected.forEach(page => {
|
|
275
|
+
page.itemsCount = page.items.length;
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
items.needsReview.rules.forEach(rule => {
|
|
279
|
+
rule.pagesAffected.forEach(page => {
|
|
280
|
+
page.itemsCount = page.items.length;
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
items.passed.rules.forEach(rule => {
|
|
284
|
+
rule.pagesAffected.forEach(page => {
|
|
285
|
+
page.itemsCount = page.items.length;
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
items.mustFix.totalRuleIssues = items.mustFix.rules.length;
|
|
289
|
+
items.goodToFix.totalRuleIssues = items.goodToFix.rules.length;
|
|
290
|
+
items.needsReview.totalRuleIssues = items.needsReview.rules.length;
|
|
291
|
+
items.passed.totalRuleIssues = items.passed.rules.length;
|
|
292
|
+
const { topTenPagesWithMostIssues, wcagLinks, wcagPassPercentage, progressPercentage, issuesPercentage, totalPagesScanned, totalPagesNotScanned, topTenIssues, } = rest;
|
|
293
|
+
const summaryItems = {
|
|
294
|
+
mustFix: {
|
|
295
|
+
totalItems: items.mustFix?.totalItems || 0,
|
|
296
|
+
totalRuleIssues: items.mustFix?.totalRuleIssues || 0,
|
|
297
|
+
},
|
|
298
|
+
goodToFix: {
|
|
299
|
+
totalItems: items.goodToFix?.totalItems || 0,
|
|
300
|
+
totalRuleIssues: items.goodToFix?.totalRuleIssues || 0,
|
|
301
|
+
},
|
|
302
|
+
needsReview: {
|
|
303
|
+
totalItems: items.needsReview?.totalItems || 0,
|
|
304
|
+
totalRuleIssues: items.needsReview?.totalRuleIssues || 0,
|
|
305
|
+
},
|
|
306
|
+
topTenPagesWithMostIssues,
|
|
307
|
+
wcagLinks,
|
|
308
|
+
wcagPassPercentage,
|
|
309
|
+
progressPercentage,
|
|
310
|
+
issuesPercentage,
|
|
311
|
+
totalPagesScanned,
|
|
312
|
+
totalPagesNotScanned,
|
|
313
|
+
topTenIssues,
|
|
314
|
+
};
|
|
315
|
+
const { jsonFilePath: scanItemsSummaryJsonFilePath, base64FilePath: scanItemsSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItems }, storagePath, 'scanItemsSummary');
|
|
316
|
+
const { jsonFilePath: scanPagesDetailJsonFilePath, base64FilePath: scanPagesDetailBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesDetail }, storagePath, 'scanPagesDetail');
|
|
317
|
+
const { jsonFilePath: scanPagesSummaryJsonFilePath, base64FilePath: scanPagesSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesSummary }, storagePath, 'scanPagesSummary');
|
|
318
|
+
return {
|
|
319
|
+
scanDataJsonFilePath,
|
|
320
|
+
scanDataBase64FilePath,
|
|
321
|
+
scanItemsJsonFilePath,
|
|
322
|
+
scanItemsBase64FilePath,
|
|
323
|
+
scanItemsSummaryJsonFilePath,
|
|
324
|
+
scanItemsSummaryBase64FilePath,
|
|
325
|
+
scanIssuesSummaryJsonFilePath,
|
|
326
|
+
scanIssuesSummaryBase64FilePath,
|
|
327
|
+
scanPagesDetailJsonFilePath,
|
|
328
|
+
scanPagesDetailBase64FilePath,
|
|
329
|
+
scanPagesSummaryJsonFilePath,
|
|
330
|
+
scanPagesSummaryBase64FilePath,
|
|
331
|
+
scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
|
|
332
|
+
scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
export { compressJsonFileStreaming, writeJsonAndBase64Files, writeJsonFileAndCompressedJsonFile };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build allIssues.scanPagesDetail and allIssues.scanPagesSummary
|
|
3
|
+
* by analyzing pagesScanned (including mustFix/goodToFix/etc.).
|
|
4
|
+
*/
|
|
5
|
+
export default function populateScanPagesDetail(allIssues) {
|
|
6
|
+
const allScannedPages = Array.isArray(allIssues.pagesScanned) ? allIssues.pagesScanned : [];
|
|
7
|
+
const mustFixCategory = 'mustFix';
|
|
8
|
+
const goodToFixCategory = 'goodToFix';
|
|
9
|
+
const needsReviewCategory = 'needsReview';
|
|
10
|
+
const passedCategory = 'passed';
|
|
11
|
+
const pagesMap = {};
|
|
12
|
+
Object.entries(allIssues.items).forEach(([categoryName, categoryData]) => {
|
|
13
|
+
if (!categoryData?.rules)
|
|
14
|
+
return;
|
|
15
|
+
categoryData.rules.forEach(rule => {
|
|
16
|
+
const { rule: ruleId, conformance = [] } = rule;
|
|
17
|
+
rule.pagesAffected.forEach(p => {
|
|
18
|
+
const { url, pageTitle, items = [] } = p;
|
|
19
|
+
const itemsCount = items.length;
|
|
20
|
+
if (!pagesMap[url]) {
|
|
21
|
+
pagesMap[url] = {
|
|
22
|
+
pageTitle,
|
|
23
|
+
url,
|
|
24
|
+
totalOccurrencesFailedIncludingNeedsReview: 0,
|
|
25
|
+
totalOccurrencesFailedExcludingNeedsReview: 0,
|
|
26
|
+
totalOccurrencesNeedsReview: 0,
|
|
27
|
+
totalOccurrencesPassed: 0,
|
|
28
|
+
typesOfIssues: {},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (!pagesMap[url].typesOfIssues[ruleId]) {
|
|
32
|
+
pagesMap[url].typesOfIssues[ruleId] = {
|
|
33
|
+
ruleId,
|
|
34
|
+
wcagConformance: conformance,
|
|
35
|
+
occurrencesMustFix: 0,
|
|
36
|
+
occurrencesGoodToFix: 0,
|
|
37
|
+
occurrencesNeedsReview: 0,
|
|
38
|
+
occurrencesPassed: 0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (categoryName === mustFixCategory) {
|
|
42
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesMustFix += itemsCount;
|
|
43
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
44
|
+
pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
|
|
45
|
+
}
|
|
46
|
+
else if (categoryName === goodToFixCategory) {
|
|
47
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesGoodToFix += itemsCount;
|
|
48
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
49
|
+
pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
|
|
50
|
+
}
|
|
51
|
+
else if (categoryName === needsReviewCategory) {
|
|
52
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesNeedsReview += itemsCount;
|
|
53
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
54
|
+
pagesMap[url].totalOccurrencesNeedsReview += itemsCount;
|
|
55
|
+
}
|
|
56
|
+
else if (categoryName === passedCategory) {
|
|
57
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesPassed += itemsCount;
|
|
58
|
+
pagesMap[url].totalOccurrencesPassed += itemsCount;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const pagesInMap = Object.values(pagesMap);
|
|
64
|
+
const pagesInMapUrls = new Set(Object.keys(pagesMap));
|
|
65
|
+
const pagesAllPassed = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview === 0);
|
|
66
|
+
const pagesNoEntries = allScannedPages
|
|
67
|
+
.filter(sp => !pagesInMapUrls.has(sp.url))
|
|
68
|
+
.map(sp => ({
|
|
69
|
+
pageTitle: sp.pageTitle,
|
|
70
|
+
url: sp.url,
|
|
71
|
+
totalOccurrencesFailedIncludingNeedsReview: 0,
|
|
72
|
+
totalOccurrencesFailedExcludingNeedsReview: 0,
|
|
73
|
+
totalOccurrencesNeedsReview: 0,
|
|
74
|
+
totalOccurrencesPassed: 0,
|
|
75
|
+
typesOfIssues: {},
|
|
76
|
+
}));
|
|
77
|
+
const pagesNotAffectedRaw = [...pagesAllPassed, ...pagesNoEntries];
|
|
78
|
+
const pagesAffectedRaw = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview > 0);
|
|
79
|
+
function transformPageData(page) {
|
|
80
|
+
const typesOfIssuesArray = Object.values(page.typesOfIssues);
|
|
81
|
+
const mustFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesMustFix, 0);
|
|
82
|
+
const goodToFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesGoodToFix, 0);
|
|
83
|
+
const needsReviewSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesNeedsReview, 0);
|
|
84
|
+
const categoriesPresent = [];
|
|
85
|
+
if (mustFixSum > 0)
|
|
86
|
+
categoriesPresent.push('mustFix');
|
|
87
|
+
if (goodToFixSum > 0)
|
|
88
|
+
categoriesPresent.push('goodToFix');
|
|
89
|
+
if (needsReviewSum > 0)
|
|
90
|
+
categoriesPresent.push('needsReview');
|
|
91
|
+
const failedRuleIds = new Set();
|
|
92
|
+
typesOfIssuesArray.forEach(r => {
|
|
93
|
+
if ((r.occurrencesMustFix || 0) > 0 ||
|
|
94
|
+
(r.occurrencesGoodToFix || 0) > 0 ||
|
|
95
|
+
(r.occurrencesNeedsReview || 0) > 0) {
|
|
96
|
+
failedRuleIds.add(r.ruleId);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const failedRuleCount = failedRuleIds.size;
|
|
100
|
+
const typesOfIssuesExcludingNeedsReviewCount = typesOfIssuesArray.filter(r => (r.occurrencesMustFix || 0) + (r.occurrencesGoodToFix || 0) > 0).length;
|
|
101
|
+
const typesOfIssuesExclusiveToNeedsReviewCount = typesOfIssuesArray.filter(r => (r.occurrencesNeedsReview || 0) > 0 &&
|
|
102
|
+
(r.occurrencesMustFix || 0) === 0 &&
|
|
103
|
+
(r.occurrencesGoodToFix || 0) === 0).length;
|
|
104
|
+
const allConformance = typesOfIssuesArray.reduce((acc, curr) => {
|
|
105
|
+
const nonPassedCount = (curr.occurrencesMustFix || 0) +
|
|
106
|
+
(curr.occurrencesGoodToFix || 0) +
|
|
107
|
+
(curr.occurrencesNeedsReview || 0);
|
|
108
|
+
if (nonPassedCount > 0) {
|
|
109
|
+
return acc.concat(curr.wcagConformance || []);
|
|
110
|
+
}
|
|
111
|
+
return acc;
|
|
112
|
+
}, []);
|
|
113
|
+
const conformance = Array.from(new Set(allConformance));
|
|
114
|
+
return {
|
|
115
|
+
pageTitle: page.pageTitle,
|
|
116
|
+
url: page.url,
|
|
117
|
+
totalOccurrencesFailedIncludingNeedsReview: page.totalOccurrencesFailedIncludingNeedsReview,
|
|
118
|
+
totalOccurrencesFailedExcludingNeedsReview: page.totalOccurrencesFailedExcludingNeedsReview,
|
|
119
|
+
totalOccurrencesMustFix: mustFixSum,
|
|
120
|
+
totalOccurrencesGoodToFix: goodToFixSum,
|
|
121
|
+
totalOccurrencesNeedsReview: needsReviewSum,
|
|
122
|
+
totalOccurrencesPassed: page.totalOccurrencesPassed,
|
|
123
|
+
typesOfIssuesExclusiveToNeedsReviewCount,
|
|
124
|
+
typesOfIssuesCount: failedRuleCount,
|
|
125
|
+
typesOfIssuesExcludingNeedsReviewCount,
|
|
126
|
+
categoriesPresent,
|
|
127
|
+
conformance,
|
|
128
|
+
typesOfIssues: typesOfIssuesArray,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const pagesAffected = pagesAffectedRaw.map(transformPageData);
|
|
132
|
+
const pagesNotAffected = pagesNotAffectedRaw.map(transformPageData);
|
|
133
|
+
pagesAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
|
|
134
|
+
pagesNotAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
|
|
135
|
+
const scannedPagesCount = pagesAffected.length + pagesNotAffected.length;
|
|
136
|
+
const pagesNotScannedCount = Array.isArray(allIssues.pagesNotScanned)
|
|
137
|
+
? allIssues.pagesNotScanned.length
|
|
138
|
+
: 0;
|
|
139
|
+
allIssues.scanPagesDetail = {
|
|
140
|
+
pagesAffected,
|
|
141
|
+
pagesNotAffected,
|
|
142
|
+
scannedPagesCount,
|
|
143
|
+
pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
|
|
144
|
+
pagesNotScannedCount,
|
|
145
|
+
};
|
|
146
|
+
function stripTypesOfIssues(page) {
|
|
147
|
+
const { typesOfIssues, ...rest } = page;
|
|
148
|
+
return rest;
|
|
149
|
+
}
|
|
150
|
+
const summaryPagesAffected = pagesAffected.map(stripTypesOfIssues);
|
|
151
|
+
const summaryPagesNotAffected = pagesNotAffected.map(stripTypesOfIssues);
|
|
152
|
+
allIssues.scanPagesSummary = {
|
|
153
|
+
pagesAffected: summaryPagesAffected,
|
|
154
|
+
pagesNotAffected: summaryPagesNotAffected,
|
|
155
|
+
scannedPagesCount,
|
|
156
|
+
pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
|
|
157
|
+
pagesNotScannedCount,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/node';
|
|
2
|
+
import { sentryConfig, setSentryUser } from '../constants/constants.js';
|
|
3
|
+
import { categorizeWcagCriteria, getUserDataTxt, getWcagCriteriaMap } from '../utils.js';
|
|
4
|
+
// Format WCAG tag in requested format: wcag111a_Occurrences
|
|
5
|
+
const formatWcagTag = async (wcagId) => {
|
|
6
|
+
// Get dynamic WCAG criteria map
|
|
7
|
+
const wcagCriteriaMap = await getWcagCriteriaMap();
|
|
8
|
+
if (wcagCriteriaMap[wcagId]) {
|
|
9
|
+
const { level } = wcagCriteriaMap[wcagId];
|
|
10
|
+
return `${wcagId}${level}_Occurrences`;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
// Send WCAG criteria breakdown to Sentry
|
|
15
|
+
const sendWcagBreakdownToSentry = async (appVersion, wcagBreakdown, ruleIdJson, scanInfo, allIssues, pagesScannedCount = 0) => {
|
|
16
|
+
try {
|
|
17
|
+
// Initialize Sentry
|
|
18
|
+
Sentry.init(sentryConfig);
|
|
19
|
+
// Set user ID for Sentry tracking
|
|
20
|
+
const userData = getUserDataTxt();
|
|
21
|
+
if (userData && userData.userId) {
|
|
22
|
+
setSentryUser(userData.userId);
|
|
23
|
+
}
|
|
24
|
+
// Prepare tags for the event
|
|
25
|
+
const tags = {};
|
|
26
|
+
const wcagCriteriaBreakdown = {};
|
|
27
|
+
// Tag app version
|
|
28
|
+
tags.version = appVersion;
|
|
29
|
+
// Get dynamic WCAG criteria map once
|
|
30
|
+
const wcagCriteriaMap = await getWcagCriteriaMap();
|
|
31
|
+
// Categorize all WCAG criteria for reporting
|
|
32
|
+
const wcagIds = Array.from(new Set([...Object.keys(wcagCriteriaMap), ...Array.from(wcagBreakdown.keys())]));
|
|
33
|
+
const categorizedWcag = await categorizeWcagCriteria(wcagIds);
|
|
34
|
+
// First ensure all WCAG criteria are included in the tags with a value of 0
|
|
35
|
+
// This ensures criteria with no violations are still reported
|
|
36
|
+
for (const [wcagId, info] of Object.entries(wcagCriteriaMap)) {
|
|
37
|
+
const formattedTag = await formatWcagTag(wcagId);
|
|
38
|
+
if (formattedTag) {
|
|
39
|
+
// Initialize with zero
|
|
40
|
+
tags[formattedTag] = '0';
|
|
41
|
+
// Store in breakdown object with category information
|
|
42
|
+
wcagCriteriaBreakdown[formattedTag] = {
|
|
43
|
+
count: 0,
|
|
44
|
+
category: categorizedWcag[wcagId] || 'mustFix', // Default to mustFix if not found
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Now override with actual counts from the scan
|
|
49
|
+
for (const [wcagId, count] of wcagBreakdown.entries()) {
|
|
50
|
+
const formattedTag = await formatWcagTag(wcagId);
|
|
51
|
+
if (formattedTag) {
|
|
52
|
+
// Add as a tag with the count as value
|
|
53
|
+
tags[formattedTag] = String(count);
|
|
54
|
+
// Update count in breakdown object
|
|
55
|
+
if (wcagCriteriaBreakdown[formattedTag]) {
|
|
56
|
+
wcagCriteriaBreakdown[formattedTag].count = count;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// If somehow this wasn't in our initial map
|
|
60
|
+
wcagCriteriaBreakdown[formattedTag] = {
|
|
61
|
+
count,
|
|
62
|
+
category: categorizedWcag[wcagId] || 'mustFix',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Calculate category counts based on actual issue counts from the report
|
|
68
|
+
// rather than occurrence counts from wcagBreakdown
|
|
69
|
+
const categoryCounts = {
|
|
70
|
+
mustFix: 0,
|
|
71
|
+
goodToFix: 0,
|
|
72
|
+
needsReview: 0,
|
|
73
|
+
};
|
|
74
|
+
if (allIssues) {
|
|
75
|
+
// Use the actual report data for the counts
|
|
76
|
+
categoryCounts.mustFix = allIssues.items.mustFix.rules.length;
|
|
77
|
+
categoryCounts.goodToFix = allIssues.items.goodToFix.rules.length;
|
|
78
|
+
categoryCounts.needsReview = allIssues.items.needsReview.rules.length;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Fallback to the old way if allIssues not provided
|
|
82
|
+
Object.values(wcagCriteriaBreakdown).forEach(item => {
|
|
83
|
+
if (item.count > 0 && categoryCounts[item.category] !== undefined) {
|
|
84
|
+
categoryCounts[item.category] += 1; // Count rules, not occurrences
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Add category counts as tags
|
|
89
|
+
tags['WCAG-MustFix-Count'] = String(categoryCounts.mustFix);
|
|
90
|
+
tags['WCAG-GoodToFix-Count'] = String(categoryCounts.goodToFix);
|
|
91
|
+
tags['WCAG-NeedsReview-Count'] = String(categoryCounts.needsReview);
|
|
92
|
+
// Also add occurrence counts for reference
|
|
93
|
+
if (allIssues) {
|
|
94
|
+
tags['WCAG-MustFix-Occurrences'] = String(allIssues.items.mustFix.totalItems);
|
|
95
|
+
tags['WCAG-GoodToFix-Occurrences'] = String(allIssues.items.goodToFix.totalItems);
|
|
96
|
+
tags['WCAG-NeedsReview-Occurrences'] = String(allIssues.items.needsReview.totalItems);
|
|
97
|
+
// Add number of pages scanned tag
|
|
98
|
+
tags['Pages-Scanned-Count'] = String(allIssues.totalPagesScanned);
|
|
99
|
+
}
|
|
100
|
+
else if (pagesScannedCount > 0) {
|
|
101
|
+
// Still add the pages scanned count even if we don't have allIssues
|
|
102
|
+
tags['Pages-Scanned-Count'] = String(pagesScannedCount);
|
|
103
|
+
}
|
|
104
|
+
// Send the event to Sentry
|
|
105
|
+
await Sentry.captureEvent({
|
|
106
|
+
message: 'Accessibility Scan Completed',
|
|
107
|
+
level: 'info',
|
|
108
|
+
tags: {
|
|
109
|
+
...tags,
|
|
110
|
+
event_type: 'accessibility_scan',
|
|
111
|
+
scanType: scanInfo.scanType,
|
|
112
|
+
browser: scanInfo.browser,
|
|
113
|
+
entryUrl: scanInfo.entryUrl,
|
|
114
|
+
},
|
|
115
|
+
user: {
|
|
116
|
+
...(scanInfo.email && scanInfo.name
|
|
117
|
+
? {
|
|
118
|
+
email: scanInfo.email,
|
|
119
|
+
username: scanInfo.name,
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
122
|
+
...(userData && userData.userId ? { id: userData.userId } : {}),
|
|
123
|
+
},
|
|
124
|
+
extra: {
|
|
125
|
+
additionalScanMetadata: ruleIdJson != null ? JSON.stringify(ruleIdJson) : '{}',
|
|
126
|
+
wcagBreakdown: wcagCriteriaBreakdown,
|
|
127
|
+
reportCounts: allIssues
|
|
128
|
+
? {
|
|
129
|
+
mustFix: {
|
|
130
|
+
issues: allIssues.items.mustFix.rules?.length ?? 0,
|
|
131
|
+
occurrences: allIssues.items.mustFix.totalItems ?? 0,
|
|
132
|
+
},
|
|
133
|
+
goodToFix: {
|
|
134
|
+
issues: allIssues.items.goodToFix.rules?.length ?? 0,
|
|
135
|
+
occurrences: allIssues.items.goodToFix.totalItems ?? 0,
|
|
136
|
+
},
|
|
137
|
+
needsReview: {
|
|
138
|
+
issues: allIssues.items.needsReview.rules?.length ?? 0,
|
|
139
|
+
occurrences: allIssues.items.needsReview.totalItems ?? 0,
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
: undefined,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
// Wait for events to be sent
|
|
146
|
+
await Sentry.flush(2000);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.error('Error sending WCAG breakdown to Sentry:', error);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
export default sendWcagBreakdownToSentry;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|