@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.
@@ -19,8 +19,10 @@ import {
19
19
  getStoragePath,
20
20
  getVersion,
21
21
  getWcagPassPercentage,
22
+ getProgressPercentage,
22
23
  retryFunction,
23
24
  zipResults,
25
+ getIssuesPercentage,
24
26
  } from './utils.js';
25
27
  import { consoleLogger, silentLogger } from './logs.js';
26
28
  import itemTypeDescription from './constants/itemTypeDescription.js';
@@ -100,6 +102,13 @@ type AllIssues = {
100
102
  wcagLinks: { [key: string]: string };
101
103
  [key: string]: any;
102
104
  advancedScanOptionsSummaryItems: { [key: string]: boolean };
105
+ scanPagesDetail: {
106
+ pagesAffected: any[];
107
+ pagesNotAffected: any[];
108
+ scannedPagesCount: number;
109
+ pagesNotScanned: any[];
110
+ pagesNotScannedCount: number;
111
+ };
103
112
  };
104
113
 
105
114
  const filename = fileURLToPath(import.meta.url);
@@ -815,6 +824,8 @@ const writeJsonAndBase64Files = async (
815
824
  pagesNotScanned,
816
825
  wcagLinks,
817
826
  wcagPassPercentage,
827
+ progressPercentage,
828
+ issuesPercentage,
818
829
  totalPagesScanned,
819
830
  totalPagesNotScanned,
820
831
  topTenIssues,
@@ -827,6 +838,8 @@ const writeJsonAndBase64Files = async (
827
838
  pagesNotScanned,
828
839
  wcagLinks,
829
840
  wcagPassPercentage,
841
+ progressPercentage,
842
+ issuesPercentage,
830
843
  totalPagesScanned,
831
844
  totalPagesNotScanned,
832
845
  topTenIssues,
@@ -836,6 +849,7 @@ const writeJsonAndBase64Files = async (
836
849
  jsonFilePath: scanItemsMiniReportJsonFilePath,
837
850
  base64FilePath: scanItemsMiniReportBase64FilePath,
838
851
  } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItemsMini }, storagePath, 'scanItemsSummaryMiniReport');
852
+
839
853
  const summaryItems = {
840
854
  mustFix: {
841
855
  totalItems: items.mustFix?.totalItems || 0,
@@ -852,6 +866,8 @@ const writeJsonAndBase64Files = async (
852
866
  topTenPagesWithMostIssues,
853
867
  wcagLinks,
854
868
  wcagPassPercentage,
869
+ progressPercentage,
870
+ issuesPercentage,
855
871
  totalPagesScanned,
856
872
  totalPagesNotScanned,
857
873
  topTenIssues,
@@ -862,247 +878,15 @@ const writeJsonAndBase64Files = async (
862
878
  base64FilePath: scanItemsSummaryBase64FilePath,
863
879
  } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItems }, storagePath, 'scanItemsSummary');
864
880
 
865
- // -----------------------------------------------------------------------------
866
- // --- Scan Pages Summary and Scan Pages Detail ---
867
- // -----------------------------------------------------------------------------
868
-
869
- // 1) Gather your "scanned" pages from allIssues
870
- const allScannedPages = Array.isArray(allIssues.pagesScanned)
871
- ? allIssues.pagesScanned
872
- : [];
873
-
874
- // Define which categories map to which occurrence property
875
- const mustFixCategory = "mustFix"; // => occurrencesMustFix
876
- const goodToFixCategory = "goodToFix"; // => occurrencesGoodToFix
877
- const needsReviewCategory = "needsReview"; // => occurrencesNeedsReview
878
- const passedCategory = "passed"; // => occurrencesPassed
879
-
880
- type RuleData = {
881
- ruleId: string;
882
- wagConformance: string[];
883
- occurrencesMustFix: number;
884
- occurrencesGoodToFix: number;
885
- occurrencesNeedsReview: number;
886
- occurrencesPassed: number;
887
- };
888
-
889
- type PageData = {
890
- pageTitle: string;
891
- url: string;
892
- // Summaries
893
- totalOccurrencesFailedIncludingNeedsReview: number; // mustFix + goodToFix + needsReview
894
- totalOccurrencesFailedExcludingNeedsReview: number; // mustFix + goodToFix
895
- totalOccurrencesNeedsReview: number; // needsReview
896
- totalOccurrencesPassed: number; // passed only
897
- typesOfIssues: Record<string, RuleData>;
898
- };
899
-
900
- // 2) We'll accumulate pages in a map keyed by URL
901
- const pagesMap: Record<string, PageData> = {};
902
-
903
- // 3) Build pagesMap by iterating over each category in allIssues.items
904
- Object.entries(allIssues.items).forEach(([categoryName, categoryData]) => {
905
- if (!categoryData?.rules) return; // no rules in this category? skip
906
-
907
- categoryData.rules.forEach((rule) => {
908
- const { rule: ruleId, conformance = [] } = rule;
909
-
910
- rule.pagesAffected.forEach((p) => {
911
- const { url, pageTitle, itemsCount = 0 } = p;
912
-
913
- // Ensure the page is in pagesMap
914
- if (!pagesMap[url]) {
915
- pagesMap[url] = {
916
- pageTitle,
917
- url,
918
- totalOccurrencesFailedIncludingNeedsReview: 0,
919
- totalOccurrencesFailedExcludingNeedsReview: 0,
920
- totalOccurrencesNeedsReview: 0,
921
- totalOccurrencesPassed: 0,
922
- typesOfIssues: {},
923
- };
924
- }
925
-
926
- // Ensure the rule is present for this page
927
- if (!pagesMap[url].typesOfIssues[ruleId]) {
928
- pagesMap[url].typesOfIssues[ruleId] = {
929
- ruleId,
930
- wagConformance: conformance,
931
- occurrencesMustFix: 0,
932
- occurrencesGoodToFix: 0,
933
- occurrencesNeedsReview: 0,
934
- occurrencesPassed: 0,
935
- };
936
- }
937
-
938
- // Depending on the category, increment the relevant occurrence counts
939
- if (categoryName === mustFixCategory) {
940
- pagesMap[url].typesOfIssues[ruleId].occurrencesMustFix += itemsCount;
941
- pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
942
- pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
943
- } else if (categoryName === goodToFixCategory) {
944
- pagesMap[url].typesOfIssues[ruleId].occurrencesGoodToFix += itemsCount;
945
- pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
946
- pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
947
- } else if (categoryName === needsReviewCategory) {
948
- pagesMap[url].typesOfIssues[ruleId].occurrencesNeedsReview += itemsCount;
949
- pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
950
- pagesMap[url].totalOccurrencesNeedsReview += itemsCount;
951
- } else if (categoryName === passedCategory) {
952
- pagesMap[url].typesOfIssues[ruleId].occurrencesPassed += itemsCount;
953
- pagesMap[url].totalOccurrencesPassed += itemsCount;
954
- }
955
- });
956
- });
957
- });
958
-
959
- // 4) Separate scanned pages into “affected” vs. “notAffected”
960
- // - "affected" => totalOccurrencesFailedIncludingNeedsReview > 0
961
- // - "notAffected" => totalOccurrencesFailedIncludingNeedsReview = 0 (only passed issues)
962
- // or pages that never appeared in pagesMap at all
963
-
964
- const pagesInMap = Object.values(pagesMap); // All pages that have some record in pagesMap
965
- const pagesInMapUrls = new Set(Object.keys(pagesMap));
966
-
967
- // (a) Pages that appear in pagesMap BUT have only passed (no mustFix/goodToFix/needsReview)
968
- const pagesAllPassed = pagesInMap.filter(
969
- (p) => p.totalOccurrencesFailedIncludingNeedsReview === 0
970
- );
971
-
972
- // (b) Pages that do NOT appear in pagesMap at all => scanned but no items found
973
- // (This can happen if a page had 0 occurrences across all categories.)
974
- const pagesNoEntries = allScannedPages
975
- .filter((sp) => !pagesInMapUrls.has(sp.url))
976
- .map((sp) => ({
977
- // We'll create a PageData with everything zeroed out
978
- pageTitle: sp.pageTitle,
979
- url: sp.url,
980
- totalOccurrencesFailedIncludingNeedsReview: 0,
981
- totalOccurrencesFailedExcludingNeedsReview: 0,
982
- totalOccurrencesNeedsReview: 0,
983
- totalOccurrencesPassed: 0,
984
- typesOfIssues: {},
985
- }));
986
-
987
- // Combine these into "notAffected"
988
- const pagesNotAffectedRaw = [...pagesAllPassed, ...pagesNoEntries];
989
-
990
- // "affected" pages => have at least 1 mustFix/goodToFix/needsReview
991
- const pagesAffectedRaw = pagesInMap.filter(
992
- (p) => p.totalOccurrencesFailedIncludingNeedsReview > 0
993
- );
994
-
995
- // 5) Transform both arrays to final shapes
996
-
997
- function transformPageData(page: PageData) {
998
- const typesOfIssuesArray = Object.values(page.typesOfIssues);
999
-
1000
- // Compute sums for each failing category
1001
- const mustFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesMustFix, 0);
1002
- const goodToFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesGoodToFix, 0);
1003
- const needsReviewSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesNeedsReview, 0);
1004
-
1005
- // Build categoriesPresent based on nonzero failing counts
1006
- const categoriesPresent: string[] = [];
1007
- if (mustFixSum > 0) categoriesPresent.push("mustFix");
1008
- if (goodToFixSum > 0) categoriesPresent.push("goodToFix");
1009
- if (needsReviewSum > 0) categoriesPresent.push("needsReview");
1010
-
1011
- // Count how many rules have failing issues (either mustFix or goodToFix)
1012
- const failedRuleCount = typesOfIssuesArray.filter(
1013
- (r) => (r.occurrencesMustFix || 0) + (r.occurrencesGoodToFix || 0) > 0
1014
- ).length;
1015
-
1016
- const typesOfIssuesExcludingNeedsReviewCount = failedRuleCount;
1017
- const occurrencesExclusiveToNeedsReview =
1018
- page.totalOccurrencesFailedExcludingNeedsReview === 0 &&
1019
- page.totalOccurrencesFailedIncludingNeedsReview > 0;
1020
-
1021
- // Aggregate wcag conformance values only for rules with failing issues.
1022
- const allConformance = typesOfIssuesArray.reduce((acc, curr) => {
1023
- const nonPassedCount =
1024
- (curr.occurrencesMustFix || 0) +
1025
- (curr.occurrencesGoodToFix || 0) +
1026
- (curr.occurrencesNeedsReview || 0);
1027
- if (nonPassedCount > 0) {
1028
- return acc.concat(curr.wagConformance || []);
1029
- }
1030
- return acc;
1031
- }, []);
1032
- // Remove duplicates.
1033
- const conformance = Array.from(new Set(allConformance));
1034
-
1035
- return {
1036
- pageTitle: page.pageTitle,
1037
- url: page.url,
1038
- totalOccurrencesFailedIncludingNeedsReview: page.totalOccurrencesFailedIncludingNeedsReview,
1039
- totalOccurrencesFailedExcludingNeedsReview: page.totalOccurrencesFailedExcludingNeedsReview,
1040
- totalOccurrencesMustFix: mustFixSum,
1041
- totalOccurrencesGoodToFix: goodToFixSum,
1042
- totalOccurrencesNeedsReview: needsReviewSum,
1043
- totalOccurrencesPassed: page.totalOccurrencesPassed,
1044
- occurrencesExclusiveToNeedsReview,
1045
- typesOfIssuesCount: failedRuleCount,
1046
- typesOfIssuesExcludingNeedsReviewCount,
1047
- categoriesPresent,
1048
- conformance,
1049
- typesOfIssues: typesOfIssuesArray, // full details for scanPagesDetail
1050
- };
1051
- }
1052
-
1053
- const pagesAffected = pagesAffectedRaw.map(transformPageData);
1054
- const pagesNotAffected = pagesNotAffectedRaw.map(transformPageData);
1055
-
1056
- // 6) SORT pages by typesOfIssuesCount (descending) for both arrays
1057
- pagesAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
1058
- pagesNotAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
1059
-
1060
- // 7) Compute scanned/ skipped counts
1061
- const scannedPagesCount = pagesAffected.length + pagesNotAffected.length;
1062
- const pagesNotScannedCount = Array.isArray(allIssues.pagesNotScanned)
1063
- ? allIssues.pagesNotScanned.length
1064
- : 0;
1065
-
1066
- // 8) Build scanPagesDetail (keeping full typesOfIssues)
1067
- const scanPagesDetail = {
1068
- pagesAffected,
1069
- pagesNotAffected, // these pages are scanned but have no "fail/review" issues
1070
- scannedPagesCount,
1071
- pagesNotScanned: Array.isArray(allIssues.pagesNotScanned)
1072
- ? allIssues.pagesNotScanned
1073
- : [],
1074
- pagesNotScannedCount,
1075
- };
1076
-
1077
- // 9) Build scanPagesSummary (remove “typesOfIssues” from both groups, but keep other fields)
1078
- function stripTypesOfIssues(page: ReturnType<typeof transformPageData>) {
1079
- const { typesOfIssues, ...rest } = page;
1080
- return rest;
1081
- }
1082
-
1083
- const summaryPagesAffected = pagesAffected.map(stripTypesOfIssues);
1084
- const summaryPagesNotAffected = pagesNotAffected.map(stripTypesOfIssues);
1085
-
1086
- const scanPagesSummary = {
1087
- pagesAffected: summaryPagesAffected,
1088
- pagesNotAffected: summaryPagesNotAffected,
1089
- scannedPagesCount,
1090
- pagesNotScanned: Array.isArray(allIssues.pagesNotScanned)
1091
- ? allIssues.pagesNotScanned
1092
- : [],
1093
- pagesNotScannedCount,
1094
- };
1095
-
1096
- // 10) Write out the detail and summary JSON files
1097
881
  const {
1098
882
  jsonFilePath: scanPagesDetailJsonFilePath,
1099
883
  base64FilePath: scanPagesDetailBase64FilePath
1100
- } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanPagesDetail }, storagePath, 'scanPagesDetail');
884
+ } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesDetail }, storagePath, 'scanPagesDetail');
1101
885
 
1102
886
  const {
1103
887
  jsonFilePath: scanPagesSummaryJsonFilePath,
1104
888
  base64FilePath: scanPagesSummaryBase64FilePath
1105
- } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanPagesSummary }, storagePath, 'scanPagesSummary');
889
+ } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesSummary }, storagePath, 'scanPagesSummary');
1106
890
 
1107
891
  return {
1108
892
  scanDataJsonFilePath,
@@ -1442,6 +1226,272 @@ const moveElemScreenshots = (randomToken: string, storagePath: string) => {
1442
1226
  }
1443
1227
  };
1444
1228
 
1229
+ /**
1230
+ * Build allIssues.scanPagesDetail and allIssues.scanPagesSummary
1231
+ * by analyzing pagesScanned (including mustFix/goodToFix/etc.).
1232
+ */
1233
+ function populateScanPagesDetail(allIssues: AllIssues): void {
1234
+ // --------------------------------------------
1235
+ // 1) Gather your "scanned" pages from allIssues
1236
+ // --------------------------------------------
1237
+ const allScannedPages = Array.isArray(allIssues.pagesScanned)
1238
+ ? allIssues.pagesScanned
1239
+ : [];
1240
+
1241
+ // --------------------------------------------
1242
+ // 2) Define category constants (optional, just for clarity)
1243
+ // --------------------------------------------
1244
+ const mustFixCategory = "mustFix";
1245
+ const goodToFixCategory = "goodToFix";
1246
+ const needsReviewCategory = "needsReview";
1247
+ const passedCategory = "passed";
1248
+
1249
+ // --------------------------------------------
1250
+ // 3) Set up type declarations (if you want them local to this function)
1251
+ // --------------------------------------------
1252
+ type RuleData = {
1253
+ ruleId: string;
1254
+ wcagConformance: string[];
1255
+ occurrencesMustFix: number;
1256
+ occurrencesGoodToFix: number;
1257
+ occurrencesNeedsReview: number;
1258
+ occurrencesPassed: number;
1259
+ };
1260
+
1261
+ type PageData = {
1262
+ pageTitle: string;
1263
+ url: string;
1264
+ // Summaries
1265
+ totalOccurrencesFailedIncludingNeedsReview: number; // mustFix + goodToFix + needsReview
1266
+ totalOccurrencesFailedExcludingNeedsReview: number; // mustFix + goodToFix
1267
+ totalOccurrencesNeedsReview: number; // needsReview
1268
+ totalOccurrencesPassed: number; // passed only
1269
+ typesOfIssues: Record<string, RuleData>;
1270
+ };
1271
+
1272
+ // --------------------------------------------
1273
+ // 4) We'll accumulate pages in a map keyed by URL
1274
+ // --------------------------------------------
1275
+ const pagesMap: Record<string, PageData> = {};
1276
+
1277
+ // --------------------------------------------
1278
+ // 5) Build pagesMap by iterating over each category in allIssues.items
1279
+ // --------------------------------------------
1280
+ Object.entries(allIssues.items).forEach(([categoryName, categoryData]) => {
1281
+ if (!categoryData?.rules) return; // no rules in this category? skip
1282
+
1283
+ categoryData.rules.forEach(rule => {
1284
+ const { rule: ruleId, conformance = [] } = rule;
1285
+
1286
+ rule.pagesAffected.forEach(p => {
1287
+ const { url, pageTitle, items = [] } = p;
1288
+ const itemsCount = items.length;
1289
+
1290
+ // Ensure the page is in pagesMap
1291
+ if (!pagesMap[url]) {
1292
+ pagesMap[url] = {
1293
+ pageTitle,
1294
+ url,
1295
+ totalOccurrencesFailedIncludingNeedsReview: 0,
1296
+ totalOccurrencesFailedExcludingNeedsReview: 0,
1297
+ totalOccurrencesNeedsReview: 0,
1298
+ totalOccurrencesPassed: 0,
1299
+ typesOfIssues: {},
1300
+ };
1301
+ }
1302
+
1303
+ // Ensure the rule is present for this page
1304
+ if (!pagesMap[url].typesOfIssues[ruleId]) {
1305
+ pagesMap[url].typesOfIssues[ruleId] = {
1306
+ ruleId,
1307
+ wcagConformance: conformance,
1308
+ occurrencesMustFix: 0,
1309
+ occurrencesGoodToFix: 0,
1310
+ occurrencesNeedsReview: 0,
1311
+ occurrencesPassed: 0,
1312
+ };
1313
+ }
1314
+
1315
+ // Depending on the category, increment the relevant occurrence counts
1316
+ if (categoryName === mustFixCategory) {
1317
+ pagesMap[url].typesOfIssues[ruleId].occurrencesMustFix += itemsCount;
1318
+ pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1319
+ pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
1320
+ } else if (categoryName === goodToFixCategory) {
1321
+ pagesMap[url].typesOfIssues[ruleId].occurrencesGoodToFix += itemsCount;
1322
+ pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1323
+ pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
1324
+ } else if (categoryName === needsReviewCategory) {
1325
+ pagesMap[url].typesOfIssues[ruleId].occurrencesNeedsReview += itemsCount;
1326
+ pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1327
+ pagesMap[url].totalOccurrencesNeedsReview += itemsCount;
1328
+ } else if (categoryName === passedCategory) {
1329
+ pagesMap[url].typesOfIssues[ruleId].occurrencesPassed += itemsCount;
1330
+ pagesMap[url].totalOccurrencesPassed += itemsCount;
1331
+ }
1332
+ });
1333
+ });
1334
+ });
1335
+
1336
+ // --------------------------------------------
1337
+ // 6) Separate scanned pages into “affected” vs. “notAffected”
1338
+ // --------------------------------------------
1339
+ const pagesInMap = Object.values(pagesMap); // All pages that have some record in pagesMap
1340
+ const pagesInMapUrls = new Set(Object.keys(pagesMap));
1341
+
1342
+ // (a) Pages with only passed (no mustFix/goodToFix/needsReview)
1343
+ const pagesAllPassed = pagesInMap.filter(
1344
+ p => p.totalOccurrencesFailedIncludingNeedsReview === 0
1345
+ );
1346
+
1347
+ // (b) Pages that do NOT appear in pagesMap at all => scanned but no items found
1348
+ const pagesNoEntries = allScannedPages
1349
+ .filter(sp => !pagesInMapUrls.has(sp.url))
1350
+ .map(sp => ({
1351
+ pageTitle: sp.pageTitle,
1352
+ url: sp.url,
1353
+ totalOccurrencesFailedIncludingNeedsReview: 0,
1354
+ totalOccurrencesFailedExcludingNeedsReview: 0,
1355
+ totalOccurrencesNeedsReview: 0,
1356
+ totalOccurrencesPassed: 0,
1357
+ typesOfIssues: {},
1358
+ }));
1359
+
1360
+ // Combine these into "notAffected"
1361
+ const pagesNotAffectedRaw = [...pagesAllPassed, ...pagesNoEntries];
1362
+
1363
+ // "affected" pages => have at least 1 mustFix/goodToFix/needsReview
1364
+ const pagesAffectedRaw = pagesInMap.filter(
1365
+ p => p.totalOccurrencesFailedIncludingNeedsReview > 0
1366
+ );
1367
+
1368
+ // --------------------------------------------
1369
+ // 7) Transform both arrays to the final shape
1370
+ // --------------------------------------------
1371
+ function transformPageData(page: PageData) {
1372
+ const typesOfIssuesArray = Object.values(page.typesOfIssues);
1373
+
1374
+ // Compute sums for each failing category
1375
+ const mustFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesMustFix, 0);
1376
+ const goodToFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesGoodToFix, 0);
1377
+ const needsReviewSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesNeedsReview, 0);
1378
+
1379
+ // Build categoriesPresent based on nonzero failing counts
1380
+ const categoriesPresent: string[] = [];
1381
+ if (mustFixSum > 0) categoriesPresent.push("mustFix");
1382
+ if (goodToFixSum > 0) categoriesPresent.push("goodToFix");
1383
+ if (needsReviewSum > 0) categoriesPresent.push("needsReview");
1384
+
1385
+ // Count how many rules have failing issues
1386
+ const failedRuleIds = new Set<string>();
1387
+ typesOfIssuesArray.forEach(r => {
1388
+ if ((r.occurrencesMustFix || 0) > 0 ||
1389
+ (r.occurrencesGoodToFix || 0) > 0 ||
1390
+ (r.occurrencesNeedsReview || 0) > 0) {
1391
+ failedRuleIds.add(r.ruleId); // Ensure ruleId is unique
1392
+ }
1393
+ });
1394
+ const failedRuleCount = failedRuleIds.size;
1395
+
1396
+ // Possibly these two for future convenience
1397
+ const typesOfIssuesExcludingNeedsReviewCount = typesOfIssuesArray.filter(
1398
+ r => (r.occurrencesMustFix || 0) + (r.occurrencesGoodToFix || 0) > 0
1399
+ ).length;
1400
+
1401
+ const typesOfIssuesExclusiveToNeedsReviewCount = typesOfIssuesArray.filter(
1402
+ r =>
1403
+ (r.occurrencesNeedsReview || 0) > 0 &&
1404
+ (r.occurrencesMustFix || 0) === 0 &&
1405
+ (r.occurrencesGoodToFix || 0) === 0
1406
+ ).length;
1407
+
1408
+ // Aggregate wcagConformance for rules that actually fail
1409
+ const allConformance = typesOfIssuesArray.reduce((acc, curr) => {
1410
+ const nonPassedCount =
1411
+ (curr.occurrencesMustFix || 0) +
1412
+ (curr.occurrencesGoodToFix || 0) +
1413
+ (curr.occurrencesNeedsReview || 0);
1414
+
1415
+ if (nonPassedCount > 0) {
1416
+ return acc.concat(curr.wcagConformance || []);
1417
+ }
1418
+ return acc;
1419
+ }, [] as string[]);
1420
+ // Remove duplicates
1421
+ const conformance = Array.from(new Set(allConformance));
1422
+
1423
+ return {
1424
+ pageTitle: page.pageTitle,
1425
+ url: page.url,
1426
+ totalOccurrencesFailedIncludingNeedsReview: page.totalOccurrencesFailedIncludingNeedsReview,
1427
+ totalOccurrencesFailedExcludingNeedsReview: page.totalOccurrencesFailedExcludingNeedsReview,
1428
+ totalOccurrencesMustFix: mustFixSum,
1429
+ totalOccurrencesGoodToFix: goodToFixSum,
1430
+ totalOccurrencesNeedsReview: needsReviewSum,
1431
+ totalOccurrencesPassed: page.totalOccurrencesPassed,
1432
+ typesOfIssuesExclusiveToNeedsReviewCount,
1433
+ typesOfIssuesCount: failedRuleCount,
1434
+ typesOfIssuesExcludingNeedsReviewCount,
1435
+ categoriesPresent,
1436
+ conformance,
1437
+ // Keep full detail for "scanPagesDetail"
1438
+ typesOfIssues: typesOfIssuesArray,
1439
+ };
1440
+ }
1441
+
1442
+ // Transform raw pages
1443
+ const pagesAffected = pagesAffectedRaw.map(transformPageData);
1444
+ const pagesNotAffected = pagesNotAffectedRaw.map(transformPageData);
1445
+
1446
+ // --------------------------------------------
1447
+ // 8) Sort pages by typesOfIssuesCount (descending) for both arrays
1448
+ // --------------------------------------------
1449
+ pagesAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
1450
+ pagesNotAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
1451
+
1452
+ // --------------------------------------------
1453
+ // 9) Compute scanned/ skipped counts
1454
+ // --------------------------------------------
1455
+ const scannedPagesCount = pagesAffected.length + pagesNotAffected.length;
1456
+ const pagesNotScannedCount = Array.isArray(allIssues.pagesNotScanned)
1457
+ ? allIssues.pagesNotScanned.length
1458
+ : 0;
1459
+
1460
+ // --------------------------------------------
1461
+ // 10) Build scanPagesDetail (with full "typesOfIssues")
1462
+ // --------------------------------------------
1463
+ allIssues.scanPagesDetail = {
1464
+ pagesAffected,
1465
+ pagesNotAffected,
1466
+ scannedPagesCount,
1467
+ pagesNotScanned: Array.isArray(allIssues.pagesNotScanned)
1468
+ ? allIssues.pagesNotScanned
1469
+ : [],
1470
+ pagesNotScannedCount,
1471
+ };
1472
+
1473
+ // --------------------------------------------
1474
+ // 11) Build scanPagesSummary (strip out "typesOfIssues")
1475
+ // --------------------------------------------
1476
+ function stripTypesOfIssues(page: ReturnType<typeof transformPageData>) {
1477
+ const { typesOfIssues, ...rest } = page;
1478
+ return rest;
1479
+ }
1480
+
1481
+ const summaryPagesAffected = pagesAffected.map(stripTypesOfIssues);
1482
+ const summaryPagesNotAffected = pagesNotAffected.map(stripTypesOfIssues);
1483
+
1484
+ allIssues.scanPagesSummary = {
1485
+ pagesAffected: summaryPagesAffected,
1486
+ pagesNotAffected: summaryPagesNotAffected,
1487
+ scannedPagesCount,
1488
+ pagesNotScanned: Array.isArray(allIssues.pagesNotScanned)
1489
+ ? allIssues.pagesNotScanned
1490
+ : [],
1491
+ pagesNotScannedCount,
1492
+ };
1493
+ }
1494
+
1445
1495
  const generateArtifacts = async (
1446
1496
  randomToken: string,
1447
1497
  urlScanned: string,
@@ -1559,6 +1609,13 @@ const generateArtifacts = async (
1559
1609
  },
1560
1610
  cypressScanAboutMetadata,
1561
1611
  wcagLinks: constants.wcagLinks,
1612
+ scanPagesDetail: {
1613
+ pagesAffected: [],
1614
+ pagesNotAffected: [],
1615
+ scannedPagesCount: 0,
1616
+ pagesNotScanned: [],
1617
+ pagesNotScannedCount: 0,
1618
+ },
1562
1619
  // Populate boolean values for id="advancedScanOptionsSummary"
1563
1620
  advancedScanOptionsSummaryItems: {
1564
1621
  showIncludeScreenshots: [true].includes(scanDetails.isIncludeScreenshots),
@@ -1604,10 +1661,18 @@ const generateArtifacts = async (
1604
1661
  createScreenshotsFolder(randomToken);
1605
1662
  }
1606
1663
 
1664
+ populateScanPagesDetail(allIssues);
1665
+
1607
1666
  allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
1608
- consoleLogger.info(
1609
- `advancedScanOptionsSummaryItems is ${allIssues.advancedScanOptionsSummaryItems}`,
1610
- );
1667
+ allIssues.progressPercentage = getProgressPercentage(allIssues.scanPagesDetail, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
1668
+
1669
+ allIssues.issuesPercentage = await getIssuesPercentage(
1670
+ allIssues.scanPagesDetail,
1671
+ allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa,
1672
+ allIssues.advancedScanOptionsSummaryItems.disableOobee);
1673
+
1674
+ // console.log(allIssues.progressPercentage);
1675
+ // console.log(allIssues.issuesPercentage);
1611
1676
 
1612
1677
  const getAxeImpactCount = (allIssues: AllIssues) => {
1613
1678
  const impactCount = {
@@ -14,7 +14,7 @@ export const takeScreenshotForHTMLElements = async (
14
14
  page: Page,
15
15
  randomToken: string,
16
16
  locatorTimeout = 2000,
17
- maxScreenshots = 50,
17
+ maxScreenshots = 100,
18
18
  ): Promise<ResultWithScreenshot[]> => {
19
19
  const newViolations: ResultWithScreenshot[] = [];
20
20
  let screenshotCount = 0;
@@ -1,3 +1,10 @@
1
+ // Monkey patch Path2D to avoid PDF.js crashing
2
+ (globalThis as any).Path2D = class {
3
+ constructor(_path?: string) {}
4
+ rect(_x: number, _y: number, _width: number, _height: number) {}
5
+ addPath(_path: any, _transform?: any) {}
6
+ };
7
+
1
8
  import _ from 'lodash';
2
9
  import { getDocument, PDFPageProxy } from 'pdfjs-dist';
3
10
  import fs from 'fs';
@@ -25,11 +32,36 @@ interface pathObject {
25
32
  annot?: number;
26
33
  }
27
34
 
35
+ // Use safe canvas to avoid Path2D issues
36
+ function createSafeCanvas(width: number, height: number) {
37
+ const canvas = createCanvas(width, height);
38
+ const ctx = canvas.getContext('2d');
39
+
40
+ // Patch clip/stroke/fill/etc. to skip if Path2D is passed
41
+ const wrapIgnorePath2D = (fn: Function) =>
42
+ function (...args: any[]) {
43
+ if (args.length > 0 && args[0] instanceof (globalThis as any).Path2D) {
44
+ // Skip the operation
45
+ return;
46
+ }
47
+ return fn.apply(this, args);
48
+ };
49
+
50
+ ctx.clip = wrapIgnorePath2D(ctx.clip);
51
+ ctx.fill = wrapIgnorePath2D(ctx.fill);
52
+ ctx.stroke = wrapIgnorePath2D(ctx.stroke);
53
+ ctx.isPointInPath = wrapIgnorePath2D(ctx.isPointInPath);
54
+ ctx.isPointInStroke = wrapIgnorePath2D(ctx.isPointInStroke);
55
+
56
+ return canvas;
57
+ }
58
+
59
+ // CanvasFactory for Node.js
28
60
  function NodeCanvasFactory() {}
29
61
  NodeCanvasFactory.prototype = {
30
62
  create: function NodeCanvasFactory_create(width: number, height: number) {
31
63
  assert(width > 0 && height > 0, 'Invalid canvas size');
32
- const canvas = createCanvas(width, height);
64
+ const canvas = createSafeCanvas(width, height);
33
65
  const context = canvas.getContext('2d');
34
66
  return {
35
67
  canvas,
@@ -69,6 +101,7 @@ export async function getPdfScreenshots(
69
101
  const newItems = _.cloneDeep(items);
70
102
  const loadingTask = getDocument({
71
103
  url: pdfFilePath,
104
+ canvasFactory,
72
105
  standardFontDataUrl: path.join(dirname, '../node_modules/pdfjs-dist/standard_fonts/'),
73
106
  disableFontFace: true,
74
107
  verbosity: 0,