@govtechsg/oobee 0.10.42 → 0.10.45
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/REPORTS.md +71 -2
- package/package.json +4 -2
- package/src/cli.ts +2 -11
- package/src/constants/common.ts +216 -76
- package/src/constants/constants.ts +89 -1
- package/src/constants/oobeeAi.ts +6 -6
- package/src/constants/questions.ts +3 -2
- package/src/crawlers/commonCrawlerFunc.ts +16 -15
- package/src/crawlers/crawlDomain.ts +82 -84
- package/src/crawlers/crawlIntelligentSitemap.ts +21 -19
- package/src/crawlers/crawlSitemap.ts +120 -109
- package/src/crawlers/custom/findElementByCssSelector.ts +1 -1
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +8 -8
- package/src/crawlers/custom/xPathToCss.ts +10 -10
- package/src/crawlers/runCustom.ts +1 -1
- package/src/index.ts +3 -4
- package/src/logs.ts +1 -1
- package/src/mergeAxeResults.ts +126 -7
- package/src/npmIndex.ts +12 -8
- package/src/screenshotFunc/htmlScreenshotFunc.ts +8 -20
- package/src/types/text-readability.d.ts +3 -0
- package/src/types/types.ts +1 -1
- package/src/utils.ts +254 -114
- package/src/xPathToCss.ts +0 -186
- package/src/xPathToCssCypress.ts +0 -178
package/src/utils.ts
CHANGED
@@ -2,6 +2,7 @@ import { execSync, spawnSync } from 'child_process';
|
|
2
2
|
import path from 'path';
|
3
3
|
import os from 'os';
|
4
4
|
import fs from 'fs-extra';
|
5
|
+
import axe, { Rule } from 'axe-core';
|
5
6
|
import constants, {
|
6
7
|
BrowserTypes,
|
7
8
|
destinationPath,
|
@@ -9,25 +10,23 @@ import constants, {
|
|
9
10
|
} from './constants/constants.js';
|
10
11
|
import { consoleLogger, silentLogger } from './logs.js';
|
11
12
|
import { getAxeConfiguration } from './crawlers/custom/getAxeConfiguration.js';
|
12
|
-
import axe from 'axe-core';
|
13
|
-
import { Rule, RuleMetadata } from 'axe-core';
|
14
13
|
|
15
14
|
export const getVersion = () => {
|
16
|
-
const loadJSON = filePath =>
|
15
|
+
const loadJSON = (filePath: string): { version: string } =>
|
17
16
|
JSON.parse(fs.readFileSync(new URL(filePath, import.meta.url)).toString());
|
18
17
|
const versionNum = loadJSON('../package.json').version;
|
19
18
|
|
20
19
|
return versionNum;
|
21
20
|
};
|
22
21
|
|
23
|
-
export const getHost = url => new URL(url).host;
|
22
|
+
export const getHost = (url: string): string => new URL(url).host;
|
24
23
|
|
25
24
|
export const getCurrentDate = () => {
|
26
25
|
const date = new Date();
|
27
26
|
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
28
27
|
};
|
29
28
|
|
30
|
-
export const isWhitelistedContentType = contentType => {
|
29
|
+
export const isWhitelistedContentType = (contentType: string): boolean => {
|
31
30
|
const whitelist = ['text/html'];
|
32
31
|
return whitelist.filter(type => contentType.trim().startsWith(type)).length === 1;
|
33
32
|
};
|
@@ -45,7 +44,7 @@ export const getStoragePath = (randomToken: string): string => {
|
|
45
44
|
return `${constants.exportDirectory}/${randomToken}`;
|
46
45
|
};
|
47
46
|
|
48
|
-
export const createDetailsAndLogs = async randomToken => {
|
47
|
+
export const createDetailsAndLogs = async (randomToken: string): Promise<void> => {
|
49
48
|
const storagePath = getStoragePath(randomToken);
|
50
49
|
const logPath = `logs/${randomToken}`;
|
51
50
|
try {
|
@@ -59,20 +58,24 @@ export const createDetailsAndLogs = async randomToken => {
|
|
59
58
|
await fs.copy('errors.txt', `${logPath}/${randomToken}.txt`);
|
60
59
|
} catch (error) {
|
61
60
|
if (error.code === 'EBUSY') {
|
62
|
-
|
61
|
+
consoleLogger.error(
|
63
62
|
`Unable to copy the file from 'errors.txt' to '${logPath}/${randomToken}.txt' because it is currently in use.`,
|
64
63
|
);
|
65
|
-
|
64
|
+
consoleLogger.error(
|
66
65
|
'Please close any applications that might be using this file and try again.',
|
67
66
|
);
|
68
67
|
} else {
|
69
|
-
|
68
|
+
consoleLogger.error(
|
69
|
+
`An unexpected error occurred while copying the file: ${error.message}`,
|
70
|
+
);
|
70
71
|
}
|
71
72
|
}
|
72
73
|
}
|
73
74
|
});
|
74
75
|
} catch (error) {
|
75
|
-
|
76
|
+
consoleLogger.error(
|
77
|
+
`An error occurred while setting up storage or log directories: ${error.message}`,
|
78
|
+
);
|
76
79
|
}
|
77
80
|
};
|
78
81
|
|
@@ -99,7 +102,7 @@ export const getUserDataTxt = () => {
|
|
99
102
|
return null;
|
100
103
|
};
|
101
104
|
|
102
|
-
export const writeToUserDataTxt = async (key, value) => {
|
105
|
+
export const writeToUserDataTxt = async (key: string, value: string): Promise<void> => {
|
103
106
|
const textFilePath = getUserDataFilePath();
|
104
107
|
|
105
108
|
// Create file if it doesn't exist
|
@@ -116,25 +119,27 @@ export const writeToUserDataTxt = async (key, value) => {
|
|
116
119
|
}
|
117
120
|
};
|
118
121
|
|
119
|
-
export const createAndUpdateResultsFolders = async randomToken => {
|
122
|
+
export const createAndUpdateResultsFolders = async (randomToken: string): Promise<void> => {
|
120
123
|
const storagePath = getStoragePath(randomToken);
|
121
124
|
await fs.ensureDir(`${storagePath}`);
|
122
125
|
|
123
126
|
const intermediatePdfResultsPath = `${randomToken}/${constants.pdfScanResultFileName}`;
|
124
127
|
|
125
|
-
const transferResults = async (intermPath, resultFile) => {
|
128
|
+
const transferResults = async (intermPath: string, resultFile: string): Promise<void> => {
|
126
129
|
try {
|
127
130
|
if (fs.existsSync(intermPath)) {
|
128
131
|
await fs.copy(intermPath, `${storagePath}/${resultFile}`);
|
129
132
|
}
|
130
133
|
} catch (error) {
|
131
134
|
if (error.code === 'EBUSY') {
|
132
|
-
|
135
|
+
consoleLogger.error(
|
133
136
|
`Unable to copy the file from ${intermPath} to ${storagePath}/${resultFile} because it is currently in use.`,
|
134
137
|
);
|
135
|
-
|
138
|
+
consoleLogger.error(
|
139
|
+
'Please close any applications that might be using this file and try again.',
|
140
|
+
);
|
136
141
|
} else {
|
137
|
-
|
142
|
+
consoleLogger.error(
|
138
143
|
`An unexpected error occurred while copying the file from ${intermPath} to ${storagePath}/${resultFile}: ${error.message}`,
|
139
144
|
);
|
140
145
|
}
|
@@ -144,20 +149,20 @@ export const createAndUpdateResultsFolders = async randomToken => {
|
|
144
149
|
await Promise.all([transferResults(intermediatePdfResultsPath, constants.pdfScanResultFileName)]);
|
145
150
|
};
|
146
151
|
|
147
|
-
export const createScreenshotsFolder = randomToken => {
|
152
|
+
export const createScreenshotsFolder = (randomToken: string): void => {
|
148
153
|
const storagePath = getStoragePath(randomToken);
|
149
154
|
const intermediateScreenshotsPath = getIntermediateScreenshotsPath(randomToken);
|
150
155
|
if (fs.existsSync(intermediateScreenshotsPath)) {
|
151
156
|
fs.readdir(intermediateScreenshotsPath, (err, files) => {
|
152
157
|
if (err) {
|
153
|
-
|
158
|
+
consoleLogger.error(`Screenshots were not moved successfully: ${err.message}`);
|
154
159
|
}
|
155
160
|
|
156
161
|
if (!fs.existsSync(destinationPath(storagePath))) {
|
157
162
|
try {
|
158
163
|
fs.mkdirSync(destinationPath(storagePath), { recursive: true });
|
159
164
|
} catch (error) {
|
160
|
-
|
165
|
+
consoleLogger.error('Screenshots folder was not created successfully:', error);
|
161
166
|
}
|
162
167
|
}
|
163
168
|
|
@@ -170,20 +175,20 @@ export const createScreenshotsFolder = randomToken => {
|
|
170
175
|
|
171
176
|
fs.rmdir(intermediateScreenshotsPath, rmdirErr => {
|
172
177
|
if (rmdirErr) {
|
173
|
-
|
178
|
+
consoleLogger.error(rmdirErr);
|
174
179
|
}
|
175
180
|
});
|
176
181
|
});
|
177
182
|
}
|
178
183
|
};
|
179
184
|
|
180
|
-
export const cleanUp =
|
185
|
+
export const cleanUp = (pathToDelete: string): void => {
|
181
186
|
fs.removeSync(pathToDelete);
|
182
187
|
};
|
183
188
|
|
184
189
|
export const getWcagPassPercentage = (
|
185
190
|
wcagViolations: string[],
|
186
|
-
showEnableWcagAaa: boolean
|
191
|
+
showEnableWcagAaa: boolean,
|
187
192
|
): {
|
188
193
|
passPercentageAA: string;
|
189
194
|
totalWcagChecksAA: number;
|
@@ -192,20 +197,28 @@ export const getWcagPassPercentage = (
|
|
192
197
|
totalWcagChecksAAandAAA: number;
|
193
198
|
totalWcagViolationsAAandAAA: number;
|
194
199
|
} => {
|
195
|
-
|
196
200
|
// These AAA rules should not be counted as WCAG Pass Percentage only contains A and AA
|
197
201
|
const wcagAAALinks = ['WCAG 1.4.6', 'WCAG 2.2.4', 'WCAG 2.4.9', 'WCAG 3.1.5', 'WCAG 3.2.5'];
|
198
202
|
const wcagAAA = ['wcag146', 'wcag224', 'wcag249', 'wcag315', 'wcag325'];
|
199
|
-
|
203
|
+
|
200
204
|
const wcagLinksAAandAAA = constants.wcagLinks;
|
201
|
-
|
205
|
+
|
202
206
|
const wcagViolationsAAandAAA = showEnableWcagAaa ? wcagViolations.length : null;
|
203
207
|
const totalChecksAAandAAA = showEnableWcagAaa ? Object.keys(wcagLinksAAandAAA).length : null;
|
204
|
-
const passedChecksAAandAAA = showEnableWcagAaa
|
205
|
-
|
208
|
+
const passedChecksAAandAAA = showEnableWcagAaa
|
209
|
+
? totalChecksAAandAAA - wcagViolationsAAandAAA
|
210
|
+
: null;
|
211
|
+
// eslint-disable-next-line no-nested-ternary
|
212
|
+
const passPercentageAAandAAA = showEnableWcagAaa
|
213
|
+
? totalChecksAAandAAA === 0
|
214
|
+
? 0
|
215
|
+
: (passedChecksAAandAAA / totalChecksAAandAAA) * 100
|
216
|
+
: null;
|
206
217
|
|
207
218
|
const wcagViolationsAA = wcagViolations.filter(violation => !wcagAAA.includes(violation)).length;
|
208
|
-
const totalChecksAA = Object.keys(wcagLinksAAandAAA).filter(
|
219
|
+
const totalChecksAA = Object.keys(wcagLinksAAandAAA).filter(
|
220
|
+
key => !wcagAAALinks.includes(key),
|
221
|
+
).length;
|
209
222
|
const passedChecksAA = totalChecksAA - wcagViolationsAA;
|
210
223
|
const passPercentageAA = totalChecksAA === 0 ? 0 : (passedChecksAA / totalChecksAA) * 100;
|
211
224
|
|
@@ -219,13 +232,15 @@ export const getWcagPassPercentage = (
|
|
219
232
|
};
|
220
233
|
};
|
221
234
|
|
222
|
-
export
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
235
|
+
export type IssueCategory = 'mustFix' | 'goodToFix' | 'needsReview' | 'passed';
|
236
|
+
|
237
|
+
export interface IssueDetail {
|
238
|
+
ruleId: string;
|
239
|
+
wcagConformance: string[];
|
240
|
+
occurrencesMustFix?: number;
|
241
|
+
occurrencesGoodToFix?: number;
|
242
|
+
occurrencesNeedsReview?: number;
|
243
|
+
occurrencesPassed: number;
|
229
244
|
}
|
230
245
|
|
231
246
|
export interface PageDetail {
|
@@ -245,43 +260,43 @@ export interface PageDetail {
|
|
245
260
|
typesOfIssues: IssueDetail[];
|
246
261
|
}
|
247
262
|
|
248
|
-
export
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
occurrencesNeedsReview?: number;
|
256
|
-
occurrencesPassed: number;
|
263
|
+
export interface ScanPagesDetail {
|
264
|
+
oobeeAppVersion?: string;
|
265
|
+
pagesAffected: PageDetail[];
|
266
|
+
pagesNotAffected: PageDetail[];
|
267
|
+
scannedPagesCount: number;
|
268
|
+
pagesNotScanned: PageDetail[];
|
269
|
+
pagesNotScannedCount: number;
|
257
270
|
}
|
258
271
|
|
259
272
|
export const getProgressPercentage = (
|
260
273
|
scanPagesDetail: ScanPagesDetail,
|
261
|
-
showEnableWcagAaa: boolean
|
274
|
+
showEnableWcagAaa: boolean,
|
262
275
|
): {
|
263
276
|
averageProgressPercentageAA: string;
|
264
277
|
averageProgressPercentageAAandAAA: string;
|
265
278
|
} => {
|
266
279
|
const pages = scanPagesDetail.pagesAffected || [];
|
267
|
-
|
268
|
-
const progressPercentagesAA = pages.map((page:
|
280
|
+
|
281
|
+
const progressPercentagesAA = pages.map((page: PageDetail) => {
|
269
282
|
const violations: string[] = page.conformance;
|
270
283
|
return getWcagPassPercentage(violations, showEnableWcagAaa).passPercentageAA;
|
271
284
|
});
|
272
|
-
|
273
|
-
const progressPercentagesAAandAAA = pages.map((page:
|
285
|
+
|
286
|
+
const progressPercentagesAAandAAA = pages.map((page: PageDetail) => {
|
274
287
|
const violations: string[] = page.conformance;
|
275
288
|
return getWcagPassPercentage(violations, showEnableWcagAaa).passPercentageAAandAAA;
|
276
289
|
});
|
277
|
-
|
290
|
+
|
278
291
|
const totalAA = progressPercentagesAA.reduce((sum, p) => sum + parseFloat(p), 0);
|
279
292
|
const avgAA = progressPercentagesAA.length ? totalAA / progressPercentagesAA.length : 0;
|
280
293
|
|
281
294
|
const totalAAandAAA = progressPercentagesAAandAAA.reduce((sum, p) => sum + parseFloat(p), 0);
|
282
|
-
const avgAAandAAA = progressPercentagesAAandAAA.length
|
283
|
-
|
284
|
-
|
295
|
+
const avgAAandAAA = progressPercentagesAAandAAA.length
|
296
|
+
? totalAAandAAA / progressPercentagesAAandAAA.length
|
297
|
+
: 0;
|
298
|
+
|
299
|
+
return {
|
285
300
|
averageProgressPercentageAA: avgAA.toFixed(2),
|
286
301
|
averageProgressPercentageAAandAAA: avgAAandAAA.toFixed(2),
|
287
302
|
};
|
@@ -289,7 +304,7 @@ export const getProgressPercentage = (
|
|
289
304
|
|
290
305
|
export const getTotalRulesCount = async (
|
291
306
|
enableWcagAaa: boolean,
|
292
|
-
disableOobee: boolean
|
307
|
+
disableOobee: boolean,
|
293
308
|
): Promise<{
|
294
309
|
totalRulesMustFix: number;
|
295
310
|
totalRulesGoodToFix: number;
|
@@ -302,11 +317,11 @@ export const getTotalRulesCount = async (
|
|
302
317
|
});
|
303
318
|
|
304
319
|
// Get default rules from axe-core
|
305
|
-
const defaultRules =
|
320
|
+
const defaultRules = axe.getRules();
|
306
321
|
|
307
322
|
// Merge custom rules with default rules, converting RuleMetadata to Rule
|
308
|
-
const mergedRules: Rule[] = defaultRules.map(
|
309
|
-
const customRule = axeConfig.rules.find(
|
323
|
+
const mergedRules: Rule[] = defaultRules.map(defaultRule => {
|
324
|
+
const customRule = axeConfig.rules.find(r => r.id === defaultRule.ruleId);
|
310
325
|
if (customRule) {
|
311
326
|
// Merge properties from customRule into defaultRule (RuleMetadata) to create a Rule
|
312
327
|
return {
|
@@ -317,15 +332,14 @@ export const getTotalRulesCount = async (
|
|
317
332
|
tags: defaultRule.tags,
|
318
333
|
metadata: customRule.metadata, // Use custom metadata if it exists
|
319
334
|
};
|
320
|
-
} else {
|
321
|
-
// Convert defaultRule (RuleMetadata) to Rule
|
322
|
-
return {
|
323
|
-
id: defaultRule.ruleId,
|
324
|
-
enabled: true, // Default to true if not overridden
|
325
|
-
tags: defaultRule.tags,
|
326
|
-
// No metadata here, since defaultRule.metadata might not exist
|
327
|
-
};
|
328
335
|
}
|
336
|
+
// Convert defaultRule (RuleMetadata) to Rule
|
337
|
+
return {
|
338
|
+
id: defaultRule.ruleId,
|
339
|
+
enabled: true, // Default to true if not overridden
|
340
|
+
tags: defaultRule.tags,
|
341
|
+
// No metadata here, since defaultRule.metadata might not exist
|
342
|
+
};
|
329
343
|
});
|
330
344
|
|
331
345
|
// Add any custom rules that don't override the default rules
|
@@ -346,9 +360,7 @@ export const getTotalRulesCount = async (
|
|
346
360
|
});
|
347
361
|
|
348
362
|
// Apply the merged configuration to axe-core
|
349
|
-
|
350
|
-
|
351
|
-
const rules = await axe.getRules();
|
363
|
+
axe.configure({ ...axeConfig, rules: mergedRules });
|
352
364
|
|
353
365
|
// ... (rest of your logic)
|
354
366
|
let totalRulesMustFix = 0;
|
@@ -357,7 +369,7 @@ export const getTotalRulesCount = async (
|
|
357
369
|
const wcagRegex = /^wcag\d+a+$/;
|
358
370
|
|
359
371
|
// Use mergedRules instead of rules to check enabled property
|
360
|
-
mergedRules.forEach(
|
372
|
+
mergedRules.forEach(rule => {
|
361
373
|
if (!rule.enabled) {
|
362
374
|
return;
|
363
375
|
}
|
@@ -371,10 +383,14 @@ export const getTotalRulesCount = async (
|
|
371
383
|
return;
|
372
384
|
}
|
373
385
|
|
374
|
-
|
386
|
+
const conformance = tags.filter(tag => tag.startsWith('wcag') || tag === 'best-practice');
|
375
387
|
|
376
388
|
// Ensure conformance level is sorted correctly
|
377
|
-
if (
|
389
|
+
if (
|
390
|
+
conformance.length > 0 &&
|
391
|
+
conformance[0] !== 'best-practice' &&
|
392
|
+
!wcagRegex.test(conformance[0])
|
393
|
+
) {
|
378
394
|
conformance.sort((a, b) => {
|
379
395
|
if (wcagRegex.test(a) && !wcagRegex.test(b)) {
|
380
396
|
return -1;
|
@@ -389,11 +405,11 @@ export const getTotalRulesCount = async (
|
|
389
405
|
if (conformance.includes('best-practice')) {
|
390
406
|
// console.log(`${totalRulesMustFix} Good To Fix: ${rule.id}`);
|
391
407
|
|
392
|
-
totalRulesGoodToFix
|
408
|
+
totalRulesGoodToFix += 1; // Categorized as "Good to Fix"
|
393
409
|
} else {
|
394
410
|
// console.log(`${totalRulesMustFix} Must Fix: ${rule.id}`);
|
395
411
|
|
396
|
-
totalRulesMustFix
|
412
|
+
totalRulesMustFix += 1; // Otherwise, it's "Must Fix"
|
397
413
|
}
|
398
414
|
});
|
399
415
|
|
@@ -404,10 +420,136 @@ export const getTotalRulesCount = async (
|
|
404
420
|
};
|
405
421
|
};
|
406
422
|
|
423
|
+
/**
|
424
|
+
* Dynamically generates a map of WCAG criteria IDs to their details (name and level)
|
425
|
+
* Reuses the rule processing logic from getTotalRulesCount
|
426
|
+
*/
|
427
|
+
export const getWcagCriteriaMap = async (
|
428
|
+
enableWcagAaa: boolean = true,
|
429
|
+
disableOobee: boolean = false
|
430
|
+
): Promise<Record<string, { name: string; level: string }>> => {
|
431
|
+
// Reuse the configuration setup from getTotalRulesCount
|
432
|
+
const axeConfig = getAxeConfiguration({
|
433
|
+
enableWcagAaa,
|
434
|
+
gradingReadabilityFlag: '',
|
435
|
+
disableOobee,
|
436
|
+
});
|
437
|
+
|
438
|
+
// Get default rules from axe-core
|
439
|
+
const defaultRules = axe.getRules();
|
440
|
+
|
441
|
+
// Merge custom rules with default rules
|
442
|
+
const mergedRules: Rule[] = defaultRules.map(defaultRule => {
|
443
|
+
const customRule = axeConfig.rules.find(r => r.id === defaultRule.ruleId);
|
444
|
+
if (customRule) {
|
445
|
+
return {
|
446
|
+
id: defaultRule.ruleId,
|
447
|
+
enabled: customRule.enabled,
|
448
|
+
selector: customRule.selector,
|
449
|
+
any: customRule.any,
|
450
|
+
tags: defaultRule.tags,
|
451
|
+
metadata: customRule.metadata,
|
452
|
+
};
|
453
|
+
}
|
454
|
+
return {
|
455
|
+
id: defaultRule.ruleId,
|
456
|
+
enabled: true,
|
457
|
+
tags: defaultRule.tags,
|
458
|
+
};
|
459
|
+
});
|
460
|
+
|
461
|
+
// Add custom rules that don't override default rules
|
462
|
+
axeConfig.rules.forEach(customRule => {
|
463
|
+
if (!mergedRules.some(rule => rule.id === customRule.id)) {
|
464
|
+
mergedRules.push({
|
465
|
+
id: customRule.id,
|
466
|
+
enabled: customRule.enabled,
|
467
|
+
selector: customRule.selector,
|
468
|
+
any: customRule.any,
|
469
|
+
tags: customRule.tags,
|
470
|
+
metadata: customRule.metadata,
|
471
|
+
});
|
472
|
+
}
|
473
|
+
});
|
474
|
+
|
475
|
+
// Apply configuration
|
476
|
+
axe.configure({ ...axeConfig, rules: mergedRules });
|
477
|
+
|
478
|
+
// Build WCAG criteria map
|
479
|
+
const wcagCriteriaMap: Record<string, { name: string; level: string }> = {};
|
480
|
+
|
481
|
+
// Process rules to extract WCAG information
|
482
|
+
mergedRules.forEach(rule => {
|
483
|
+
if (!rule.enabled) return;
|
484
|
+
if (rule.id === 'frame-tested') return;
|
485
|
+
|
486
|
+
const tags = rule.tags || [];
|
487
|
+
if (tags.includes('experimental') || tags.includes('deprecated')) return;
|
488
|
+
|
489
|
+
// Look for WCAG criteria tags (format: wcag111, wcag143, etc.)
|
490
|
+
tags.forEach(tag => {
|
491
|
+
const wcagMatch = tag.match(/^wcag(\d+)$/);
|
492
|
+
if (wcagMatch) {
|
493
|
+
const wcagId = tag;
|
494
|
+
|
495
|
+
// Default values
|
496
|
+
let level = 'a';
|
497
|
+
let name = '';
|
498
|
+
|
499
|
+
// Try to extract better info from metadata if available
|
500
|
+
const metadata = rule.metadata as any;
|
501
|
+
if (metadata && metadata.wcag) {
|
502
|
+
const wcagInfo = metadata.wcag as any;
|
503
|
+
|
504
|
+
// Find matching criterion in metadata
|
505
|
+
for (const key in wcagInfo) {
|
506
|
+
const criterion = wcagInfo[key];
|
507
|
+
if (criterion &&
|
508
|
+
criterion.num &&
|
509
|
+
`wcag${criterion.num.replace(/\./g, '')}` === wcagId) {
|
510
|
+
|
511
|
+
// Extract level
|
512
|
+
if (criterion.level) {
|
513
|
+
level = String(criterion.level).toLowerCase();
|
514
|
+
}
|
515
|
+
|
516
|
+
// Extract name
|
517
|
+
if (criterion.handle) {
|
518
|
+
name = String(criterion.handle);
|
519
|
+
} else if (criterion.id) {
|
520
|
+
name = String(criterion.id);
|
521
|
+
} else if (criterion.num) {
|
522
|
+
name = `wcag-${String(criterion.num).replace(/\./g, '-')}`;
|
523
|
+
}
|
524
|
+
|
525
|
+
break;
|
526
|
+
}
|
527
|
+
}
|
528
|
+
}
|
529
|
+
|
530
|
+
// Generate fallback name if none found
|
531
|
+
if (!name) {
|
532
|
+
const numStr = wcagMatch[1];
|
533
|
+
const formattedNum = numStr.replace(/(\d)(\d)(\d+)?/, '$1.$2.$3');
|
534
|
+
name = `wcag-${formattedNum.replace(/\./g, '-')}`;
|
535
|
+
}
|
536
|
+
|
537
|
+
// Store in map
|
538
|
+
wcagCriteriaMap[wcagId] = {
|
539
|
+
name: name.toLowerCase().replace(/_/g, '-'),
|
540
|
+
level
|
541
|
+
};
|
542
|
+
}
|
543
|
+
});
|
544
|
+
});
|
545
|
+
|
546
|
+
return wcagCriteriaMap;
|
547
|
+
};
|
548
|
+
|
407
549
|
export const getIssuesPercentage = async (
|
408
550
|
scanPagesDetail: ScanPagesDetail,
|
409
551
|
enableWcagAaa: boolean,
|
410
|
-
disableOobee: boolean
|
552
|
+
disableOobee: boolean,
|
411
553
|
): Promise<{
|
412
554
|
avgTypesOfIssuesPercentageOfTotalRulesAtMustFix: string;
|
413
555
|
avgTypesOfIssuesPercentageOfTotalRulesAtGoodToFix: string;
|
@@ -426,8 +568,8 @@ export const getIssuesPercentage = async (
|
|
426
568
|
|
427
569
|
const pagesAffectedPerRule: Record<string, number> = {};
|
428
570
|
|
429
|
-
pages.forEach(
|
430
|
-
page.typesOfIssues.forEach(
|
571
|
+
pages.forEach(page => {
|
572
|
+
page.typesOfIssues.forEach(issue => {
|
431
573
|
if ((issue.occurrencesMustFix || issue.occurrencesGoodToFix) > 0) {
|
432
574
|
pagesAffectedPerRule[issue.ruleId] = (pagesAffectedPerRule[issue.ruleId] || 0) + 1;
|
433
575
|
}
|
@@ -435,55 +577,54 @@ export const getIssuesPercentage = async (
|
|
435
577
|
});
|
436
578
|
|
437
579
|
const pagesPercentageAffectedPerRule: Record<string, string> = {};
|
438
|
-
|
439
|
-
pagesPercentageAffectedPerRule[ruleId] =
|
440
|
-
|
580
|
+
Object.entries(pagesAffectedPerRule).forEach(([ruleId, count]) => {
|
581
|
+
pagesPercentageAffectedPerRule[ruleId] =
|
582
|
+
totalPages > 0 ? ((count / totalPages) * 100).toFixed(2) : '0.00';
|
583
|
+
});
|
441
584
|
|
442
|
-
const typesOfIssuesCountAtMustFix = pages.map(
|
443
|
-
page.typesOfIssues.filter(
|
585
|
+
const typesOfIssuesCountAtMustFix = pages.map(
|
586
|
+
page => page.typesOfIssues.filter(issue => (issue.occurrencesMustFix || 0) > 0).length,
|
444
587
|
);
|
445
588
|
|
446
|
-
const typesOfIssuesCountAtGoodToFix = pages.map(
|
447
|
-
page.typesOfIssues.filter(
|
589
|
+
const typesOfIssuesCountAtGoodToFix = pages.map(
|
590
|
+
page => page.typesOfIssues.filter(issue => (issue.occurrencesGoodToFix || 0) > 0).length,
|
448
591
|
);
|
449
592
|
|
450
593
|
const typesOfIssuesCountSumMustFixAndGoodToFix = pages.map(
|
451
594
|
(_, index) =>
|
452
|
-
(typesOfIssuesCountAtMustFix[index] || 0) +
|
453
|
-
(typesOfIssuesCountAtGoodToFix[index] || 0)
|
595
|
+
(typesOfIssuesCountAtMustFix[index] || 0) + (typesOfIssuesCountAtGoodToFix[index] || 0),
|
454
596
|
);
|
455
597
|
|
456
|
-
const { totalRulesMustFix, totalRulesGoodToFix, totalRulesMustFixAndGoodToFix } =
|
457
|
-
enableWcagAaa,
|
458
|
-
disableOobee
|
459
|
-
);
|
598
|
+
const { totalRulesMustFix, totalRulesGoodToFix, totalRulesMustFixAndGoodToFix } =
|
599
|
+
await getTotalRulesCount(enableWcagAaa, disableOobee);
|
460
600
|
|
461
|
-
const avgMustFixPerPage =
|
462
|
-
|
463
|
-
|
601
|
+
const avgMustFixPerPage =
|
602
|
+
totalPages > 0
|
603
|
+
? typesOfIssuesCountAtMustFix.reduce((sum, count) => sum + count, 0) / totalPages
|
604
|
+
: 0;
|
464
605
|
|
465
|
-
const avgGoodToFixPerPage =
|
466
|
-
|
467
|
-
|
606
|
+
const avgGoodToFixPerPage =
|
607
|
+
totalPages > 0
|
608
|
+
? typesOfIssuesCountAtGoodToFix.reduce((sum, count) => sum + count, 0) / totalPages
|
609
|
+
: 0;
|
468
610
|
|
469
|
-
const avgMustFixAndGoodToFixPerPage =
|
470
|
-
|
471
|
-
|
611
|
+
const avgMustFixAndGoodToFixPerPage =
|
612
|
+
totalPages > 0
|
613
|
+
? typesOfIssuesCountSumMustFixAndGoodToFix.reduce((sum, count) => sum + count, 0) / totalPages
|
614
|
+
: 0;
|
472
615
|
|
473
616
|
const avgTypesOfIssuesPercentageOfTotalRulesAtMustFix =
|
474
|
-
totalRulesMustFix > 0
|
475
|
-
? ((avgMustFixPerPage / totalRulesMustFix) * 100).toFixed(2)
|
476
|
-
: "0.00";
|
617
|
+
totalRulesMustFix > 0 ? ((avgMustFixPerPage / totalRulesMustFix) * 100).toFixed(2) : '0.00';
|
477
618
|
|
478
619
|
const avgTypesOfIssuesPercentageOfTotalRulesAtGoodToFix =
|
479
620
|
totalRulesGoodToFix > 0
|
480
621
|
? ((avgGoodToFixPerPage / totalRulesGoodToFix) * 100).toFixed(2)
|
481
|
-
:
|
622
|
+
: '0.00';
|
482
623
|
|
483
624
|
const avgTypesOfIssuesPercentageOfTotalRulesAtMustFixAndGoodToFix =
|
484
625
|
totalRulesMustFixAndGoodToFix > 0
|
485
626
|
? ((avgMustFixAndGoodToFixPerPage / totalRulesMustFixAndGoodToFix) * 100).toFixed(2)
|
486
|
-
:
|
627
|
+
: '0.00';
|
487
628
|
|
488
629
|
const avgTypesOfIssuesCountAtMustFix = avgMustFixPerPage.toFixed(2);
|
489
630
|
const avgTypesOfIssuesCountAtGoodToFix = avgGoodToFixPerPage.toFixed(2);
|
@@ -504,7 +645,7 @@ export const getIssuesPercentage = async (
|
|
504
645
|
};
|
505
646
|
};
|
506
647
|
|
507
|
-
export const getFormattedTime = inputDate => {
|
648
|
+
export const getFormattedTime = (inputDate: Date): string => {
|
508
649
|
if (inputDate) {
|
509
650
|
return inputDate.toLocaleTimeString('en-GB', {
|
510
651
|
year: 'numeric',
|
@@ -526,7 +667,7 @@ export const getFormattedTime = inputDate => {
|
|
526
667
|
});
|
527
668
|
};
|
528
669
|
|
529
|
-
export const formatDateTimeForMassScanner = date => {
|
670
|
+
export const formatDateTimeForMassScanner = (date: Date): string => {
|
530
671
|
// Format date and time parts separately
|
531
672
|
const year = date.getFullYear().toString().slice(-2); // Get the last two digits of the year
|
532
673
|
const month = `0${date.getMonth() + 1}`.slice(-2); // Month is zero-indexed
|
@@ -547,14 +688,13 @@ export const setHeadlessMode = (browser: string, isHeadless: boolean): void => {
|
|
547
688
|
} else {
|
548
689
|
process.env.CRAWLEE_HEADLESS = '0';
|
549
690
|
}
|
550
|
-
|
551
691
|
};
|
552
692
|
|
553
|
-
export const setThresholdLimits = setWarnLevel => {
|
693
|
+
export const setThresholdLimits = (setWarnLevel: string): void => {
|
554
694
|
process.env.WARN_LEVEL = setWarnLevel;
|
555
695
|
};
|
556
696
|
|
557
|
-
export const zipResults = (zipName, resultsPath) => {
|
697
|
+
export const zipResults = (zipName: string, resultsPath: string): void => {
|
558
698
|
// Check prior zip file exist and remove
|
559
699
|
if (fs.existsSync(zipName)) {
|
560
700
|
fs.unlinkSync(zipName);
|
@@ -585,9 +725,9 @@ export const zipResults = (zipName, resultsPath) => {
|
|
585
725
|
|
586
726
|
// areLinksEqual compares 2 string URLs and ignores comparison of 'www.' and url protocol
|
587
727
|
// i.e. 'http://google.com' and 'https://www.google.com' returns true
|
588
|
-
export const areLinksEqual = (link1, link2) => {
|
728
|
+
export const areLinksEqual = (link1: string, link2: string): boolean => {
|
589
729
|
try {
|
590
|
-
const format = link => {
|
730
|
+
const format = (link: string): URL => {
|
591
731
|
return new URL(link.replace(/www\./, ''));
|
592
732
|
};
|
593
733
|
const l1 = format(link1);
|
@@ -612,7 +752,7 @@ export const randomThreeDigitNumberString = () => {
|
|
612
752
|
return String(threeDigitNumber);
|
613
753
|
};
|
614
754
|
|
615
|
-
export const isFollowStrategy = (link1, link2, rule) => {
|
755
|
+
export const isFollowStrategy = (link1: string, link2: string, rule: string): boolean => {
|
616
756
|
const parsedLink1 = new URL(link1);
|
617
757
|
const parsedLink2 = new URL(link2);
|
618
758
|
if (rule === 'same-domain') {
|
@@ -623,17 +763,17 @@ export const isFollowStrategy = (link1, link2, rule) => {
|
|
623
763
|
return parsedLink1.hostname === parsedLink2.hostname;
|
624
764
|
};
|
625
765
|
|
626
|
-
|
627
|
-
export const retryFunction = async (func, maxAttempt) => {
|
766
|
+
export const retryFunction = async <T>(func: () => Promise<T>, maxAttempt: number): Promise<T> => {
|
628
767
|
let attemptCount = 0;
|
629
768
|
while (attemptCount < maxAttempt) {
|
630
769
|
attemptCount += 1;
|
631
770
|
try {
|
771
|
+
// eslint-disable-next-line no-await-in-loop
|
632
772
|
const result = await func();
|
633
773
|
return result;
|
634
774
|
} catch (error) {
|
635
775
|
silentLogger.error(`(Attempt count: ${attemptCount} of ${maxAttempt}) ${error}`);
|
636
776
|
}
|
637
777
|
}
|
778
|
+
throw new Error('Maximum number of attempts reached');
|
638
779
|
};
|
639
|
-
/* eslint-enable no-await-in-loop */
|