@govtechsg/oobee 0.10.39 → 0.10.42
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 +362 -0
- package/package.json +1 -1
- package/src/crawlers/commonCrawlerFunc.ts +29 -1
- package/src/crawlers/crawlDomain.ts +4 -21
- package/src/crawlers/crawlSitemap.ts +1 -1
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +589 -554
- package/src/crawlers/pdfScanFunc.ts +67 -26
- package/src/mergeAxeResults.ts +302 -237
- package/src/screenshotFunc/htmlScreenshotFunc.ts +1 -1
- package/src/screenshotFunc/pdfScreenshotFunc.ts +34 -1
- package/src/utils.ts +289 -13
package/src/utils.ts
CHANGED
@@ -7,7 +7,10 @@ import constants, {
|
|
7
7
|
destinationPath,
|
8
8
|
getIntermediateScreenshotsPath,
|
9
9
|
} from './constants/constants.js';
|
10
|
-
import { silentLogger } from './logs.js';
|
10
|
+
import { consoleLogger, silentLogger } from './logs.js';
|
11
|
+
import { getAxeConfiguration } from './crawlers/custom/getAxeConfiguration.js';
|
12
|
+
import axe from 'axe-core';
|
13
|
+
import { Rule, RuleMetadata } from 'axe-core';
|
11
14
|
|
12
15
|
export const getVersion = () => {
|
13
16
|
const loadJSON = filePath =>
|
@@ -178,18 +181,6 @@ export const cleanUp = async pathToDelete => {
|
|
178
181
|
fs.removeSync(pathToDelete);
|
179
182
|
};
|
180
183
|
|
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
184
|
export const getWcagPassPercentage = (
|
194
185
|
wcagViolations: string[],
|
195
186
|
showEnableWcagAaa: boolean
|
@@ -228,6 +219,291 @@ export const getWcagPassPercentage = (
|
|
228
219
|
};
|
229
220
|
};
|
230
221
|
|
222
|
+
export interface ScanPagesDetail {
|
223
|
+
oobeeAppVersion?: string;
|
224
|
+
pagesAffected: PageDetail[];
|
225
|
+
pagesNotAffected: PageDetail[];
|
226
|
+
scannedPagesCount: number;
|
227
|
+
pagesNotScanned: PageDetail[];
|
228
|
+
pagesNotScannedCount: number;
|
229
|
+
}
|
230
|
+
|
231
|
+
export interface PageDetail {
|
232
|
+
pageTitle: string;
|
233
|
+
url: string;
|
234
|
+
totalOccurrencesFailedIncludingNeedsReview: number;
|
235
|
+
totalOccurrencesFailedExcludingNeedsReview: number;
|
236
|
+
totalOccurrencesMustFix?: number;
|
237
|
+
totalOccurrencesGoodToFix?: number;
|
238
|
+
totalOccurrencesNeedsReview: number;
|
239
|
+
totalOccurrencesPassed: number;
|
240
|
+
occurrencesExclusiveToNeedsReview: boolean;
|
241
|
+
typesOfIssuesCount: number;
|
242
|
+
typesOfIssuesExcludingNeedsReviewCount: number;
|
243
|
+
categoriesPresent: IssueCategory[];
|
244
|
+
conformance?: string[]; // WCAG levels as flexible strings
|
245
|
+
typesOfIssues: IssueDetail[];
|
246
|
+
}
|
247
|
+
|
248
|
+
export type IssueCategory = "mustFix" | "goodToFix" | "needsReview" | "passed";
|
249
|
+
|
250
|
+
export interface IssueDetail {
|
251
|
+
ruleId: string;
|
252
|
+
wcagConformance: string[];
|
253
|
+
occurrencesMustFix?: number;
|
254
|
+
occurrencesGoodToFix?: number;
|
255
|
+
occurrencesNeedsReview?: number;
|
256
|
+
occurrencesPassed: number;
|
257
|
+
}
|
258
|
+
|
259
|
+
export const getProgressPercentage = (
|
260
|
+
scanPagesDetail: ScanPagesDetail,
|
261
|
+
showEnableWcagAaa: boolean
|
262
|
+
): {
|
263
|
+
averageProgressPercentageAA: string;
|
264
|
+
averageProgressPercentageAAandAAA: string;
|
265
|
+
} => {
|
266
|
+
const pages = scanPagesDetail.pagesAffected || [];
|
267
|
+
|
268
|
+
const progressPercentagesAA = pages.map((page: any) => {
|
269
|
+
const violations: string[] = page.conformance;
|
270
|
+
return getWcagPassPercentage(violations, showEnableWcagAaa).passPercentageAA;
|
271
|
+
});
|
272
|
+
|
273
|
+
const progressPercentagesAAandAAA = pages.map((page: any) => {
|
274
|
+
const violations: string[] = page.conformance;
|
275
|
+
return getWcagPassPercentage(violations, showEnableWcagAaa).passPercentageAAandAAA;
|
276
|
+
});
|
277
|
+
|
278
|
+
const totalAA = progressPercentagesAA.reduce((sum, p) => sum + parseFloat(p), 0);
|
279
|
+
const avgAA = progressPercentagesAA.length ? totalAA / progressPercentagesAA.length : 0;
|
280
|
+
|
281
|
+
const totalAAandAAA = progressPercentagesAAandAAA.reduce((sum, p) => sum + parseFloat(p), 0);
|
282
|
+
const avgAAandAAA = progressPercentagesAAandAAA.length ? totalAAandAAA / progressPercentagesAAandAAA.length : 0;
|
283
|
+
|
284
|
+
return {
|
285
|
+
averageProgressPercentageAA: avgAA.toFixed(2),
|
286
|
+
averageProgressPercentageAAandAAA: avgAAandAAA.toFixed(2),
|
287
|
+
};
|
288
|
+
};
|
289
|
+
|
290
|
+
export const getTotalRulesCount = async (
|
291
|
+
enableWcagAaa: boolean,
|
292
|
+
disableOobee: boolean
|
293
|
+
): Promise<{
|
294
|
+
totalRulesMustFix: number;
|
295
|
+
totalRulesGoodToFix: number;
|
296
|
+
totalRulesMustFixAndGoodToFix: number;
|
297
|
+
}> => {
|
298
|
+
const axeConfig = getAxeConfiguration({
|
299
|
+
enableWcagAaa,
|
300
|
+
gradingReadabilityFlag: '',
|
301
|
+
disableOobee,
|
302
|
+
});
|
303
|
+
|
304
|
+
// Get default rules from axe-core
|
305
|
+
const defaultRules = await axe.getRules();
|
306
|
+
|
307
|
+
// Merge custom rules with default rules, converting RuleMetadata to Rule
|
308
|
+
const mergedRules: Rule[] = defaultRules.map((defaultRule) => {
|
309
|
+
const customRule = axeConfig.rules.find((r) => r.id === defaultRule.ruleId);
|
310
|
+
if (customRule) {
|
311
|
+
// Merge properties from customRule into defaultRule (RuleMetadata) to create a Rule
|
312
|
+
return {
|
313
|
+
id: defaultRule.ruleId,
|
314
|
+
enabled: customRule.enabled,
|
315
|
+
selector: customRule.selector,
|
316
|
+
any: customRule.any,
|
317
|
+
tags: defaultRule.tags,
|
318
|
+
metadata: customRule.metadata, // Use custom metadata if it exists
|
319
|
+
};
|
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
|
+
}
|
329
|
+
});
|
330
|
+
|
331
|
+
// Add any custom rules that don't override the default rules
|
332
|
+
axeConfig.rules.forEach(customRule => {
|
333
|
+
if (!mergedRules.some(mergedRule => mergedRule.id === customRule.id)) {
|
334
|
+
// Ensure customRule is of type Rule
|
335
|
+
const rule: Rule = {
|
336
|
+
id: customRule.id,
|
337
|
+
enabled: customRule.enabled,
|
338
|
+
selector: customRule.selector,
|
339
|
+
any: customRule.any,
|
340
|
+
tags: customRule.tags,
|
341
|
+
metadata: customRule.metadata,
|
342
|
+
// Add other properties if needed
|
343
|
+
};
|
344
|
+
mergedRules.push(rule);
|
345
|
+
}
|
346
|
+
});
|
347
|
+
|
348
|
+
// Apply the merged configuration to axe-core
|
349
|
+
await axe.configure({ ...axeConfig, rules: mergedRules });
|
350
|
+
|
351
|
+
const rules = await axe.getRules();
|
352
|
+
|
353
|
+
// ... (rest of your logic)
|
354
|
+
let totalRulesMustFix = 0;
|
355
|
+
let totalRulesGoodToFix = 0;
|
356
|
+
|
357
|
+
const wcagRegex = /^wcag\d+a+$/;
|
358
|
+
|
359
|
+
// Use mergedRules instead of rules to check enabled property
|
360
|
+
mergedRules.forEach((rule) => {
|
361
|
+
if (!rule.enabled) {
|
362
|
+
return;
|
363
|
+
}
|
364
|
+
|
365
|
+
if (rule.id === 'frame-tested') return; // Ignore 'frame-tested' rule
|
366
|
+
|
367
|
+
const tags = rule.tags || [];
|
368
|
+
|
369
|
+
// Skip experimental and deprecated rules
|
370
|
+
if (tags.includes('experimental') || tags.includes('deprecated')) {
|
371
|
+
return;
|
372
|
+
}
|
373
|
+
|
374
|
+
let conformance = tags.filter(tag => tag.startsWith('wcag') || tag === 'best-practice');
|
375
|
+
|
376
|
+
// Ensure conformance level is sorted correctly
|
377
|
+
if (conformance.length > 0 && conformance[0] !== 'best-practice' && !wcagRegex.test(conformance[0])) {
|
378
|
+
conformance.sort((a, b) => {
|
379
|
+
if (wcagRegex.test(a) && !wcagRegex.test(b)) {
|
380
|
+
return -1;
|
381
|
+
}
|
382
|
+
if (!wcagRegex.test(a) && wcagRegex.test(b)) {
|
383
|
+
return 1;
|
384
|
+
}
|
385
|
+
return 0;
|
386
|
+
});
|
387
|
+
}
|
388
|
+
|
389
|
+
if (conformance.includes('best-practice')) {
|
390
|
+
// console.log(`${totalRulesMustFix} Good To Fix: ${rule.id}`);
|
391
|
+
|
392
|
+
totalRulesGoodToFix++; // Categorized as "Good to Fix"
|
393
|
+
} else {
|
394
|
+
// console.log(`${totalRulesMustFix} Must Fix: ${rule.id}`);
|
395
|
+
|
396
|
+
totalRulesMustFix++; // Otherwise, it's "Must Fix"
|
397
|
+
}
|
398
|
+
});
|
399
|
+
|
400
|
+
return {
|
401
|
+
totalRulesMustFix,
|
402
|
+
totalRulesGoodToFix,
|
403
|
+
totalRulesMustFixAndGoodToFix: totalRulesMustFix + totalRulesGoodToFix,
|
404
|
+
};
|
405
|
+
};
|
406
|
+
|
407
|
+
export const getIssuesPercentage = async (
|
408
|
+
scanPagesDetail: ScanPagesDetail,
|
409
|
+
enableWcagAaa: boolean,
|
410
|
+
disableOobee: boolean
|
411
|
+
): Promise<{
|
412
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtMustFix: string;
|
413
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtGoodToFix: string;
|
414
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtMustFixAndGoodToFix: string;
|
415
|
+
totalRulesMustFix: number;
|
416
|
+
totalRulesGoodToFix: number;
|
417
|
+
totalRulesMustFixAndGoodToFix: number;
|
418
|
+
avgTypesOfIssuesCountAtMustFix: string;
|
419
|
+
avgTypesOfIssuesCountAtGoodToFix: string;
|
420
|
+
avgTypesOfIssuesCountAtMustFixAndGoodToFix: string;
|
421
|
+
pagesAffectedPerRule: Record<string, number>;
|
422
|
+
pagesPercentageAffectedPerRule: Record<string, string>;
|
423
|
+
}> => {
|
424
|
+
const pages = scanPagesDetail.pagesAffected || [];
|
425
|
+
const totalPages = pages.length;
|
426
|
+
|
427
|
+
const pagesAffectedPerRule: Record<string, number> = {};
|
428
|
+
|
429
|
+
pages.forEach((page) => {
|
430
|
+
page.typesOfIssues.forEach((issue) => {
|
431
|
+
if ((issue.occurrencesMustFix || issue.occurrencesGoodToFix) > 0) {
|
432
|
+
pagesAffectedPerRule[issue.ruleId] = (pagesAffectedPerRule[issue.ruleId] || 0) + 1;
|
433
|
+
}
|
434
|
+
});
|
435
|
+
});
|
436
|
+
|
437
|
+
const pagesPercentageAffectedPerRule: Record<string, string> = {};
|
438
|
+
for (const [ruleId, count] of Object.entries(pagesAffectedPerRule)) {
|
439
|
+
pagesPercentageAffectedPerRule[ruleId] = totalPages > 0 ? ((count / totalPages) * 100).toFixed(2) : "0.00";
|
440
|
+
}
|
441
|
+
|
442
|
+
const typesOfIssuesCountAtMustFix = pages.map((page) =>
|
443
|
+
page.typesOfIssues.filter((issue) => (issue.occurrencesMustFix || 0) > 0).length
|
444
|
+
);
|
445
|
+
|
446
|
+
const typesOfIssuesCountAtGoodToFix = pages.map((page) =>
|
447
|
+
page.typesOfIssues.filter((issue) => (issue.occurrencesGoodToFix || 0) > 0).length
|
448
|
+
);
|
449
|
+
|
450
|
+
const typesOfIssuesCountSumMustFixAndGoodToFix = pages.map(
|
451
|
+
(_, index) =>
|
452
|
+
(typesOfIssuesCountAtMustFix[index] || 0) +
|
453
|
+
(typesOfIssuesCountAtGoodToFix[index] || 0)
|
454
|
+
);
|
455
|
+
|
456
|
+
const { totalRulesMustFix, totalRulesGoodToFix, totalRulesMustFixAndGoodToFix } = await getTotalRulesCount(
|
457
|
+
enableWcagAaa,
|
458
|
+
disableOobee
|
459
|
+
);
|
460
|
+
|
461
|
+
const avgMustFixPerPage = totalPages > 0
|
462
|
+
? typesOfIssuesCountAtMustFix.reduce((sum, count) => sum + count, 0) / totalPages
|
463
|
+
: 0;
|
464
|
+
|
465
|
+
const avgGoodToFixPerPage = totalPages > 0
|
466
|
+
? typesOfIssuesCountAtGoodToFix.reduce((sum, count) => sum + count, 0) / totalPages
|
467
|
+
: 0;
|
468
|
+
|
469
|
+
const avgMustFixAndGoodToFixPerPage = totalPages > 0
|
470
|
+
? typesOfIssuesCountSumMustFixAndGoodToFix.reduce((sum, count) => sum + count, 0) / totalPages
|
471
|
+
: 0;
|
472
|
+
|
473
|
+
const avgTypesOfIssuesPercentageOfTotalRulesAtMustFix =
|
474
|
+
totalRulesMustFix > 0
|
475
|
+
? ((avgMustFixPerPage / totalRulesMustFix) * 100).toFixed(2)
|
476
|
+
: "0.00";
|
477
|
+
|
478
|
+
const avgTypesOfIssuesPercentageOfTotalRulesAtGoodToFix =
|
479
|
+
totalRulesGoodToFix > 0
|
480
|
+
? ((avgGoodToFixPerPage / totalRulesGoodToFix) * 100).toFixed(2)
|
481
|
+
: "0.00";
|
482
|
+
|
483
|
+
const avgTypesOfIssuesPercentageOfTotalRulesAtMustFixAndGoodToFix =
|
484
|
+
totalRulesMustFixAndGoodToFix > 0
|
485
|
+
? ((avgMustFixAndGoodToFixPerPage / totalRulesMustFixAndGoodToFix) * 100).toFixed(2)
|
486
|
+
: "0.00";
|
487
|
+
|
488
|
+
const avgTypesOfIssuesCountAtMustFix = avgMustFixPerPage.toFixed(2);
|
489
|
+
const avgTypesOfIssuesCountAtGoodToFix = avgGoodToFixPerPage.toFixed(2);
|
490
|
+
const avgTypesOfIssuesCountAtMustFixAndGoodToFix = avgMustFixAndGoodToFixPerPage.toFixed(2);
|
491
|
+
|
492
|
+
return {
|
493
|
+
avgTypesOfIssuesCountAtMustFix,
|
494
|
+
avgTypesOfIssuesCountAtGoodToFix,
|
495
|
+
avgTypesOfIssuesCountAtMustFixAndGoodToFix,
|
496
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtMustFix,
|
497
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtGoodToFix,
|
498
|
+
avgTypesOfIssuesPercentageOfTotalRulesAtMustFixAndGoodToFix,
|
499
|
+
totalRulesMustFix,
|
500
|
+
totalRulesGoodToFix,
|
501
|
+
totalRulesMustFixAndGoodToFix,
|
502
|
+
pagesAffectedPerRule,
|
503
|
+
pagesPercentageAffectedPerRule,
|
504
|
+
};
|
505
|
+
};
|
506
|
+
|
231
507
|
export const getFormattedTime = inputDate => {
|
232
508
|
if (inputDate) {
|
233
509
|
return inputDate.toLocaleTimeString('en-GB', {
|