@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/mergeAxeResults.ts
CHANGED
@@ -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: '
|
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
|
-
|
1609
|
-
|
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 = {
|