@govtechsg/oobee 0.10.39 → 0.10.43
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/docker-test.yml +1 -1
- package/README.md +2 -0
- package/REPORTS.md +431 -0
- package/package.json +3 -2
- package/src/cli.ts +2 -11
- package/src/constants/common.ts +68 -52
- package/src/constants/constants.ts +81 -1
- package/src/constants/oobeeAi.ts +6 -6
- package/src/constants/questions.ts +3 -2
- package/src/crawlers/commonCrawlerFunc.ts +45 -16
- package/src/crawlers/crawlDomain.ts +83 -102
- package/src/crawlers/crawlIntelligentSitemap.ts +21 -19
- package/src/crawlers/crawlSitemap.ts +121 -110
- package/src/crawlers/custom/findElementByCssSelector.ts +1 -1
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +593 -558
- package/src/crawlers/custom/xPathToCss.ts +10 -10
- package/src/crawlers/pdfScanFunc.ts +67 -26
- package/src/crawlers/runCustom.ts +1 -1
- package/src/index.ts +3 -4
- package/src/logs.ts +1 -1
- package/src/mergeAxeResults.ts +305 -242
- package/src/npmIndex.ts +12 -8
- package/src/screenshotFunc/htmlScreenshotFunc.ts +8 -20
- package/src/screenshotFunc/pdfScreenshotFunc.ts +34 -1
- package/src/types/text-readability.d.ts +3 -0
- package/src/types/types.ts +1 -1
- package/src/utils.ts +340 -50
- package/src/xPathToCss.ts +0 -186
- package/src/xPathToCssCypress.ts +0 -178
package/src/utils.ts
CHANGED
@@ -2,29 +2,31 @@ 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,
|
8
9
|
getIntermediateScreenshotsPath,
|
9
10
|
} from './constants/constants.js';
|
10
|
-
import { silentLogger } from './logs.js';
|
11
|
+
import { consoleLogger, silentLogger } from './logs.js';
|
12
|
+
import { getAxeConfiguration } from './crawlers/custom/getAxeConfiguration.js';
|
11
13
|
|
12
14
|
export const getVersion = () => {
|
13
|
-
const loadJSON = filePath =>
|
15
|
+
const loadJSON = (filePath: string): { version: string } =>
|
14
16
|
JSON.parse(fs.readFileSync(new URL(filePath, import.meta.url)).toString());
|
15
17
|
const versionNum = loadJSON('../package.json').version;
|
16
18
|
|
17
19
|
return versionNum;
|
18
20
|
};
|
19
21
|
|
20
|
-
export const getHost = url => new URL(url).host;
|
22
|
+
export const getHost = (url: string): string => new URL(url).host;
|
21
23
|
|
22
24
|
export const getCurrentDate = () => {
|
23
25
|
const date = new Date();
|
24
26
|
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
25
27
|
};
|
26
28
|
|
27
|
-
export const isWhitelistedContentType = contentType => {
|
29
|
+
export const isWhitelistedContentType = (contentType: string): boolean => {
|
28
30
|
const whitelist = ['text/html'];
|
29
31
|
return whitelist.filter(type => contentType.trim().startsWith(type)).length === 1;
|
30
32
|
};
|
@@ -42,7 +44,7 @@ export const getStoragePath = (randomToken: string): string => {
|
|
42
44
|
return `${constants.exportDirectory}/${randomToken}`;
|
43
45
|
};
|
44
46
|
|
45
|
-
export const createDetailsAndLogs = async randomToken => {
|
47
|
+
export const createDetailsAndLogs = async (randomToken: string): Promise<void> => {
|
46
48
|
const storagePath = getStoragePath(randomToken);
|
47
49
|
const logPath = `logs/${randomToken}`;
|
48
50
|
try {
|
@@ -56,20 +58,24 @@ export const createDetailsAndLogs = async randomToken => {
|
|
56
58
|
await fs.copy('errors.txt', `${logPath}/${randomToken}.txt`);
|
57
59
|
} catch (error) {
|
58
60
|
if (error.code === 'EBUSY') {
|
59
|
-
|
61
|
+
consoleLogger.error(
|
60
62
|
`Unable to copy the file from 'errors.txt' to '${logPath}/${randomToken}.txt' because it is currently in use.`,
|
61
63
|
);
|
62
|
-
|
64
|
+
consoleLogger.error(
|
63
65
|
'Please close any applications that might be using this file and try again.',
|
64
66
|
);
|
65
67
|
} else {
|
66
|
-
|
68
|
+
consoleLogger.error(
|
69
|
+
`An unexpected error occurred while copying the file: ${error.message}`,
|
70
|
+
);
|
67
71
|
}
|
68
72
|
}
|
69
73
|
}
|
70
74
|
});
|
71
75
|
} catch (error) {
|
72
|
-
|
76
|
+
consoleLogger.error(
|
77
|
+
`An error occurred while setting up storage or log directories: ${error.message}`,
|
78
|
+
);
|
73
79
|
}
|
74
80
|
};
|
75
81
|
|
@@ -96,7 +102,7 @@ export const getUserDataTxt = () => {
|
|
96
102
|
return null;
|
97
103
|
};
|
98
104
|
|
99
|
-
export const writeToUserDataTxt = async (key, value) => {
|
105
|
+
export const writeToUserDataTxt = async (key: string, value: string): Promise<void> => {
|
100
106
|
const textFilePath = getUserDataFilePath();
|
101
107
|
|
102
108
|
// Create file if it doesn't exist
|
@@ -113,25 +119,27 @@ export const writeToUserDataTxt = async (key, value) => {
|
|
113
119
|
}
|
114
120
|
};
|
115
121
|
|
116
|
-
export const createAndUpdateResultsFolders = async randomToken => {
|
122
|
+
export const createAndUpdateResultsFolders = async (randomToken: string): Promise<void> => {
|
117
123
|
const storagePath = getStoragePath(randomToken);
|
118
124
|
await fs.ensureDir(`${storagePath}`);
|
119
125
|
|
120
126
|
const intermediatePdfResultsPath = `${randomToken}/${constants.pdfScanResultFileName}`;
|
121
127
|
|
122
|
-
const transferResults = async (intermPath, resultFile) => {
|
128
|
+
const transferResults = async (intermPath: string, resultFile: string): Promise<void> => {
|
123
129
|
try {
|
124
130
|
if (fs.existsSync(intermPath)) {
|
125
131
|
await fs.copy(intermPath, `${storagePath}/${resultFile}`);
|
126
132
|
}
|
127
133
|
} catch (error) {
|
128
134
|
if (error.code === 'EBUSY') {
|
129
|
-
|
135
|
+
consoleLogger.error(
|
130
136
|
`Unable to copy the file from ${intermPath} to ${storagePath}/${resultFile} because it is currently in use.`,
|
131
137
|
);
|
132
|
-
|
138
|
+
consoleLogger.error(
|
139
|
+
'Please close any applications that might be using this file and try again.',
|
140
|
+
);
|
133
141
|
} else {
|
134
|
-
|
142
|
+
consoleLogger.error(
|
135
143
|
`An unexpected error occurred while copying the file from ${intermPath} to ${storagePath}/${resultFile}: ${error.message}`,
|
136
144
|
);
|
137
145
|
}
|
@@ -141,20 +149,20 @@ export const createAndUpdateResultsFolders = async randomToken => {
|
|
141
149
|
await Promise.all([transferResults(intermediatePdfResultsPath, constants.pdfScanResultFileName)]);
|
142
150
|
};
|
143
151
|
|
144
|
-
export const createScreenshotsFolder = randomToken => {
|
152
|
+
export const createScreenshotsFolder = (randomToken: string): void => {
|
145
153
|
const storagePath = getStoragePath(randomToken);
|
146
154
|
const intermediateScreenshotsPath = getIntermediateScreenshotsPath(randomToken);
|
147
155
|
if (fs.existsSync(intermediateScreenshotsPath)) {
|
148
156
|
fs.readdir(intermediateScreenshotsPath, (err, files) => {
|
149
157
|
if (err) {
|
150
|
-
|
158
|
+
consoleLogger.error(`Screenshots were not moved successfully: ${err.message}`);
|
151
159
|
}
|
152
160
|
|
153
161
|
if (!fs.existsSync(destinationPath(storagePath))) {
|
154
162
|
try {
|
155
163
|
fs.mkdirSync(destinationPath(storagePath), { recursive: true });
|
156
164
|
} catch (error) {
|
157
|
-
|
165
|
+
consoleLogger.error('Screenshots folder was not created successfully:', error);
|
158
166
|
}
|
159
167
|
}
|
160
168
|
|
@@ -167,32 +175,20 @@ export const createScreenshotsFolder = randomToken => {
|
|
167
175
|
|
168
176
|
fs.rmdir(intermediateScreenshotsPath, rmdirErr => {
|
169
177
|
if (rmdirErr) {
|
170
|
-
|
178
|
+
consoleLogger.error(rmdirErr);
|
171
179
|
}
|
172
180
|
});
|
173
181
|
});
|
174
182
|
}
|
175
183
|
};
|
176
184
|
|
177
|
-
export const cleanUp =
|
185
|
+
export const cleanUp = (pathToDelete: string): void => {
|
178
186
|
fs.removeSync(pathToDelete);
|
179
187
|
};
|
180
188
|
|
181
|
-
/* istanbul ignore next */
|
182
|
-
// export const getFormattedTime = () =>
|
183
|
-
// new Date().toLocaleTimeString('en-GB', {
|
184
|
-
// year: 'numeric',
|
185
|
-
// month: 'short',
|
186
|
-
// day: 'numeric',
|
187
|
-
// hour12: true,
|
188
|
-
// hour: 'numeric',
|
189
|
-
// minute: '2-digit',
|
190
|
-
// timeZoneName: "longGeneric",
|
191
|
-
// });
|
192
|
-
|
193
189
|
export const getWcagPassPercentage = (
|
194
190
|
wcagViolations: string[],
|
195
|
-
showEnableWcagAaa: boolean
|
191
|
+
showEnableWcagAaa: boolean,
|
196
192
|
): {
|
197
193
|
passPercentageAA: string;
|
198
194
|
totalWcagChecksAA: number;
|
@@ -201,20 +197,28 @@ export const getWcagPassPercentage = (
|
|
201
197
|
totalWcagChecksAAandAAA: number;
|
202
198
|
totalWcagViolationsAAandAAA: number;
|
203
199
|
} => {
|
204
|
-
|
205
200
|
// These AAA rules should not be counted as WCAG Pass Percentage only contains A and AA
|
206
201
|
const wcagAAALinks = ['WCAG 1.4.6', 'WCAG 2.2.4', 'WCAG 2.4.9', 'WCAG 3.1.5', 'WCAG 3.2.5'];
|
207
202
|
const wcagAAA = ['wcag146', 'wcag224', 'wcag249', 'wcag315', 'wcag325'];
|
208
|
-
|
203
|
+
|
209
204
|
const wcagLinksAAandAAA = constants.wcagLinks;
|
210
|
-
|
205
|
+
|
211
206
|
const wcagViolationsAAandAAA = showEnableWcagAaa ? wcagViolations.length : null;
|
212
207
|
const totalChecksAAandAAA = showEnableWcagAaa ? Object.keys(wcagLinksAAandAAA).length : null;
|
213
|
-
const passedChecksAAandAAA = showEnableWcagAaa
|
214
|
-
|
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;
|
215
217
|
|
216
218
|
const wcagViolationsAA = wcagViolations.filter(violation => !wcagAAA.includes(violation)).length;
|
217
|
-
const totalChecksAA = Object.keys(wcagLinksAAandAAA).filter(
|
219
|
+
const totalChecksAA = Object.keys(wcagLinksAAandAAA).filter(
|
220
|
+
key => !wcagAAALinks.includes(key),
|
221
|
+
).length;
|
218
222
|
const passedChecksAA = totalChecksAA - wcagViolationsAA;
|
219
223
|
const passPercentageAA = totalChecksAA === 0 ? 0 : (passedChecksAA / totalChecksAA) * 100;
|
220
224
|
|
@@ -228,7 +232,294 @@ export const getWcagPassPercentage = (
|
|
228
232
|
};
|
229
233
|
};
|
230
234
|
|
231
|
-
export
|
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;
|
244
|
+
}
|
245
|
+
|
246
|
+
export interface PageDetail {
|
247
|
+
pageTitle: string;
|
248
|
+
url: string;
|
249
|
+
totalOccurrencesFailedIncludingNeedsReview: number;
|
250
|
+
totalOccurrencesFailedExcludingNeedsReview: number;
|
251
|
+
totalOccurrencesMustFix?: number;
|
252
|
+
totalOccurrencesGoodToFix?: number;
|
253
|
+
totalOccurrencesNeedsReview: number;
|
254
|
+
totalOccurrencesPassed: number;
|
255
|
+
occurrencesExclusiveToNeedsReview: boolean;
|
256
|
+
typesOfIssuesCount: number;
|
257
|
+
typesOfIssuesExcludingNeedsReviewCount: number;
|
258
|
+
categoriesPresent: IssueCategory[];
|
259
|
+
conformance?: string[]; // WCAG levels as flexible strings
|
260
|
+
typesOfIssues: IssueDetail[];
|
261
|
+
}
|
262
|
+
|
263
|
+
export interface ScanPagesDetail {
|
264
|
+
oobeeAppVersion?: string;
|
265
|
+
pagesAffected: PageDetail[];
|
266
|
+
pagesNotAffected: PageDetail[];
|
267
|
+
scannedPagesCount: number;
|
268
|
+
pagesNotScanned: PageDetail[];
|
269
|
+
pagesNotScannedCount: number;
|
270
|
+
}
|
271
|
+
|
272
|
+
export const getProgressPercentage = (
|
273
|
+
scanPagesDetail: ScanPagesDetail,
|
274
|
+
showEnableWcagAaa: boolean,
|
275
|
+
): {
|
276
|
+
averageProgressPercentageAA: string;
|
277
|
+
averageProgressPercentageAAandAAA: string;
|
278
|
+
} => {
|
279
|
+
const pages = scanPagesDetail.pagesAffected || [];
|
280
|
+
|
281
|
+
const progressPercentagesAA = pages.map((page: PageDetail) => {
|
282
|
+
const violations: string[] = page.conformance;
|
283
|
+
return getWcagPassPercentage(violations, showEnableWcagAaa).passPercentageAA;
|
284
|
+
});
|
285
|
+
|
286
|
+
const progressPercentagesAAandAAA = pages.map((page: PageDetail) => {
|
287
|
+
const violations: string[] = page.conformance;
|
288
|
+
return getWcagPassPercentage(violations, showEnableWcagAaa).passPercentageAAandAAA;
|
289
|
+
});
|
290
|
+
|
291
|
+
const totalAA = progressPercentagesAA.reduce((sum, p) => sum + parseFloat(p), 0);
|
292
|
+
const avgAA = progressPercentagesAA.length ? totalAA / progressPercentagesAA.length : 0;
|
293
|
+
|
294
|
+
const totalAAandAAA = progressPercentagesAAandAAA.reduce((sum, p) => sum + parseFloat(p), 0);
|
295
|
+
const avgAAandAAA = progressPercentagesAAandAAA.length
|
296
|
+
? totalAAandAAA / progressPercentagesAAandAAA.length
|
297
|
+
: 0;
|
298
|
+
|
299
|
+
return {
|
300
|
+
averageProgressPercentageAA: avgAA.toFixed(2),
|
301
|
+
averageProgressPercentageAAandAAA: avgAAandAAA.toFixed(2),
|
302
|
+
};
|
303
|
+
};
|
304
|
+
|
305
|
+
export const getTotalRulesCount = async (
|
306
|
+
enableWcagAaa: boolean,
|
307
|
+
disableOobee: boolean,
|
308
|
+
): Promise<{
|
309
|
+
totalRulesMustFix: number;
|
310
|
+
totalRulesGoodToFix: number;
|
311
|
+
totalRulesMustFixAndGoodToFix: number;
|
312
|
+
}> => {
|
313
|
+
const axeConfig = getAxeConfiguration({
|
314
|
+
enableWcagAaa,
|
315
|
+
gradingReadabilityFlag: '',
|
316
|
+
disableOobee,
|
317
|
+
});
|
318
|
+
|
319
|
+
// Get default rules from axe-core
|
320
|
+
const defaultRules = axe.getRules();
|
321
|
+
|
322
|
+
// Merge custom rules with default rules, converting RuleMetadata to Rule
|
323
|
+
const mergedRules: Rule[] = defaultRules.map(defaultRule => {
|
324
|
+
const customRule = axeConfig.rules.find(r => r.id === defaultRule.ruleId);
|
325
|
+
if (customRule) {
|
326
|
+
// Merge properties from customRule into defaultRule (RuleMetadata) to create a Rule
|
327
|
+
return {
|
328
|
+
id: defaultRule.ruleId,
|
329
|
+
enabled: customRule.enabled,
|
330
|
+
selector: customRule.selector,
|
331
|
+
any: customRule.any,
|
332
|
+
tags: defaultRule.tags,
|
333
|
+
metadata: customRule.metadata, // Use custom metadata if it exists
|
334
|
+
};
|
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
|
+
};
|
343
|
+
});
|
344
|
+
|
345
|
+
// Add any custom rules that don't override the default rules
|
346
|
+
axeConfig.rules.forEach(customRule => {
|
347
|
+
if (!mergedRules.some(mergedRule => mergedRule.id === customRule.id)) {
|
348
|
+
// Ensure customRule is of type Rule
|
349
|
+
const rule: Rule = {
|
350
|
+
id: customRule.id,
|
351
|
+
enabled: customRule.enabled,
|
352
|
+
selector: customRule.selector,
|
353
|
+
any: customRule.any,
|
354
|
+
tags: customRule.tags,
|
355
|
+
metadata: customRule.metadata,
|
356
|
+
// Add other properties if needed
|
357
|
+
};
|
358
|
+
mergedRules.push(rule);
|
359
|
+
}
|
360
|
+
});
|
361
|
+
|
362
|
+
// Apply the merged configuration to axe-core
|
363
|
+
axe.configure({ ...axeConfig, rules: mergedRules });
|
364
|
+
|
365
|
+
// ... (rest of your logic)
|
366
|
+
let totalRulesMustFix = 0;
|
367
|
+
let totalRulesGoodToFix = 0;
|
368
|
+
|
369
|
+
const wcagRegex = /^wcag\d+a+$/;
|
370
|
+
|
371
|
+
// Use mergedRules instead of rules to check enabled property
|
372
|
+
mergedRules.forEach(rule => {
|
373
|
+
if (!rule.enabled) {
|
374
|
+
return;
|
375
|
+
}
|
376
|
+
|
377
|
+
if (rule.id === 'frame-tested') return; // Ignore 'frame-tested' rule
|
378
|
+
|
379
|
+
const tags = rule.tags || [];
|
380
|
+
|
381
|
+
// Skip experimental and deprecated rules
|
382
|
+
if (tags.includes('experimental') || tags.includes('deprecated')) {
|
383
|
+
return;
|
384
|
+
}
|
385
|
+
|
386
|
+
const conformance = tags.filter(tag => tag.startsWith('wcag') || tag === 'best-practice');
|
387
|
+
|
388
|
+
// Ensure conformance level is sorted correctly
|
389
|
+
if (
|
390
|
+
conformance.length > 0 &&
|
391
|
+
conformance[0] !== 'best-practice' &&
|
392
|
+
!wcagRegex.test(conformance[0])
|
393
|
+
) {
|
394
|
+
conformance.sort((a, b) => {
|
395
|
+
if (wcagRegex.test(a) && !wcagRegex.test(b)) {
|
396
|
+
return -1;
|
397
|
+
}
|
398
|
+
if (!wcagRegex.test(a) && wcagRegex.test(b)) {
|
399
|
+
return 1;
|
400
|
+
}
|
401
|
+
return 0;
|
402
|
+
});
|
403
|
+
}
|
404
|
+
|
405
|
+
if (conformance.includes('best-practice')) {
|
406
|
+
// console.log(`${totalRulesMustFix} Good To Fix: ${rule.id}`);
|
407
|
+
|
408
|
+
totalRulesGoodToFix += 1; // Categorized as "Good to Fix"
|
409
|
+
} else {
|
410
|
+
// console.log(`${totalRulesMustFix} Must Fix: ${rule.id}`);
|
411
|
+
|
412
|
+
totalRulesMustFix += 1; // Otherwise, it's "Must Fix"
|
413
|
+
}
|
414
|
+
});
|
415
|
+
|
416
|
+
return {
|
417
|
+
totalRulesMustFix,
|
418
|
+
totalRulesGoodToFix,
|
419
|
+
totalRulesMustFixAndGoodToFix: totalRulesMustFix + totalRulesGoodToFix,
|
420
|
+
};
|
421
|
+
};
|
422
|
+
|
423
|
+
export const getIssuesPercentage = async (
|
424
|
+
scanPagesDetail: ScanPagesDetail,
|
425
|
+
enableWcagAaa: boolean,
|
426
|
+
disableOobee: boolean,
|
427
|
+
): Promise<{
|
428
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtMustFix: string;
|
429
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtGoodToFix: string;
|
430
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtMustFixAndGoodToFix: string;
|
431
|
+
totalRulesMustFix: number;
|
432
|
+
totalRulesGoodToFix: number;
|
433
|
+
totalRulesMustFixAndGoodToFix: number;
|
434
|
+
avgTypesOfIssuesCountAtMustFix: string;
|
435
|
+
avgTypesOfIssuesCountAtGoodToFix: string;
|
436
|
+
avgTypesOfIssuesCountAtMustFixAndGoodToFix: string;
|
437
|
+
pagesAffectedPerRule: Record<string, number>;
|
438
|
+
pagesPercentageAffectedPerRule: Record<string, string>;
|
439
|
+
}> => {
|
440
|
+
const pages = scanPagesDetail.pagesAffected || [];
|
441
|
+
const totalPages = pages.length;
|
442
|
+
|
443
|
+
const pagesAffectedPerRule: Record<string, number> = {};
|
444
|
+
|
445
|
+
pages.forEach(page => {
|
446
|
+
page.typesOfIssues.forEach(issue => {
|
447
|
+
if ((issue.occurrencesMustFix || issue.occurrencesGoodToFix) > 0) {
|
448
|
+
pagesAffectedPerRule[issue.ruleId] = (pagesAffectedPerRule[issue.ruleId] || 0) + 1;
|
449
|
+
}
|
450
|
+
});
|
451
|
+
});
|
452
|
+
|
453
|
+
const pagesPercentageAffectedPerRule: Record<string, string> = {};
|
454
|
+
Object.entries(pagesAffectedPerRule).forEach(([ruleId, count]) => {
|
455
|
+
pagesPercentageAffectedPerRule[ruleId] =
|
456
|
+
totalPages > 0 ? ((count / totalPages) * 100).toFixed(2) : '0.00';
|
457
|
+
});
|
458
|
+
|
459
|
+
const typesOfIssuesCountAtMustFix = pages.map(
|
460
|
+
page => page.typesOfIssues.filter(issue => (issue.occurrencesMustFix || 0) > 0).length,
|
461
|
+
);
|
462
|
+
|
463
|
+
const typesOfIssuesCountAtGoodToFix = pages.map(
|
464
|
+
page => page.typesOfIssues.filter(issue => (issue.occurrencesGoodToFix || 0) > 0).length,
|
465
|
+
);
|
466
|
+
|
467
|
+
const typesOfIssuesCountSumMustFixAndGoodToFix = pages.map(
|
468
|
+
(_, index) =>
|
469
|
+
(typesOfIssuesCountAtMustFix[index] || 0) + (typesOfIssuesCountAtGoodToFix[index] || 0),
|
470
|
+
);
|
471
|
+
|
472
|
+
const { totalRulesMustFix, totalRulesGoodToFix, totalRulesMustFixAndGoodToFix } =
|
473
|
+
await getTotalRulesCount(enableWcagAaa, disableOobee);
|
474
|
+
|
475
|
+
const avgMustFixPerPage =
|
476
|
+
totalPages > 0
|
477
|
+
? typesOfIssuesCountAtMustFix.reduce((sum, count) => sum + count, 0) / totalPages
|
478
|
+
: 0;
|
479
|
+
|
480
|
+
const avgGoodToFixPerPage =
|
481
|
+
totalPages > 0
|
482
|
+
? typesOfIssuesCountAtGoodToFix.reduce((sum, count) => sum + count, 0) / totalPages
|
483
|
+
: 0;
|
484
|
+
|
485
|
+
const avgMustFixAndGoodToFixPerPage =
|
486
|
+
totalPages > 0
|
487
|
+
? typesOfIssuesCountSumMustFixAndGoodToFix.reduce((sum, count) => sum + count, 0) / totalPages
|
488
|
+
: 0;
|
489
|
+
|
490
|
+
const avgTypesOfIssuesPercentageOfTotalRulesAtMustFix =
|
491
|
+
totalRulesMustFix > 0 ? ((avgMustFixPerPage / totalRulesMustFix) * 100).toFixed(2) : '0.00';
|
492
|
+
|
493
|
+
const avgTypesOfIssuesPercentageOfTotalRulesAtGoodToFix =
|
494
|
+
totalRulesGoodToFix > 0
|
495
|
+
? ((avgGoodToFixPerPage / totalRulesGoodToFix) * 100).toFixed(2)
|
496
|
+
: '0.00';
|
497
|
+
|
498
|
+
const avgTypesOfIssuesPercentageOfTotalRulesAtMustFixAndGoodToFix =
|
499
|
+
totalRulesMustFixAndGoodToFix > 0
|
500
|
+
? ((avgMustFixAndGoodToFixPerPage / totalRulesMustFixAndGoodToFix) * 100).toFixed(2)
|
501
|
+
: '0.00';
|
502
|
+
|
503
|
+
const avgTypesOfIssuesCountAtMustFix = avgMustFixPerPage.toFixed(2);
|
504
|
+
const avgTypesOfIssuesCountAtGoodToFix = avgGoodToFixPerPage.toFixed(2);
|
505
|
+
const avgTypesOfIssuesCountAtMustFixAndGoodToFix = avgMustFixAndGoodToFixPerPage.toFixed(2);
|
506
|
+
|
507
|
+
return {
|
508
|
+
avgTypesOfIssuesCountAtMustFix,
|
509
|
+
avgTypesOfIssuesCountAtGoodToFix,
|
510
|
+
avgTypesOfIssuesCountAtMustFixAndGoodToFix,
|
511
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtMustFix,
|
512
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtGoodToFix,
|
513
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtMustFixAndGoodToFix,
|
514
|
+
totalRulesMustFix,
|
515
|
+
totalRulesGoodToFix,
|
516
|
+
totalRulesMustFixAndGoodToFix,
|
517
|
+
pagesAffectedPerRule,
|
518
|
+
pagesPercentageAffectedPerRule,
|
519
|
+
};
|
520
|
+
};
|
521
|
+
|
522
|
+
export const getFormattedTime = (inputDate: Date): string => {
|
232
523
|
if (inputDate) {
|
233
524
|
return inputDate.toLocaleTimeString('en-GB', {
|
234
525
|
year: 'numeric',
|
@@ -250,7 +541,7 @@ export const getFormattedTime = inputDate => {
|
|
250
541
|
});
|
251
542
|
};
|
252
543
|
|
253
|
-
export const formatDateTimeForMassScanner = date => {
|
544
|
+
export const formatDateTimeForMassScanner = (date: Date): string => {
|
254
545
|
// Format date and time parts separately
|
255
546
|
const year = date.getFullYear().toString().slice(-2); // Get the last two digits of the year
|
256
547
|
const month = `0${date.getMonth() + 1}`.slice(-2); // Month is zero-indexed
|
@@ -271,14 +562,13 @@ export const setHeadlessMode = (browser: string, isHeadless: boolean): void => {
|
|
271
562
|
} else {
|
272
563
|
process.env.CRAWLEE_HEADLESS = '0';
|
273
564
|
}
|
274
|
-
|
275
565
|
};
|
276
566
|
|
277
|
-
export const setThresholdLimits = setWarnLevel => {
|
567
|
+
export const setThresholdLimits = (setWarnLevel: string): void => {
|
278
568
|
process.env.WARN_LEVEL = setWarnLevel;
|
279
569
|
};
|
280
570
|
|
281
|
-
export const zipResults = (zipName, resultsPath) => {
|
571
|
+
export const zipResults = (zipName: string, resultsPath: string): void => {
|
282
572
|
// Check prior zip file exist and remove
|
283
573
|
if (fs.existsSync(zipName)) {
|
284
574
|
fs.unlinkSync(zipName);
|
@@ -309,9 +599,9 @@ export const zipResults = (zipName, resultsPath) => {
|
|
309
599
|
|
310
600
|
// areLinksEqual compares 2 string URLs and ignores comparison of 'www.' and url protocol
|
311
601
|
// i.e. 'http://google.com' and 'https://www.google.com' returns true
|
312
|
-
export const areLinksEqual = (link1, link2) => {
|
602
|
+
export const areLinksEqual = (link1: string, link2: string): boolean => {
|
313
603
|
try {
|
314
|
-
const format = link => {
|
604
|
+
const format = (link: string): URL => {
|
315
605
|
return new URL(link.replace(/www\./, ''));
|
316
606
|
};
|
317
607
|
const l1 = format(link1);
|
@@ -336,7 +626,7 @@ export const randomThreeDigitNumberString = () => {
|
|
336
626
|
return String(threeDigitNumber);
|
337
627
|
};
|
338
628
|
|
339
|
-
export const isFollowStrategy = (link1, link2, rule) => {
|
629
|
+
export const isFollowStrategy = (link1: string, link2: string, rule: string): boolean => {
|
340
630
|
const parsedLink1 = new URL(link1);
|
341
631
|
const parsedLink2 = new URL(link2);
|
342
632
|
if (rule === 'same-domain') {
|
@@ -347,17 +637,17 @@ export const isFollowStrategy = (link1, link2, rule) => {
|
|
347
637
|
return parsedLink1.hostname === parsedLink2.hostname;
|
348
638
|
};
|
349
639
|
|
350
|
-
|
351
|
-
export const retryFunction = async (func, maxAttempt) => {
|
640
|
+
export const retryFunction = async <T>(func: () => Promise<T>, maxAttempt: number): Promise<T> => {
|
352
641
|
let attemptCount = 0;
|
353
642
|
while (attemptCount < maxAttempt) {
|
354
643
|
attemptCount += 1;
|
355
644
|
try {
|
645
|
+
// eslint-disable-next-line no-await-in-loop
|
356
646
|
const result = await func();
|
357
647
|
return result;
|
358
648
|
} catch (error) {
|
359
649
|
silentLogger.error(`(Attempt count: ${attemptCount} of ${maxAttempt}) ${error}`);
|
360
650
|
}
|
361
651
|
}
|
652
|
+
throw new Error('Maximum number of attempts reached');
|
362
653
|
};
|
363
|
-
/* eslint-enable no-await-in-loop */
|