@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/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', {