@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,451 @@
|
|
|
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
|
+
import type { AllIssues } from './types.js';
|
|
9
|
+
|
|
10
|
+
function* serializeObject(obj: any, depth = 0, indent = ' ') {
|
|
11
|
+
const currentIndent = indent.repeat(depth);
|
|
12
|
+
const nextIndent = indent.repeat(depth + 1);
|
|
13
|
+
|
|
14
|
+
if (obj instanceof Date) {
|
|
15
|
+
yield JSON.stringify(obj.toISOString());
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Array.isArray(obj)) {
|
|
20
|
+
yield '[\n';
|
|
21
|
+
for (let i = 0; i < obj.length; i++) {
|
|
22
|
+
if (i > 0) yield ',\n';
|
|
23
|
+
yield nextIndent;
|
|
24
|
+
yield* serializeObject(obj[i], depth + 1, indent);
|
|
25
|
+
}
|
|
26
|
+
yield `\n${currentIndent}]`;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (obj !== null && typeof obj === 'object') {
|
|
31
|
+
yield '{\n';
|
|
32
|
+
const keys = Object.keys(obj);
|
|
33
|
+
for (let i = 0; i < keys.length; i++) {
|
|
34
|
+
const key = keys[i];
|
|
35
|
+
if (i > 0) yield ',\n';
|
|
36
|
+
yield `${nextIndent}${JSON.stringify(key)}: `;
|
|
37
|
+
yield* serializeObject(obj[key], depth + 1, indent);
|
|
38
|
+
}
|
|
39
|
+
yield `\n${currentIndent}}`;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (obj === null || typeof obj === 'function' || typeof obj === 'undefined') {
|
|
44
|
+
yield 'null';
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
yield JSON.stringify(obj);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeLargeJsonToFile(obj: object, filePath: string) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const writeStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
|
54
|
+
|
|
55
|
+
writeStream.on('error', error => {
|
|
56
|
+
consoleLogger.error('Stream error:', error);
|
|
57
|
+
reject(error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
writeStream.on('finish', () => {
|
|
61
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
62
|
+
resolve(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const generator = serializeObject(obj);
|
|
66
|
+
|
|
67
|
+
function write() {
|
|
68
|
+
let next: any;
|
|
69
|
+
while (!(next = generator.next()).done) {
|
|
70
|
+
if (!writeStream.write(next.value)) {
|
|
71
|
+
writeStream.once('drain', write);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
writeStream.end();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
write();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const writeLargeScanItemsJsonToFile = async (obj: object, filePath: string) => {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const writeStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
|
|
85
|
+
const writeQueue: string[] = [];
|
|
86
|
+
let isWriting = false;
|
|
87
|
+
|
|
88
|
+
const processNextWrite = async () => {
|
|
89
|
+
if (isWriting || writeQueue.length === 0) return;
|
|
90
|
+
|
|
91
|
+
isWriting = true;
|
|
92
|
+
const data = writeQueue.shift()!;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (!writeStream.write(data)) {
|
|
96
|
+
await new Promise<void>(resolve => {
|
|
97
|
+
writeStream.once('drain', () => {
|
|
98
|
+
resolve();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
writeStream.destroy(error as Error);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isWriting = false;
|
|
108
|
+
processNextWrite();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const queueWrite = (data: string) => {
|
|
112
|
+
writeQueue.push(data);
|
|
113
|
+
processNextWrite();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
writeStream.on('error', error => {
|
|
117
|
+
consoleLogger.error(`Error writing object to JSON file: ${error}`);
|
|
118
|
+
reject(error);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
writeStream.on('finish', () => {
|
|
122
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
123
|
+
resolve(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
queueWrite('{\n');
|
|
128
|
+
const keys = Object.keys(obj);
|
|
129
|
+
|
|
130
|
+
keys.forEach((key, i) => {
|
|
131
|
+
const value = obj[key];
|
|
132
|
+
|
|
133
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
134
|
+
queueWrite(` "${key}": ${JSON.stringify(value)}`);
|
|
135
|
+
} else {
|
|
136
|
+
queueWrite(` "${key}": {\n`);
|
|
137
|
+
|
|
138
|
+
const { rules, ...otherProperties } = value;
|
|
139
|
+
|
|
140
|
+
Object.entries(otherProperties).forEach(([propKey, propValue], j) => {
|
|
141
|
+
const propValueString =
|
|
142
|
+
propValue === null ||
|
|
143
|
+
typeof propValue === 'function' ||
|
|
144
|
+
typeof propValue === 'undefined'
|
|
145
|
+
? 'null'
|
|
146
|
+
: JSON.stringify(propValue);
|
|
147
|
+
queueWrite(` "${propKey}": ${propValueString}`);
|
|
148
|
+
if (j < Object.keys(otherProperties).length - 1 || (rules && rules.length >= 0)) {
|
|
149
|
+
queueWrite(',\n');
|
|
150
|
+
} else {
|
|
151
|
+
queueWrite('\n');
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (rules && Array.isArray(rules)) {
|
|
156
|
+
queueWrite(' "rules": [\n');
|
|
157
|
+
|
|
158
|
+
rules.forEach((rule, j) => {
|
|
159
|
+
queueWrite(' {\n');
|
|
160
|
+
const { pagesAffected, ...otherRuleProperties } = rule;
|
|
161
|
+
|
|
162
|
+
Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
|
|
163
|
+
const ruleValueString =
|
|
164
|
+
ruleValue === null ||
|
|
165
|
+
typeof ruleValue === 'function' ||
|
|
166
|
+
typeof ruleValue === 'undefined'
|
|
167
|
+
? 'null'
|
|
168
|
+
: JSON.stringify(ruleValue);
|
|
169
|
+
queueWrite(` "${ruleKey}": ${ruleValueString}`);
|
|
170
|
+
if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
|
|
171
|
+
queueWrite(',\n');
|
|
172
|
+
} else {
|
|
173
|
+
queueWrite('\n');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (pagesAffected && Array.isArray(pagesAffected)) {
|
|
178
|
+
queueWrite(' "pagesAffected": [\n');
|
|
179
|
+
|
|
180
|
+
pagesAffected.forEach((page, p) => {
|
|
181
|
+
const pageJson = JSON.stringify(page, null, 2)
|
|
182
|
+
.split('\n')
|
|
183
|
+
.map(line => ` ${line}`)
|
|
184
|
+
.join('\n');
|
|
185
|
+
|
|
186
|
+
queueWrite(pageJson);
|
|
187
|
+
|
|
188
|
+
if (p < pagesAffected.length - 1) {
|
|
189
|
+
queueWrite(',\n');
|
|
190
|
+
} else {
|
|
191
|
+
queueWrite('\n');
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
queueWrite(' ]');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
queueWrite('\n }');
|
|
199
|
+
if (j < rules.length - 1) {
|
|
200
|
+
queueWrite(',\n');
|
|
201
|
+
} else {
|
|
202
|
+
queueWrite('\n');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
queueWrite(' ]');
|
|
207
|
+
}
|
|
208
|
+
queueWrite('\n }');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (i < keys.length - 1) {
|
|
212
|
+
queueWrite(',\n');
|
|
213
|
+
} else {
|
|
214
|
+
queueWrite('\n');
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
queueWrite('}\n');
|
|
219
|
+
|
|
220
|
+
const checkQueueAndEnd = () => {
|
|
221
|
+
if (writeQueue.length === 0 && !isWriting) {
|
|
222
|
+
writeStream.end();
|
|
223
|
+
} else {
|
|
224
|
+
setTimeout(checkQueueAndEnd, 100);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
checkQueueAndEnd();
|
|
229
|
+
} catch (err) {
|
|
230
|
+
writeStream.destroy(err as Error);
|
|
231
|
+
reject(err);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
async function compressJsonFileStreaming(inputPath: string, outputPath: string) {
|
|
237
|
+
const readStream = fs.createReadStream(inputPath);
|
|
238
|
+
const writeStream = fs.createWriteStream(outputPath);
|
|
239
|
+
const gzip = zlib.createGzip();
|
|
240
|
+
const base64Encode = new Base64Encode();
|
|
241
|
+
|
|
242
|
+
await pipeline(readStream, gzip, base64Encode, writeStream);
|
|
243
|
+
consoleLogger.info(`File successfully compressed and saved to ${outputPath}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const writeJsonFileAndCompressedJsonFile = async (
|
|
247
|
+
data: object,
|
|
248
|
+
storagePath: string,
|
|
249
|
+
filename: string,
|
|
250
|
+
): Promise<{ jsonFilePath: string; base64FilePath: string }> => {
|
|
251
|
+
try {
|
|
252
|
+
consoleLogger.info(`Writing JSON to ${filename}.json`);
|
|
253
|
+
const jsonFilePath = path.join(storagePath, `${filename}.json`);
|
|
254
|
+
if (filename === 'scanItems') {
|
|
255
|
+
await writeLargeScanItemsJsonToFile(data, jsonFilePath);
|
|
256
|
+
} else {
|
|
257
|
+
await writeLargeJsonToFile(data, jsonFilePath);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
consoleLogger.info(
|
|
261
|
+
`Reading ${filename}.json, gzipping and base64 encoding it into ${filename}.json.gz.b64`,
|
|
262
|
+
);
|
|
263
|
+
const base64FilePath = path.join(storagePath, `${filename}.json.gz.b64`);
|
|
264
|
+
await compressJsonFileStreaming(jsonFilePath, base64FilePath);
|
|
265
|
+
|
|
266
|
+
consoleLogger.info(`Finished compression and base64 encoding for ${filename}`);
|
|
267
|
+
return {
|
|
268
|
+
jsonFilePath,
|
|
269
|
+
base64FilePath,
|
|
270
|
+
};
|
|
271
|
+
} catch (error) {
|
|
272
|
+
consoleLogger.error(`Error compressing and encoding ${filename}`);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const writeJsonAndBase64Files = async (
|
|
278
|
+
allIssues: AllIssues,
|
|
279
|
+
storagePath: string,
|
|
280
|
+
): Promise<{
|
|
281
|
+
scanDataJsonFilePath: string;
|
|
282
|
+
scanDataBase64FilePath: string;
|
|
283
|
+
scanItemsJsonFilePath: string;
|
|
284
|
+
scanItemsBase64FilePath: string;
|
|
285
|
+
scanItemsSummaryJsonFilePath: string;
|
|
286
|
+
scanItemsSummaryBase64FilePath: string;
|
|
287
|
+
scanIssuesSummaryJsonFilePath: string;
|
|
288
|
+
scanIssuesSummaryBase64FilePath: string;
|
|
289
|
+
scanPagesDetailJsonFilePath: string;
|
|
290
|
+
scanPagesDetailBase64FilePath: string;
|
|
291
|
+
scanPagesSummaryJsonFilePath: string;
|
|
292
|
+
scanPagesSummaryBase64FilePath: string;
|
|
293
|
+
scanDataJsonFileSize: number;
|
|
294
|
+
scanItemsJsonFileSize: number;
|
|
295
|
+
}> => {
|
|
296
|
+
const { items, ...rest } = allIssues;
|
|
297
|
+
const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } =
|
|
298
|
+
await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
|
|
299
|
+
const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } =
|
|
300
|
+
await writeJsonFileAndCompressedJsonFile(
|
|
301
|
+
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...items },
|
|
302
|
+
storagePath,
|
|
303
|
+
'scanItems',
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
307
|
+
if (items[category].rules && Array.isArray(items[category].rules)) {
|
|
308
|
+
items[category].rules.forEach(rule => {
|
|
309
|
+
rule.pagesAffectedCount = Array.isArray(rule.pagesAffected) ? rule.pagesAffected.length : 0;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
items[category].rules.sort(
|
|
313
|
+
(a, b) => (b.pagesAffectedCount || 0) - (a.pagesAffectedCount || 0),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const scanIssuesSummary = {
|
|
319
|
+
mustFix: items.mustFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
320
|
+
...ruleInfo,
|
|
321
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
322
|
+
})),
|
|
323
|
+
goodToFix: items.goodToFix.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
324
|
+
...ruleInfo,
|
|
325
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
326
|
+
})),
|
|
327
|
+
needsReview: items.needsReview.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
328
|
+
...ruleInfo,
|
|
329
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
330
|
+
})),
|
|
331
|
+
passed: items.passed.rules.map(({ pagesAffected, ...ruleInfo }) => ({
|
|
332
|
+
...ruleInfo,
|
|
333
|
+
description: a11yRuleShortDescriptionMap[ruleInfo.rule] || ruleInfo.description,
|
|
334
|
+
})),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const {
|
|
338
|
+
jsonFilePath: scanIssuesSummaryJsonFilePath,
|
|
339
|
+
base64FilePath: scanIssuesSummaryBase64FilePath,
|
|
340
|
+
} = await writeJsonFileAndCompressedJsonFile(
|
|
341
|
+
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanIssuesSummary },
|
|
342
|
+
storagePath,
|
|
343
|
+
'scanIssuesSummary',
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
items.mustFix.rules.forEach(rule => {
|
|
347
|
+
rule.pagesAffected.forEach(page => {
|
|
348
|
+
page.itemsCount = page.items.length;
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
items.goodToFix.rules.forEach(rule => {
|
|
352
|
+
rule.pagesAffected.forEach(page => {
|
|
353
|
+
page.itemsCount = page.items.length;
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
items.needsReview.rules.forEach(rule => {
|
|
357
|
+
rule.pagesAffected.forEach(page => {
|
|
358
|
+
page.itemsCount = page.items.length;
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
items.passed.rules.forEach(rule => {
|
|
362
|
+
rule.pagesAffected.forEach(page => {
|
|
363
|
+
page.itemsCount = page.items.length;
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
items.mustFix.totalRuleIssues = items.mustFix.rules.length;
|
|
368
|
+
items.goodToFix.totalRuleIssues = items.goodToFix.rules.length;
|
|
369
|
+
items.needsReview.totalRuleIssues = items.needsReview.rules.length;
|
|
370
|
+
items.passed.totalRuleIssues = items.passed.rules.length;
|
|
371
|
+
|
|
372
|
+
const {
|
|
373
|
+
topTenPagesWithMostIssues,
|
|
374
|
+
wcagLinks,
|
|
375
|
+
wcagPassPercentage,
|
|
376
|
+
progressPercentage,
|
|
377
|
+
issuesPercentage,
|
|
378
|
+
totalPagesScanned,
|
|
379
|
+
totalPagesNotScanned,
|
|
380
|
+
topTenIssues,
|
|
381
|
+
} = rest;
|
|
382
|
+
|
|
383
|
+
const summaryItems = {
|
|
384
|
+
mustFix: {
|
|
385
|
+
totalItems: items.mustFix?.totalItems || 0,
|
|
386
|
+
totalRuleIssues: items.mustFix?.totalRuleIssues || 0,
|
|
387
|
+
},
|
|
388
|
+
goodToFix: {
|
|
389
|
+
totalItems: items.goodToFix?.totalItems || 0,
|
|
390
|
+
totalRuleIssues: items.goodToFix?.totalRuleIssues || 0,
|
|
391
|
+
},
|
|
392
|
+
needsReview: {
|
|
393
|
+
totalItems: items.needsReview?.totalItems || 0,
|
|
394
|
+
totalRuleIssues: items.needsReview?.totalRuleIssues || 0,
|
|
395
|
+
},
|
|
396
|
+
topTenPagesWithMostIssues,
|
|
397
|
+
wcagLinks,
|
|
398
|
+
wcagPassPercentage,
|
|
399
|
+
progressPercentage,
|
|
400
|
+
issuesPercentage,
|
|
401
|
+
totalPagesScanned,
|
|
402
|
+
totalPagesNotScanned,
|
|
403
|
+
topTenIssues,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const {
|
|
407
|
+
jsonFilePath: scanItemsSummaryJsonFilePath,
|
|
408
|
+
base64FilePath: scanItemsSummaryBase64FilePath,
|
|
409
|
+
} = await writeJsonFileAndCompressedJsonFile(
|
|
410
|
+
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItems },
|
|
411
|
+
storagePath,
|
|
412
|
+
'scanItemsSummary',
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const {
|
|
416
|
+
jsonFilePath: scanPagesDetailJsonFilePath,
|
|
417
|
+
base64FilePath: scanPagesDetailBase64FilePath,
|
|
418
|
+
} = await writeJsonFileAndCompressedJsonFile(
|
|
419
|
+
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesDetail },
|
|
420
|
+
storagePath,
|
|
421
|
+
'scanPagesDetail',
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const {
|
|
425
|
+
jsonFilePath: scanPagesSummaryJsonFilePath,
|
|
426
|
+
base64FilePath: scanPagesSummaryBase64FilePath,
|
|
427
|
+
} = await writeJsonFileAndCompressedJsonFile(
|
|
428
|
+
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesSummary },
|
|
429
|
+
storagePath,
|
|
430
|
+
'scanPagesSummary',
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
scanDataJsonFilePath,
|
|
435
|
+
scanDataBase64FilePath,
|
|
436
|
+
scanItemsJsonFilePath,
|
|
437
|
+
scanItemsBase64FilePath,
|
|
438
|
+
scanItemsSummaryJsonFilePath,
|
|
439
|
+
scanItemsSummaryBase64FilePath,
|
|
440
|
+
scanIssuesSummaryJsonFilePath,
|
|
441
|
+
scanIssuesSummaryBase64FilePath,
|
|
442
|
+
scanPagesDetailJsonFilePath,
|
|
443
|
+
scanPagesDetailBase64FilePath,
|
|
444
|
+
scanPagesSummaryJsonFilePath,
|
|
445
|
+
scanPagesSummaryBase64FilePath,
|
|
446
|
+
scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
|
|
447
|
+
scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
export { compressJsonFileStreaming, writeJsonAndBase64Files, writeJsonFileAndCompressedJsonFile };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { AllIssues } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build allIssues.scanPagesDetail and allIssues.scanPagesSummary
|
|
5
|
+
* by analyzing pagesScanned (including mustFix/goodToFix/etc.).
|
|
6
|
+
*/
|
|
7
|
+
export default function populateScanPagesDetail(allIssues: AllIssues): void {
|
|
8
|
+
const allScannedPages = Array.isArray(allIssues.pagesScanned) ? allIssues.pagesScanned : [];
|
|
9
|
+
|
|
10
|
+
const mustFixCategory = 'mustFix';
|
|
11
|
+
const goodToFixCategory = 'goodToFix';
|
|
12
|
+
const needsReviewCategory = 'needsReview';
|
|
13
|
+
const passedCategory = 'passed';
|
|
14
|
+
|
|
15
|
+
type RuleData = {
|
|
16
|
+
ruleId: string;
|
|
17
|
+
wcagConformance: string[];
|
|
18
|
+
occurrencesMustFix: number;
|
|
19
|
+
occurrencesGoodToFix: number;
|
|
20
|
+
occurrencesNeedsReview: number;
|
|
21
|
+
occurrencesPassed: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type PageData = {
|
|
25
|
+
pageTitle: string;
|
|
26
|
+
url: string;
|
|
27
|
+
totalOccurrencesFailedIncludingNeedsReview: number;
|
|
28
|
+
totalOccurrencesFailedExcludingNeedsReview: number;
|
|
29
|
+
totalOccurrencesNeedsReview: number;
|
|
30
|
+
totalOccurrencesPassed: number;
|
|
31
|
+
typesOfIssues: Record<string, RuleData>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const pagesMap: Record<string, PageData> = {};
|
|
35
|
+
|
|
36
|
+
Object.entries(allIssues.items).forEach(([categoryName, categoryData]) => {
|
|
37
|
+
if (!categoryData?.rules) return;
|
|
38
|
+
|
|
39
|
+
categoryData.rules.forEach(rule => {
|
|
40
|
+
const { rule: ruleId, conformance = [] } = rule;
|
|
41
|
+
|
|
42
|
+
rule.pagesAffected.forEach(p => {
|
|
43
|
+
const { url, pageTitle, items = [] } = p;
|
|
44
|
+
const itemsCount = items.length;
|
|
45
|
+
|
|
46
|
+
if (!pagesMap[url]) {
|
|
47
|
+
pagesMap[url] = {
|
|
48
|
+
pageTitle,
|
|
49
|
+
url,
|
|
50
|
+
totalOccurrencesFailedIncludingNeedsReview: 0,
|
|
51
|
+
totalOccurrencesFailedExcludingNeedsReview: 0,
|
|
52
|
+
totalOccurrencesNeedsReview: 0,
|
|
53
|
+
totalOccurrencesPassed: 0,
|
|
54
|
+
typesOfIssues: {},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!pagesMap[url].typesOfIssues[ruleId]) {
|
|
59
|
+
pagesMap[url].typesOfIssues[ruleId] = {
|
|
60
|
+
ruleId,
|
|
61
|
+
wcagConformance: conformance,
|
|
62
|
+
occurrencesMustFix: 0,
|
|
63
|
+
occurrencesGoodToFix: 0,
|
|
64
|
+
occurrencesNeedsReview: 0,
|
|
65
|
+
occurrencesPassed: 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (categoryName === mustFixCategory) {
|
|
70
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesMustFix += itemsCount;
|
|
71
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
72
|
+
pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
|
|
73
|
+
} else if (categoryName === goodToFixCategory) {
|
|
74
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesGoodToFix += itemsCount;
|
|
75
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
76
|
+
pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
|
|
77
|
+
} else if (categoryName === needsReviewCategory) {
|
|
78
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesNeedsReview += itemsCount;
|
|
79
|
+
pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
|
|
80
|
+
pagesMap[url].totalOccurrencesNeedsReview += itemsCount;
|
|
81
|
+
} else if (categoryName === passedCategory) {
|
|
82
|
+
pagesMap[url].typesOfIssues[ruleId].occurrencesPassed += itemsCount;
|
|
83
|
+
pagesMap[url].totalOccurrencesPassed += itemsCount;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const pagesInMap = Object.values(pagesMap);
|
|
90
|
+
const pagesInMapUrls = new Set(Object.keys(pagesMap));
|
|
91
|
+
|
|
92
|
+
const pagesAllPassed = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview === 0);
|
|
93
|
+
|
|
94
|
+
const pagesNoEntries = allScannedPages
|
|
95
|
+
.filter(sp => !pagesInMapUrls.has(sp.url))
|
|
96
|
+
.map(sp => ({
|
|
97
|
+
pageTitle: sp.pageTitle,
|
|
98
|
+
url: sp.url,
|
|
99
|
+
totalOccurrencesFailedIncludingNeedsReview: 0,
|
|
100
|
+
totalOccurrencesFailedExcludingNeedsReview: 0,
|
|
101
|
+
totalOccurrencesNeedsReview: 0,
|
|
102
|
+
totalOccurrencesPassed: 0,
|
|
103
|
+
typesOfIssues: {},
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const pagesNotAffectedRaw = [...pagesAllPassed, ...pagesNoEntries];
|
|
107
|
+
const pagesAffectedRaw = pagesInMap.filter(p => p.totalOccurrencesFailedIncludingNeedsReview > 0);
|
|
108
|
+
|
|
109
|
+
function transformPageData(page: PageData) {
|
|
110
|
+
const typesOfIssuesArray = Object.values(page.typesOfIssues);
|
|
111
|
+
const mustFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesMustFix, 0);
|
|
112
|
+
const goodToFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesGoodToFix, 0);
|
|
113
|
+
const needsReviewSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesNeedsReview, 0);
|
|
114
|
+
|
|
115
|
+
const categoriesPresent: string[] = [];
|
|
116
|
+
if (mustFixSum > 0) categoriesPresent.push('mustFix');
|
|
117
|
+
if (goodToFixSum > 0) categoriesPresent.push('goodToFix');
|
|
118
|
+
if (needsReviewSum > 0) categoriesPresent.push('needsReview');
|
|
119
|
+
|
|
120
|
+
const failedRuleIds = new Set<string>();
|
|
121
|
+
typesOfIssuesArray.forEach(r => {
|
|
122
|
+
if (
|
|
123
|
+
(r.occurrencesMustFix || 0) > 0 ||
|
|
124
|
+
(r.occurrencesGoodToFix || 0) > 0 ||
|
|
125
|
+
(r.occurrencesNeedsReview || 0) > 0
|
|
126
|
+
) {
|
|
127
|
+
failedRuleIds.add(r.ruleId);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
const failedRuleCount = failedRuleIds.size;
|
|
131
|
+
|
|
132
|
+
const typesOfIssuesExcludingNeedsReviewCount = typesOfIssuesArray.filter(
|
|
133
|
+
r => (r.occurrencesMustFix || 0) + (r.occurrencesGoodToFix || 0) > 0,
|
|
134
|
+
).length;
|
|
135
|
+
|
|
136
|
+
const typesOfIssuesExclusiveToNeedsReviewCount = typesOfIssuesArray.filter(
|
|
137
|
+
r =>
|
|
138
|
+
(r.occurrencesNeedsReview || 0) > 0 &&
|
|
139
|
+
(r.occurrencesMustFix || 0) === 0 &&
|
|
140
|
+
(r.occurrencesGoodToFix || 0) === 0,
|
|
141
|
+
).length;
|
|
142
|
+
|
|
143
|
+
const allConformance = typesOfIssuesArray.reduce((acc, curr) => {
|
|
144
|
+
const nonPassedCount =
|
|
145
|
+
(curr.occurrencesMustFix || 0) +
|
|
146
|
+
(curr.occurrencesGoodToFix || 0) +
|
|
147
|
+
(curr.occurrencesNeedsReview || 0);
|
|
148
|
+
|
|
149
|
+
if (nonPassedCount > 0) {
|
|
150
|
+
return acc.concat(curr.wcagConformance || []);
|
|
151
|
+
}
|
|
152
|
+
return acc;
|
|
153
|
+
}, [] as string[]);
|
|
154
|
+
const conformance = Array.from(new Set(allConformance));
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
pageTitle: page.pageTitle,
|
|
158
|
+
url: page.url,
|
|
159
|
+
totalOccurrencesFailedIncludingNeedsReview: page.totalOccurrencesFailedIncludingNeedsReview,
|
|
160
|
+
totalOccurrencesFailedExcludingNeedsReview: page.totalOccurrencesFailedExcludingNeedsReview,
|
|
161
|
+
totalOccurrencesMustFix: mustFixSum,
|
|
162
|
+
totalOccurrencesGoodToFix: goodToFixSum,
|
|
163
|
+
totalOccurrencesNeedsReview: needsReviewSum,
|
|
164
|
+
totalOccurrencesPassed: page.totalOccurrencesPassed,
|
|
165
|
+
typesOfIssuesExclusiveToNeedsReviewCount,
|
|
166
|
+
typesOfIssuesCount: failedRuleCount,
|
|
167
|
+
typesOfIssuesExcludingNeedsReviewCount,
|
|
168
|
+
categoriesPresent,
|
|
169
|
+
conformance,
|
|
170
|
+
typesOfIssues: typesOfIssuesArray,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pagesAffected = pagesAffectedRaw.map(transformPageData);
|
|
175
|
+
const pagesNotAffected = pagesNotAffectedRaw.map(transformPageData);
|
|
176
|
+
pagesAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
|
|
177
|
+
pagesNotAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
|
|
178
|
+
|
|
179
|
+
const scannedPagesCount = pagesAffected.length + pagesNotAffected.length;
|
|
180
|
+
const pagesNotScannedCount = Array.isArray(allIssues.pagesNotScanned)
|
|
181
|
+
? allIssues.pagesNotScanned.length
|
|
182
|
+
: 0;
|
|
183
|
+
|
|
184
|
+
allIssues.scanPagesDetail = {
|
|
185
|
+
pagesAffected,
|
|
186
|
+
pagesNotAffected,
|
|
187
|
+
scannedPagesCount,
|
|
188
|
+
pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
|
|
189
|
+
pagesNotScannedCount,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
function stripTypesOfIssues(page: ReturnType<typeof transformPageData>) {
|
|
193
|
+
const { typesOfIssues, ...rest } = page;
|
|
194
|
+
return rest;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const summaryPagesAffected = pagesAffected.map(stripTypesOfIssues);
|
|
198
|
+
const summaryPagesNotAffected = pagesNotAffected.map(stripTypesOfIssues);
|
|
199
|
+
|
|
200
|
+
allIssues.scanPagesSummary = {
|
|
201
|
+
pagesAffected: summaryPagesAffected,
|
|
202
|
+
pagesNotAffected: summaryPagesNotAffected,
|
|
203
|
+
scannedPagesCount,
|
|
204
|
+
pagesNotScanned: Array.isArray(allIssues.pagesNotScanned) ? allIssues.pagesNotScanned : [],
|
|
205
|
+
pagesNotScannedCount,
|
|
206
|
+
};
|
|
207
|
+
}
|