@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.
@@ -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';
@@ -43,6 +45,7 @@ export type PageInfo = {
43
45
  pageImagePath?: string;
44
46
  pageIndex?: number;
45
47
  metadata?: string;
48
+ httpStatusCode?: number;
46
49
  };
47
50
 
48
51
  export type RuleInfo = {
@@ -100,6 +103,13 @@ type AllIssues = {
100
103
  wcagLinks: { [key: string]: string };
101
104
  [key: string]: any;
102
105
  advancedScanOptionsSummaryItems: { [key: string]: boolean };
106
+ scanPagesDetail: {
107
+ pagesAffected: any[];
108
+ pagesNotAffected: any[];
109
+ scannedPagesCount: number;
110
+ pagesNotScanned: any[];
111
+ pagesNotScannedCount: number;
112
+ };
103
113
  };
104
114
 
105
115
  const filename = fileURLToPath(import.meta.url);
@@ -239,7 +249,7 @@ const writeCsv = async (allIssues, storagePath) => {
239
249
  scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
240
250
  severity: 'error',
241
251
  issueId: 'error-pages-skipped',
242
- issueDescription: 'Page was skipped during the scan',
252
+ issueDescription: page.metadata ? page.metadata : 'An unknown error caused the page to be skipped',
243
253
  wcagConformance: '',
244
254
  url: page.url || page || '',
245
255
  pageTitle: 'Error',
@@ -782,25 +792,21 @@ const writeJsonAndBase64Files = async (
782
792
  items.mustFix.rules.forEach(rule => {
783
793
  rule.pagesAffected.forEach(page => {
784
794
  page.itemsCount = page.items.length;
785
- page.items = [];
786
795
  });
787
796
  });
788
797
  items.goodToFix.rules.forEach(rule => {
789
798
  rule.pagesAffected.forEach(page => {
790
799
  page.itemsCount = page.items.length;
791
- page.items = [];
792
800
  });
793
801
  });
794
802
  items.needsReview.rules.forEach(rule => {
795
803
  rule.pagesAffected.forEach(page => {
796
804
  page.itemsCount = page.items.length;
797
- page.items = [];
798
805
  });
799
806
  });
800
807
  items.passed.rules.forEach(rule => {
801
808
  rule.pagesAffected.forEach(page => {
802
809
  page.itemsCount = page.items.length;
803
- page.items = [];
804
810
  });
805
811
  });
806
812
 
@@ -815,6 +821,8 @@ const writeJsonAndBase64Files = async (
815
821
  pagesNotScanned,
816
822
  wcagLinks,
817
823
  wcagPassPercentage,
824
+ progressPercentage,
825
+ issuesPercentage,
818
826
  totalPagesScanned,
819
827
  totalPagesNotScanned,
820
828
  topTenIssues,
@@ -827,6 +835,8 @@ const writeJsonAndBase64Files = async (
827
835
  pagesNotScanned,
828
836
  wcagLinks,
829
837
  wcagPassPercentage,
838
+ progressPercentage,
839
+ issuesPercentage,
830
840
  totalPagesScanned,
831
841
  totalPagesNotScanned,
832
842
  topTenIssues,
@@ -836,6 +846,7 @@ const writeJsonAndBase64Files = async (
836
846
  jsonFilePath: scanItemsMiniReportJsonFilePath,
837
847
  base64FilePath: scanItemsMiniReportBase64FilePath,
838
848
  } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItemsMini }, storagePath, 'scanItemsSummaryMiniReport');
849
+
839
850
  const summaryItems = {
840
851
  mustFix: {
841
852
  totalItems: items.mustFix?.totalItems || 0,
@@ -852,6 +863,8 @@ const writeJsonAndBase64Files = async (
852
863
  topTenPagesWithMostIssues,
853
864
  wcagLinks,
854
865
  wcagPassPercentage,
866
+ progressPercentage,
867
+ issuesPercentage,
855
868
  totalPagesScanned,
856
869
  totalPagesNotScanned,
857
870
  topTenIssues,
@@ -862,247 +875,15 @@ const writeJsonAndBase64Files = async (
862
875
  base64FilePath: scanItemsSummaryBase64FilePath,
863
876
  } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItems }, storagePath, 'scanItemsSummary');
864
877
 
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
878
  const {
1098
879
  jsonFilePath: scanPagesDetailJsonFilePath,
1099
880
  base64FilePath: scanPagesDetailBase64FilePath
1100
- } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanPagesDetail }, storagePath, 'scanPagesDetail');
881
+ } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesDetail }, storagePath, 'scanPagesDetail');
1101
882
 
1102
883
  const {
1103
884
  jsonFilePath: scanPagesSummaryJsonFilePath,
1104
885
  base64FilePath: scanPagesSummaryBase64FilePath
1105
- } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanPagesSummary }, storagePath, 'scanPagesSummary');
886
+ } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...allIssues.scanPagesSummary }, storagePath, 'scanPagesSummary');
1106
887
 
1107
888
  return {
1108
889
  scanDataJsonFilePath,
@@ -1421,6 +1202,7 @@ const createRuleIdJson = allIssues => {
1421
1202
  });
1422
1203
  });
1423
1204
  snippets = [...snippetsSet];
1205
+ rule.pagesAffected.forEach(p => { delete p.items; });
1424
1206
  }
1425
1207
  compiledRuleJson[ruleId] = {
1426
1208
  snippets,
@@ -1442,6 +1224,272 @@ const moveElemScreenshots = (randomToken: string, storagePath: string) => {
1442
1224
  }
1443
1225
  };
1444
1226
 
1227
+ /**
1228
+ * Build allIssues.scanPagesDetail and allIssues.scanPagesSummary
1229
+ * by analyzing pagesScanned (including mustFix/goodToFix/etc.).
1230
+ */
1231
+ function populateScanPagesDetail(allIssues: AllIssues): void {
1232
+ // --------------------------------------------
1233
+ // 1) Gather your "scanned" pages from allIssues
1234
+ // --------------------------------------------
1235
+ const allScannedPages = Array.isArray(allIssues.pagesScanned)
1236
+ ? allIssues.pagesScanned
1237
+ : [];
1238
+
1239
+ // --------------------------------------------
1240
+ // 2) Define category constants (optional, just for clarity)
1241
+ // --------------------------------------------
1242
+ const mustFixCategory = "mustFix";
1243
+ const goodToFixCategory = "goodToFix";
1244
+ const needsReviewCategory = "needsReview";
1245
+ const passedCategory = "passed";
1246
+
1247
+ // --------------------------------------------
1248
+ // 3) Set up type declarations (if you want them local to this function)
1249
+ // --------------------------------------------
1250
+ type RuleData = {
1251
+ ruleId: string;
1252
+ wcagConformance: string[];
1253
+ occurrencesMustFix: number;
1254
+ occurrencesGoodToFix: number;
1255
+ occurrencesNeedsReview: number;
1256
+ occurrencesPassed: number;
1257
+ };
1258
+
1259
+ type PageData = {
1260
+ pageTitle: string;
1261
+ url: string;
1262
+ // Summaries
1263
+ totalOccurrencesFailedIncludingNeedsReview: number; // mustFix + goodToFix + needsReview
1264
+ totalOccurrencesFailedExcludingNeedsReview: number; // mustFix + goodToFix
1265
+ totalOccurrencesNeedsReview: number; // needsReview
1266
+ totalOccurrencesPassed: number; // passed only
1267
+ typesOfIssues: Record<string, RuleData>;
1268
+ };
1269
+
1270
+ // --------------------------------------------
1271
+ // 4) We'll accumulate pages in a map keyed by URL
1272
+ // --------------------------------------------
1273
+ const pagesMap: Record<string, PageData> = {};
1274
+
1275
+ // --------------------------------------------
1276
+ // 5) Build pagesMap by iterating over each category in allIssues.items
1277
+ // --------------------------------------------
1278
+ Object.entries(allIssues.items).forEach(([categoryName, categoryData]) => {
1279
+ if (!categoryData?.rules) return; // no rules in this category? skip
1280
+
1281
+ categoryData.rules.forEach(rule => {
1282
+ const { rule: ruleId, conformance = [] } = rule;
1283
+
1284
+ rule.pagesAffected.forEach(p => {
1285
+ const { url, pageTitle, items = [] } = p;
1286
+ const itemsCount = items.length;
1287
+
1288
+ // Ensure the page is in pagesMap
1289
+ if (!pagesMap[url]) {
1290
+ pagesMap[url] = {
1291
+ pageTitle,
1292
+ url,
1293
+ totalOccurrencesFailedIncludingNeedsReview: 0,
1294
+ totalOccurrencesFailedExcludingNeedsReview: 0,
1295
+ totalOccurrencesNeedsReview: 0,
1296
+ totalOccurrencesPassed: 0,
1297
+ typesOfIssues: {},
1298
+ };
1299
+ }
1300
+
1301
+ // Ensure the rule is present for this page
1302
+ if (!pagesMap[url].typesOfIssues[ruleId]) {
1303
+ pagesMap[url].typesOfIssues[ruleId] = {
1304
+ ruleId,
1305
+ wcagConformance: conformance,
1306
+ occurrencesMustFix: 0,
1307
+ occurrencesGoodToFix: 0,
1308
+ occurrencesNeedsReview: 0,
1309
+ occurrencesPassed: 0,
1310
+ };
1311
+ }
1312
+
1313
+ // Depending on the category, increment the relevant occurrence counts
1314
+ if (categoryName === mustFixCategory) {
1315
+ pagesMap[url].typesOfIssues[ruleId].occurrencesMustFix += itemsCount;
1316
+ pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1317
+ pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
1318
+ } else if (categoryName === goodToFixCategory) {
1319
+ pagesMap[url].typesOfIssues[ruleId].occurrencesGoodToFix += itemsCount;
1320
+ pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1321
+ pagesMap[url].totalOccurrencesFailedExcludingNeedsReview += itemsCount;
1322
+ } else if (categoryName === needsReviewCategory) {
1323
+ pagesMap[url].typesOfIssues[ruleId].occurrencesNeedsReview += itemsCount;
1324
+ pagesMap[url].totalOccurrencesFailedIncludingNeedsReview += itemsCount;
1325
+ pagesMap[url].totalOccurrencesNeedsReview += itemsCount;
1326
+ } else if (categoryName === passedCategory) {
1327
+ pagesMap[url].typesOfIssues[ruleId].occurrencesPassed += itemsCount;
1328
+ pagesMap[url].totalOccurrencesPassed += itemsCount;
1329
+ }
1330
+ });
1331
+ });
1332
+ });
1333
+
1334
+ // --------------------------------------------
1335
+ // 6) Separate scanned pages into “affected” vs. “notAffected”
1336
+ // --------------------------------------------
1337
+ const pagesInMap = Object.values(pagesMap); // All pages that have some record in pagesMap
1338
+ const pagesInMapUrls = new Set(Object.keys(pagesMap));
1339
+
1340
+ // (a) Pages with only passed (no mustFix/goodToFix/needsReview)
1341
+ const pagesAllPassed = pagesInMap.filter(
1342
+ p => p.totalOccurrencesFailedIncludingNeedsReview === 0
1343
+ );
1344
+
1345
+ // (b) Pages that do NOT appear in pagesMap at all => scanned but no items found
1346
+ const pagesNoEntries = allScannedPages
1347
+ .filter(sp => !pagesInMapUrls.has(sp.url))
1348
+ .map(sp => ({
1349
+ pageTitle: sp.pageTitle,
1350
+ url: sp.url,
1351
+ totalOccurrencesFailedIncludingNeedsReview: 0,
1352
+ totalOccurrencesFailedExcludingNeedsReview: 0,
1353
+ totalOccurrencesNeedsReview: 0,
1354
+ totalOccurrencesPassed: 0,
1355
+ typesOfIssues: {},
1356
+ }));
1357
+
1358
+ // Combine these into "notAffected"
1359
+ const pagesNotAffectedRaw = [...pagesAllPassed, ...pagesNoEntries];
1360
+
1361
+ // "affected" pages => have at least 1 mustFix/goodToFix/needsReview
1362
+ const pagesAffectedRaw = pagesInMap.filter(
1363
+ p => p.totalOccurrencesFailedIncludingNeedsReview > 0
1364
+ );
1365
+
1366
+ // --------------------------------------------
1367
+ // 7) Transform both arrays to the final shape
1368
+ // --------------------------------------------
1369
+ function transformPageData(page: PageData) {
1370
+ const typesOfIssuesArray = Object.values(page.typesOfIssues);
1371
+
1372
+ // Compute sums for each failing category
1373
+ const mustFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesMustFix, 0);
1374
+ const goodToFixSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesGoodToFix, 0);
1375
+ const needsReviewSum = typesOfIssuesArray.reduce((acc, r) => acc + r.occurrencesNeedsReview, 0);
1376
+
1377
+ // Build categoriesPresent based on nonzero failing counts
1378
+ const categoriesPresent: string[] = [];
1379
+ if (mustFixSum > 0) categoriesPresent.push("mustFix");
1380
+ if (goodToFixSum > 0) categoriesPresent.push("goodToFix");
1381
+ if (needsReviewSum > 0) categoriesPresent.push("needsReview");
1382
+
1383
+ // Count how many rules have failing issues
1384
+ const failedRuleIds = new Set<string>();
1385
+ typesOfIssuesArray.forEach(r => {
1386
+ if ((r.occurrencesMustFix || 0) > 0 ||
1387
+ (r.occurrencesGoodToFix || 0) > 0 ||
1388
+ (r.occurrencesNeedsReview || 0) > 0) {
1389
+ failedRuleIds.add(r.ruleId); // Ensure ruleId is unique
1390
+ }
1391
+ });
1392
+ const failedRuleCount = failedRuleIds.size;
1393
+
1394
+ // Possibly these two for future convenience
1395
+ const typesOfIssuesExcludingNeedsReviewCount = typesOfIssuesArray.filter(
1396
+ r => (r.occurrencesMustFix || 0) + (r.occurrencesGoodToFix || 0) > 0
1397
+ ).length;
1398
+
1399
+ const typesOfIssuesExclusiveToNeedsReviewCount = typesOfIssuesArray.filter(
1400
+ r =>
1401
+ (r.occurrencesNeedsReview || 0) > 0 &&
1402
+ (r.occurrencesMustFix || 0) === 0 &&
1403
+ (r.occurrencesGoodToFix || 0) === 0
1404
+ ).length;
1405
+
1406
+ // Aggregate wcagConformance for rules that actually fail
1407
+ const allConformance = typesOfIssuesArray.reduce((acc, curr) => {
1408
+ const nonPassedCount =
1409
+ (curr.occurrencesMustFix || 0) +
1410
+ (curr.occurrencesGoodToFix || 0) +
1411
+ (curr.occurrencesNeedsReview || 0);
1412
+
1413
+ if (nonPassedCount > 0) {
1414
+ return acc.concat(curr.wcagConformance || []);
1415
+ }
1416
+ return acc;
1417
+ }, [] as string[]);
1418
+ // Remove duplicates
1419
+ const conformance = Array.from(new Set(allConformance));
1420
+
1421
+ return {
1422
+ pageTitle: page.pageTitle,
1423
+ url: page.url,
1424
+ totalOccurrencesFailedIncludingNeedsReview: page.totalOccurrencesFailedIncludingNeedsReview,
1425
+ totalOccurrencesFailedExcludingNeedsReview: page.totalOccurrencesFailedExcludingNeedsReview,
1426
+ totalOccurrencesMustFix: mustFixSum,
1427
+ totalOccurrencesGoodToFix: goodToFixSum,
1428
+ totalOccurrencesNeedsReview: needsReviewSum,
1429
+ totalOccurrencesPassed: page.totalOccurrencesPassed,
1430
+ typesOfIssuesExclusiveToNeedsReviewCount,
1431
+ typesOfIssuesCount: failedRuleCount,
1432
+ typesOfIssuesExcludingNeedsReviewCount,
1433
+ categoriesPresent,
1434
+ conformance,
1435
+ // Keep full detail for "scanPagesDetail"
1436
+ typesOfIssues: typesOfIssuesArray,
1437
+ };
1438
+ }
1439
+
1440
+ // Transform raw pages
1441
+ const pagesAffected = pagesAffectedRaw.map(transformPageData);
1442
+ const pagesNotAffected = pagesNotAffectedRaw.map(transformPageData);
1443
+
1444
+ // --------------------------------------------
1445
+ // 8) Sort pages by typesOfIssuesCount (descending) for both arrays
1446
+ // --------------------------------------------
1447
+ pagesAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
1448
+ pagesNotAffected.sort((a, b) => b.typesOfIssuesCount - a.typesOfIssuesCount);
1449
+
1450
+ // --------------------------------------------
1451
+ // 9) Compute scanned/ skipped counts
1452
+ // --------------------------------------------
1453
+ const scannedPagesCount = pagesAffected.length + pagesNotAffected.length;
1454
+ const pagesNotScannedCount = Array.isArray(allIssues.pagesNotScanned)
1455
+ ? allIssues.pagesNotScanned.length
1456
+ : 0;
1457
+
1458
+ // --------------------------------------------
1459
+ // 10) Build scanPagesDetail (with full "typesOfIssues")
1460
+ // --------------------------------------------
1461
+ allIssues.scanPagesDetail = {
1462
+ pagesAffected,
1463
+ pagesNotAffected,
1464
+ scannedPagesCount,
1465
+ pagesNotScanned: Array.isArray(allIssues.pagesNotScanned)
1466
+ ? allIssues.pagesNotScanned
1467
+ : [],
1468
+ pagesNotScannedCount,
1469
+ };
1470
+
1471
+ // --------------------------------------------
1472
+ // 11) Build scanPagesSummary (strip out "typesOfIssues")
1473
+ // --------------------------------------------
1474
+ function stripTypesOfIssues(page: ReturnType<typeof transformPageData>) {
1475
+ const { typesOfIssues, ...rest } = page;
1476
+ return rest;
1477
+ }
1478
+
1479
+ const summaryPagesAffected = pagesAffected.map(stripTypesOfIssues);
1480
+ const summaryPagesNotAffected = pagesNotAffected.map(stripTypesOfIssues);
1481
+
1482
+ allIssues.scanPagesSummary = {
1483
+ pagesAffected: summaryPagesAffected,
1484
+ pagesNotAffected: summaryPagesNotAffected,
1485
+ scannedPagesCount,
1486
+ pagesNotScanned: Array.isArray(allIssues.pagesNotScanned)
1487
+ ? allIssues.pagesNotScanned
1488
+ : [],
1489
+ pagesNotScannedCount,
1490
+ };
1491
+ }
1492
+
1445
1493
  const generateArtifacts = async (
1446
1494
  randomToken: string,
1447
1495
  urlScanned: string,
@@ -1559,6 +1607,13 @@ const generateArtifacts = async (
1559
1607
  },
1560
1608
  cypressScanAboutMetadata,
1561
1609
  wcagLinks: constants.wcagLinks,
1610
+ scanPagesDetail: {
1611
+ pagesAffected: [],
1612
+ pagesNotAffected: [],
1613
+ scannedPagesCount: 0,
1614
+ pagesNotScanned: [],
1615
+ pagesNotScannedCount: 0,
1616
+ },
1562
1617
  // Populate boolean values for id="advancedScanOptionsSummary"
1563
1618
  advancedScanOptionsSummaryItems: {
1564
1619
  showIncludeScreenshots: [true].includes(scanDetails.isIncludeScreenshots),
@@ -1604,10 +1659,18 @@ const generateArtifacts = async (
1604
1659
  createScreenshotsFolder(randomToken);
1605
1660
  }
1606
1661
 
1662
+ populateScanPagesDetail(allIssues);
1663
+
1607
1664
  allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
1608
- consoleLogger.info(
1609
- `advancedScanOptionsSummaryItems is ${allIssues.advancedScanOptionsSummaryItems}`,
1610
- );
1665
+ allIssues.progressPercentage = getProgressPercentage(allIssues.scanPagesDetail, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
1666
+
1667
+ allIssues.issuesPercentage = await getIssuesPercentage(
1668
+ allIssues.scanPagesDetail,
1669
+ allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa,
1670
+ allIssues.advancedScanOptionsSummaryItems.disableOobee);
1671
+
1672
+ // console.log(allIssues.progressPercentage);
1673
+ // console.log(allIssues.issuesPercentage);
1611
1674
 
1612
1675
  const getAxeImpactCount = (allIssues: AllIssues) => {
1613
1676
  const impactCount = {