@govtechsg/oobee 0.10.89 → 0.10.91
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/.github/workflows/bump-package-version.yml +10 -1
- package/.github/workflows/docker-push-ghcr.yml +5 -1
- package/.github/workflows/publish.yml +8 -3
- package/README.md +2 -0
- package/bf04540e-0894-4d00-98ec-c1be74c6f199.txt +0 -0
- package/dist/mergeAxeResults/itemsStore.js +51 -0
- package/dist/mergeAxeResults/jsonArtifacts.js +122 -132
- package/dist/mergeAxeResults/scanPages.js +2 -2
- package/dist/mergeAxeResults/writeCsv.js +115 -96
- package/dist/mergeAxeResults.js +94 -41
- package/oobee-client-scanner.js +6914 -6754
- package/package.json +1 -1
- package/src/mergeAxeResults/itemsStore.ts +73 -0
- package/src/mergeAxeResults/jsonArtifacts.ts +135 -138
- package/src/mergeAxeResults/scanPages.ts +2 -2
- package/src/mergeAxeResults/writeCsv.ts +129 -100
- package/src/mergeAxeResults.ts +102 -49
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
import { createWriteStream } from 'fs';
|
|
2
|
-
import { AsyncParser } from '@json2csv/node';
|
|
3
2
|
import { a11yRuleShortDescriptionMap } from '../constants/constants.js';
|
|
4
|
-
|
|
3
|
+
function escapeCsvField(value) {
|
|
4
|
+
if (value == null)
|
|
5
|
+
return '';
|
|
6
|
+
const str = String(value);
|
|
7
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
8
|
+
}
|
|
9
|
+
const writeCsv = async (allIssues, storagePath, itemsStore) => {
|
|
5
10
|
const csvOutput = createWriteStream(`${storagePath}/report.csv`, { encoding: 'utf8' });
|
|
6
11
|
const formatPageViolation = (pageNum) => {
|
|
7
12
|
if (pageNum < 0)
|
|
8
13
|
return 'Document';
|
|
9
14
|
return `Page ${pageNum}`;
|
|
10
15
|
};
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
const fields = [
|
|
17
|
+
'customFlowLabel',
|
|
18
|
+
'deviceChosen',
|
|
19
|
+
'scanCompletedAt',
|
|
20
|
+
'severity',
|
|
21
|
+
'issueId',
|
|
22
|
+
'issueDescription',
|
|
23
|
+
'wcagConformance',
|
|
24
|
+
'url',
|
|
25
|
+
'pageTitle',
|
|
26
|
+
'context',
|
|
27
|
+
'howToFix',
|
|
28
|
+
'axeImpact',
|
|
29
|
+
'xpath',
|
|
30
|
+
'learnMore',
|
|
31
|
+
];
|
|
32
|
+
csvOutput.write(fields.map(escapeCsvField).join(',') + '\n');
|
|
13
33
|
const getRulesByCategory = (issues) => {
|
|
14
34
|
return Object.entries(issues.items)
|
|
15
35
|
.filter(([category]) => category !== 'passed')
|
|
@@ -21,105 +41,104 @@ const writeCsv = async (allIssues, storagePath) => {
|
|
|
21
41
|
return prev;
|
|
22
42
|
}, [])
|
|
23
43
|
.sort((a, b) => {
|
|
24
|
-
// sort rules according to severity, then ruleId
|
|
25
44
|
const compareCategory = -a[0].localeCompare(b[0]);
|
|
26
45
|
return compareCategory === 0 ? a[1].rule.localeCompare(b[1].rule) : compareCategory;
|
|
27
46
|
});
|
|
28
47
|
};
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const results = [];
|
|
48
|
+
const rulesByCategory = getRulesByCategory(allIssues);
|
|
49
|
+
for (const [severity, rule] of rulesByCategory) {
|
|
32
50
|
const { rule: issueId, description: issueDescription, axeImpact, conformance, pagesAffected, helpUrl: learnMore, } = rule;
|
|
33
|
-
// format clauses as a string
|
|
34
51
|
const wcagConformance = conformance.join(',');
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
fields: [
|
|
69
|
-
'customFlowLabel',
|
|
70
|
-
'deviceChosen',
|
|
71
|
-
'scanCompletedAt',
|
|
72
|
-
'severity',
|
|
73
|
-
'issueId',
|
|
74
|
-
'issueDescription',
|
|
75
|
-
'wcagConformance',
|
|
76
|
-
'url',
|
|
77
|
-
'pageTitle',
|
|
78
|
-
'context',
|
|
79
|
-
'howToFix',
|
|
80
|
-
'axeImpact',
|
|
81
|
-
'xpath',
|
|
82
|
-
'learnMore',
|
|
83
|
-
],
|
|
84
|
-
includeEmptyRows: true,
|
|
85
|
-
};
|
|
86
|
-
// Create the parse stream (it's asynchronous)
|
|
87
|
-
const parser = new AsyncParser(opts);
|
|
88
|
-
const parseStream = parser.parse(allIssues);
|
|
89
|
-
// Pipe JSON2CSV output into the file, but don't end automatically
|
|
90
|
-
parseStream.pipe(csvOutput, { end: false });
|
|
91
|
-
// Once JSON2CSV is done writing all normal rows, append any "pagesNotScanned"
|
|
92
|
-
parseStream.on('end', () => {
|
|
93
|
-
if (allIssues.pagesNotScanned && allIssues.pagesNotScanned.length > 0) {
|
|
94
|
-
csvOutput.write('\n');
|
|
95
|
-
allIssues.pagesNotScanned.forEach(page => {
|
|
96
|
-
const skippedPage = {
|
|
97
|
-
customFlowLabel: allIssues.customFlowLabel || '',
|
|
98
|
-
deviceChosen: allIssues.deviceChosen || '',
|
|
99
|
-
scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
100
|
-
severity: 'error',
|
|
101
|
-
issueId: 'error-pages-skipped',
|
|
102
|
-
issueDescription: page.metadata
|
|
103
|
-
? page.metadata
|
|
104
|
-
: 'An unknown error caused the page to be skipped',
|
|
105
|
-
wcagConformance: '',
|
|
106
|
-
url: page.url || page || '',
|
|
107
|
-
pageTitle: 'Error',
|
|
108
|
-
context: '',
|
|
109
|
-
howToFix: '',
|
|
110
|
-
axeImpact: '',
|
|
111
|
-
xpath: '',
|
|
112
|
-
learnMore: '',
|
|
113
|
-
};
|
|
114
|
-
csvOutput.write(`${Object.values(skippedPage).join(',')}\n`);
|
|
115
|
-
});
|
|
52
|
+
if (itemsStore) {
|
|
53
|
+
const itemsMap = await itemsStore.readRuleItemsMap(severity, issueId);
|
|
54
|
+
const sortedPages = [...pagesAffected].sort((a, b) => (a.url || '').localeCompare(b.url || ''));
|
|
55
|
+
for (const affectedPage of sortedPages) {
|
|
56
|
+
const key = affectedPage.pageIndex != null ? String(affectedPage.pageIndex) : affectedPage.url;
|
|
57
|
+
const entry = itemsMap.get(key);
|
|
58
|
+
if (!entry)
|
|
59
|
+
continue;
|
|
60
|
+
for (const item of entry.items) {
|
|
61
|
+
const { html, message, xpath } = item;
|
|
62
|
+
const page = item.page;
|
|
63
|
+
const howToFix = (message || '').replace(/(\r\n|\n|\r)/g, '\\n');
|
|
64
|
+
const violation = html || formatPageViolation(page);
|
|
65
|
+
const context = violation.replace(/(\r\n|\n|\r)/g, '');
|
|
66
|
+
const row = [
|
|
67
|
+
allIssues.customFlowLabel || '',
|
|
68
|
+
allIssues.deviceChosen || '',
|
|
69
|
+
allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
70
|
+
severity || '',
|
|
71
|
+
issueId || '',
|
|
72
|
+
a11yRuleShortDescriptionMap[issueId] || issueDescription || '',
|
|
73
|
+
wcagConformance || '',
|
|
74
|
+
affectedPage.url || '',
|
|
75
|
+
affectedPage.pageTitle || 'No page title',
|
|
76
|
+
context || '',
|
|
77
|
+
howToFix || '',
|
|
78
|
+
axeImpact || '',
|
|
79
|
+
xpath || '',
|
|
80
|
+
learnMore || '',
|
|
81
|
+
].map(escapeCsvField);
|
|
82
|
+
csvOutput.write(row.join(',') + '\n');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
116
85
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
86
|
+
else {
|
|
87
|
+
const sortedPages = [...pagesAffected].sort((a, b) => (a.url || '').localeCompare(b.url || ''));
|
|
88
|
+
for (const affectedPage of sortedPages) {
|
|
89
|
+
const items = affectedPage.items || [];
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
const { html, message, xpath } = item;
|
|
92
|
+
const page = item.page;
|
|
93
|
+
const howToFix = (message || '').replace(/(\r\n|\n|\r)/g, '\\n');
|
|
94
|
+
const violation = html || formatPageViolation(page);
|
|
95
|
+
const context = violation.replace(/(\r\n|\n|\r)/g, '');
|
|
96
|
+
const row = [
|
|
97
|
+
allIssues.customFlowLabel || '',
|
|
98
|
+
allIssues.deviceChosen || '',
|
|
99
|
+
allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
100
|
+
severity || '',
|
|
101
|
+
issueId || '',
|
|
102
|
+
a11yRuleShortDescriptionMap[issueId] || issueDescription || '',
|
|
103
|
+
wcagConformance || '',
|
|
104
|
+
affectedPage.url || '',
|
|
105
|
+
affectedPage.pageTitle || 'No page title',
|
|
106
|
+
context || '',
|
|
107
|
+
howToFix || '',
|
|
108
|
+
axeImpact || '',
|
|
109
|
+
xpath || '',
|
|
110
|
+
learnMore || '',
|
|
111
|
+
].map(escapeCsvField);
|
|
112
|
+
csvOutput.write(row.join(',') + '\n');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (allIssues.pagesNotScanned && allIssues.pagesNotScanned.length > 0) {
|
|
118
|
+
allIssues.pagesNotScanned.forEach(page => {
|
|
119
|
+
const row = [
|
|
120
|
+
allIssues.customFlowLabel || '',
|
|
121
|
+
allIssues.deviceChosen || '',
|
|
122
|
+
allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
123
|
+
'error',
|
|
124
|
+
'error-pages-skipped',
|
|
125
|
+
page.metadata ? page.metadata : 'An unknown error caused the page to be skipped',
|
|
126
|
+
'',
|
|
127
|
+
page.url || page || '',
|
|
128
|
+
'Error',
|
|
129
|
+
'',
|
|
130
|
+
'',
|
|
131
|
+
'',
|
|
132
|
+
'',
|
|
133
|
+
'',
|
|
134
|
+
].map(escapeCsvField);
|
|
135
|
+
csvOutput.write(row.join(',') + '\n');
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
csvOutput.end();
|
|
139
|
+
await new Promise((resolve, reject) => {
|
|
140
|
+
csvOutput.on('finish', resolve);
|
|
141
|
+
csvOutput.on('error', reject);
|
|
123
142
|
});
|
|
124
143
|
};
|
|
125
144
|
export default writeCsv;
|
package/dist/mergeAxeResults.js
CHANGED
|
@@ -12,6 +12,7 @@ import { consoleLogger } from './logs.js';
|
|
|
12
12
|
import itemTypeDescription from './constants/itemTypeDescription.js';
|
|
13
13
|
import { oobeeAiHtmlETL, oobeeAiRules } from './constants/oobeeAi.js';
|
|
14
14
|
import { buildHtmlGroups, convertItemsToReferences } from './mergeAxeResults/itemReferences.js';
|
|
15
|
+
import { ItemsStore } from './mergeAxeResults/itemsStore.js';
|
|
15
16
|
import { compressJsonFileStreaming, writeJsonAndBase64Files, writeJsonFileAndCompressedJsonFile, } from './mergeAxeResults/jsonArtifacts.js';
|
|
16
17
|
import writeCsv from './mergeAxeResults/writeCsv.js';
|
|
17
18
|
import writeScanDetailsCsv from './mergeAxeResults/writeScanDetailsCsv.js';
|
|
@@ -306,7 +307,7 @@ const writeSummaryPdf = async (storagePath, pagesScanned, filename = 'summary',
|
|
|
306
307
|
};
|
|
307
308
|
// Tracking WCAG occurrences
|
|
308
309
|
const wcagOccurrencesMap = new Map();
|
|
309
|
-
const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
310
|
+
const pushResults = async (pageResults, allIssues, isCustomFlow, itemsStore) => {
|
|
310
311
|
const { url, pageTitle, filePath } = pageResults;
|
|
311
312
|
const totalIssuesInPage = new Set();
|
|
312
313
|
Object.keys(pageResults.mustFix?.rules ?? {}).forEach(k => totalIssuesInPage.add(k));
|
|
@@ -318,13 +319,13 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
318
319
|
totalIssues: totalIssuesInPage.size,
|
|
319
320
|
totalOccurrences: 0,
|
|
320
321
|
});
|
|
321
|
-
['mustFix', 'goodToFix', 'needsReview', 'passed']
|
|
322
|
+
for (const category of ['mustFix', 'goodToFix', 'needsReview', 'passed']) {
|
|
322
323
|
if (!pageResults[category])
|
|
323
|
-
|
|
324
|
+
continue;
|
|
324
325
|
const { totalItems, rules } = pageResults[category];
|
|
325
326
|
const currCategoryFromAllIssues = allIssues.items[category];
|
|
326
327
|
currCategoryFromAllIssues.totalItems += totalItems;
|
|
327
|
-
Object.keys(rules)
|
|
328
|
+
for (const rule of Object.keys(rules)) {
|
|
328
329
|
const { description, axeImpact, helpUrl, conformance, totalItems: count, items, } = rules[rule];
|
|
329
330
|
if (!(rule in currCategoryFromAllIssues.rules)) {
|
|
330
331
|
currCategoryFromAllIssues.rules[rule] = {
|
|
@@ -333,7 +334,6 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
333
334
|
helpUrl,
|
|
334
335
|
conformance,
|
|
335
336
|
totalItems: 0,
|
|
336
|
-
// numberOfPagesAffectedAfterRedirects: 0,
|
|
337
337
|
pagesAffected: {},
|
|
338
338
|
};
|
|
339
339
|
}
|
|
@@ -344,15 +344,12 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
344
344
|
if (!allIssues.wcagViolations.includes(c)) {
|
|
345
345
|
allIssues.wcagViolations.push(c);
|
|
346
346
|
}
|
|
347
|
-
// Track WCAG criteria occurrences for Sentry
|
|
348
347
|
const currentCount = wcagOccurrencesMap.get(c) || 0;
|
|
349
348
|
wcagOccurrencesMap.set(c, currentCount + count);
|
|
350
349
|
});
|
|
351
350
|
}
|
|
352
351
|
const currRuleFromAllIssues = currCategoryFromAllIssues.rules[rule];
|
|
353
352
|
currRuleFromAllIssues.totalItems += count;
|
|
354
|
-
// Build htmlGroups for pre-computed Group by HTML Element
|
|
355
|
-
buildHtmlGroups(currRuleFromAllIssues, items, url);
|
|
356
353
|
if (isCustomFlow) {
|
|
357
354
|
const { pageIndex, pageImagePath, metadata } = pageResults;
|
|
358
355
|
currRuleFromAllIssues.pagesAffected[pageIndex] = {
|
|
@@ -360,18 +357,32 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
360
357
|
pageTitle,
|
|
361
358
|
pageImagePath,
|
|
362
359
|
metadata,
|
|
363
|
-
|
|
360
|
+
itemsCount: items.length,
|
|
364
361
|
};
|
|
362
|
+
await itemsStore.appendPageItems(category, rule, {
|
|
363
|
+
url,
|
|
364
|
+
pageTitle,
|
|
365
|
+
items,
|
|
366
|
+
pageIndex,
|
|
367
|
+
pageImagePath,
|
|
368
|
+
metadata,
|
|
369
|
+
});
|
|
365
370
|
}
|
|
366
371
|
else if (!(url in currRuleFromAllIssues.pagesAffected)) {
|
|
367
372
|
currRuleFromAllIssues.pagesAffected[url] = {
|
|
368
373
|
pageTitle,
|
|
369
|
-
|
|
374
|
+
itemsCount: items.length,
|
|
370
375
|
...(filePath && { filePath }),
|
|
371
376
|
};
|
|
377
|
+
await itemsStore.appendPageItems(category, rule, {
|
|
378
|
+
url,
|
|
379
|
+
pageTitle,
|
|
380
|
+
items,
|
|
381
|
+
...(filePath && { filePath }),
|
|
382
|
+
});
|
|
372
383
|
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
375
386
|
};
|
|
376
387
|
const getTopTenIssues = allIssues => {
|
|
377
388
|
const categories = ['mustFix', 'goodToFix'];
|
|
@@ -422,23 +433,20 @@ const flattenAndSortResults = (allIssues, isCustomFlow) => {
|
|
|
422
433
|
.map(pageEntry => {
|
|
423
434
|
if (isCustomFlow) {
|
|
424
435
|
const [pageIndex, pageInfo] = pageEntry;
|
|
425
|
-
// Only update the occurrences map if not passed.
|
|
426
436
|
if (category !== 'passed') {
|
|
427
|
-
urlOccurrencesMap.set(pageInfo.url, (urlOccurrencesMap.get(pageInfo.url) || 0) + pageInfo.
|
|
437
|
+
urlOccurrencesMap.set(pageInfo.url, (urlOccurrencesMap.get(pageInfo.url) || 0) + (pageInfo.itemsCount || 0));
|
|
428
438
|
}
|
|
429
439
|
return { pageIndex, ...pageInfo };
|
|
430
440
|
}
|
|
431
441
|
const [url, pageInfo] = pageEntry;
|
|
432
442
|
if (category !== 'passed') {
|
|
433
|
-
urlOccurrencesMap.set(url, (urlOccurrencesMap.get(url) || 0) + pageInfo.
|
|
443
|
+
urlOccurrencesMap.set(url, (urlOccurrencesMap.get(url) || 0) + (pageInfo.itemsCount || 0));
|
|
434
444
|
}
|
|
435
445
|
return { url, ...pageInfo };
|
|
436
446
|
})
|
|
437
|
-
|
|
438
|
-
.sort((page1, page2) => page2.items.length - page1.items.length);
|
|
447
|
+
.sort((page1, page2) => (page2.itemsCount || 0) - (page1.itemsCount || 0));
|
|
439
448
|
return { rule, ...ruleInfo };
|
|
440
449
|
})
|
|
441
|
-
// Sort the rules by totalItems (descending)
|
|
442
450
|
.sort((rule1, rule2) => rule2.totalItems - rule1.totalItems);
|
|
443
451
|
});
|
|
444
452
|
// Sort top pages (assumes topFiveMostIssues is already populated)
|
|
@@ -478,18 +486,22 @@ const extractRuleAiData = (ruleId, totalItems, items, callback) => {
|
|
|
478
486
|
};
|
|
479
487
|
};
|
|
480
488
|
// This is for telemetry purposes called within mergeAxeResults.ts
|
|
481
|
-
export const createRuleIdJson = allIssues => {
|
|
489
|
+
export const createRuleIdJson = async (allIssues, itemsStore) => {
|
|
482
490
|
const compiledRuleJson = {};
|
|
483
|
-
['mustFix', 'goodToFix', 'needsReview']
|
|
484
|
-
allIssues.items[category].rules
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
491
|
+
for (const category of ['mustFix', 'goodToFix', 'needsReview']) {
|
|
492
|
+
for (const rule of allIssues.items[category].rules) {
|
|
493
|
+
let allItems = [];
|
|
494
|
+
if (itemsStore) {
|
|
495
|
+
for await (const entry of itemsStore.readRuleItems(category, rule.rule)) {
|
|
496
|
+
allItems.push(...entry.items);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
allItems = rule.pagesAffected.flatMap(page => page.items || []);
|
|
501
|
+
}
|
|
502
|
+
compiledRuleJson[rule.rule] = extractRuleAiData(rule.rule, rule.totalItems, allItems);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
493
505
|
return compiledRuleJson;
|
|
494
506
|
};
|
|
495
507
|
// This is for telemetry purposes called from npmIndex (scanPage and scanHTML) where report is not generated
|
|
@@ -619,12 +631,18 @@ generateJsonFiles = false) => {
|
|
|
619
631
|
},
|
|
620
632
|
};
|
|
621
633
|
const allFiles = await extractFileNames(intermediateDatasetsPath);
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
634
|
+
const itemsStore = new ItemsStore(storagePath);
|
|
635
|
+
for (const file of allFiles) {
|
|
636
|
+
try {
|
|
637
|
+
const pageResults = await parseContentToJson(`${intermediateDatasetsPath}/${file}`);
|
|
638
|
+
if (pageResults) {
|
|
639
|
+
await pushResults(pageResults, allIssues, isCustomFlow, itemsStore);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch (flattenIssuesError) {
|
|
643
|
+
consoleLogger.error(`[generateArtifacts] Error processing ${file}: ${flattenIssuesError?.stack || flattenIssuesError}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
628
646
|
flattenAndSortResults(allIssues, isCustomFlow);
|
|
629
647
|
const labelKey = scanType.toLowerCase() === 'custom' ? 'CustomFlowLabel' : 'Label';
|
|
630
648
|
const labelValue = allIssues.customFlowLabel || 'N/A';
|
|
@@ -654,6 +672,14 @@ generateJsonFiles = false) => {
|
|
|
654
672
|
createScreenshotsFolder(randomToken);
|
|
655
673
|
}
|
|
656
674
|
populateScanPagesDetail(allIssues);
|
|
675
|
+
// Build htmlGroups in a second pass from disk-backed items
|
|
676
|
+
for (const category of ['mustFix', 'goodToFix', 'needsReview', 'passed']) {
|
|
677
|
+
for (const rule of allIssues.items[category].rules) {
|
|
678
|
+
for await (const entry of itemsStore.readRuleItems(category, rule.rule)) {
|
|
679
|
+
buildHtmlGroups(rule, entry.items, entry.url);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
657
683
|
allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
|
|
658
684
|
allIssues.progressPercentage = getProgressPercentage(allIssues.scanPagesDetail, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
|
|
659
685
|
allIssues.issuesPercentage = await getIssuesPercentage(allIssues.scanPagesDetail, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa, allIssues.advancedScanOptionsSummaryItems.disableOobee);
|
|
@@ -703,9 +729,9 @@ generateJsonFiles = false) => {
|
|
|
703
729
|
rest.moderate = axeImpactCount.moderate;
|
|
704
730
|
rest.minor = axeImpactCount.minor;
|
|
705
731
|
}
|
|
706
|
-
await writeCsv(allIssues, storagePath);
|
|
732
|
+
await writeCsv(allIssues, storagePath, itemsStore);
|
|
707
733
|
await writeSitemap(pagesScanned, storagePath);
|
|
708
|
-
const { scanDataJsonFilePath, scanDataBase64FilePath, scanItemsJsonFilePath, scanItemsBase64FilePath, scanItemsSummaryJsonFilePath, scanItemsSummaryBase64FilePath, scanIssuesSummaryJsonFilePath, scanIssuesSummaryBase64FilePath, scanPagesDetailJsonFilePath, scanPagesDetailBase64FilePath, scanPagesSummaryJsonFilePath, scanPagesSummaryBase64FilePath, scanDataJsonFileSize, scanItemsJsonFileSize, } = await writeJsonAndBase64Files(allIssues, storagePath);
|
|
734
|
+
const { scanDataJsonFilePath, scanDataBase64FilePath, scanItemsJsonFilePath, scanItemsBase64FilePath, scanItemsSummaryJsonFilePath, scanItemsSummaryBase64FilePath, scanIssuesSummaryJsonFilePath, scanIssuesSummaryBase64FilePath, scanPagesDetailJsonFilePath, scanPagesDetailBase64FilePath, scanPagesSummaryJsonFilePath, scanPagesSummaryBase64FilePath, scanDataJsonFileSize, scanItemsJsonFileSize, } = await writeJsonAndBase64Files(allIssues, storagePath, itemsStore);
|
|
709
735
|
// Removed BIG_RESULTS_THRESHOLD check - always use full scanItems
|
|
710
736
|
await writeScanDetailsCsv(scanDataBase64FilePath, scanItemsBase64FilePath, scanItemsSummaryBase64FilePath, storagePath);
|
|
711
737
|
await writeSummaryHTML(allIssues, storagePath);
|
|
@@ -729,13 +755,36 @@ generateJsonFiles = false) => {
|
|
|
729
755
|
const browserChannel = getBrowserToRun(randomToken, BrowserTypes.CHROME, false).browserToRun;
|
|
730
756
|
// Should consider refactor constants.userDataDirectory to be a parameter in future
|
|
731
757
|
await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length, 'summary', browserChannel, constants.userDataDirectory), 1);
|
|
758
|
+
// Suppress uncaught EPERM errors from lingering Crawlee async lock-file operations
|
|
759
|
+
// (Windows holds mandatory file locks; Crawlee may still attempt mkdir on .json.lock
|
|
760
|
+
// files after the crawl has finished). Without this, Node crashes with uncaughtException.
|
|
761
|
+
const crawleeEpermHandler = (err) => {
|
|
762
|
+
if (err.code === 'EPERM' && err.message?.includes('crawlee')) {
|
|
763
|
+
consoleLogger.info(`Suppressed lingering Crawlee storage error: ${err.message}`);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Re-throw non-crawlee EPERM errors so they aren't silently swallowed
|
|
767
|
+
throw err;
|
|
768
|
+
};
|
|
769
|
+
process.on('uncaughtException', crawleeEpermHandler);
|
|
770
|
+
process.on('unhandledRejection', crawleeEpermHandler);
|
|
732
771
|
// Brief delay to allow lingering async crawlee storage operations to flush
|
|
733
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
772
|
+
await new Promise(resolve => setTimeout(resolve, process.platform === 'win32' ? 5000 : 3000));
|
|
773
|
+
const crawleePath = path.join(storagePath, 'crawlee');
|
|
734
774
|
try {
|
|
735
|
-
await fs.promises.rm(
|
|
775
|
+
await fs.promises.rm(crawleePath, { recursive: true, force: true });
|
|
736
776
|
}
|
|
737
777
|
catch (error) {
|
|
738
|
-
//
|
|
778
|
+
// On Windows, retry once after a delay if the folder is still locked
|
|
779
|
+
if (process.platform === 'win32') {
|
|
780
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
781
|
+
try {
|
|
782
|
+
await fs.promises.rm(crawleePath, { recursive: true, force: true });
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
// Best-effort cleanup — leave the folder; report generation continues
|
|
786
|
+
}
|
|
787
|
+
}
|
|
739
788
|
}
|
|
740
789
|
try {
|
|
741
790
|
await fs.promises.rm(path.join(storagePath, 'pdfs'), { recursive: true, force: true });
|
|
@@ -779,7 +828,9 @@ generateJsonFiles = false) => {
|
|
|
779
828
|
printMessage([`Error in zipping results: ${error}`]);
|
|
780
829
|
}
|
|
781
830
|
// Generate scrubbed HTML Code Snippets
|
|
782
|
-
const ruleIdJson = createRuleIdJson(allIssues);
|
|
831
|
+
const ruleIdJson = await createRuleIdJson(allIssues, itemsStore);
|
|
832
|
+
// Clean up intermediate items files
|
|
833
|
+
await itemsStore.cleanup();
|
|
783
834
|
// At the end of the function where results are generated, add:
|
|
784
835
|
try {
|
|
785
836
|
// Always send WCAG breakdown to Sentry, even if no violations were found
|
|
@@ -797,6 +848,8 @@ generateJsonFiles = false) => {
|
|
|
797
848
|
}
|
|
798
849
|
if (process.env.RUNNING_FROM_PH_GUI || process.env.OOBEE_VERBOSE)
|
|
799
850
|
console.log('Report generated successfully');
|
|
851
|
+
process.removeListener('uncaughtException', crawleeEpermHandler);
|
|
852
|
+
process.removeListener('unhandledRejection', crawleeEpermHandler);
|
|
800
853
|
return ruleIdJson;
|
|
801
854
|
};
|
|
802
855
|
export { writeHTML, compressJsonFileStreaming, convertItemsToReferences, flattenAndSortResults, populateScanPagesDetail, sendWcagBreakdownToSentry, getWcagPassPercentage, getProgressPercentage, getIssuesPercentage, itemTypeDescription, oobeeAiHtmlETL, oobeeAiRules, formatAboutStartTime, };
|