@govtechsg/oobee 0.10.39 → 0.10.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docker-test.yml +1 -1
- package/README.md +2 -0
- package/REPORTS.md +362 -0
- package/package.json +1 -1
- package/src/crawlers/commonCrawlerFunc.ts +29 -1
- package/src/crawlers/crawlDomain.ts +4 -21
- package/src/crawlers/crawlSitemap.ts +1 -1
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +589 -554
- package/src/crawlers/pdfScanFunc.ts +67 -26
- package/src/mergeAxeResults.ts +302 -237
- package/src/screenshotFunc/htmlScreenshotFunc.ts +1 -1
- package/src/screenshotFunc/pdfScreenshotFunc.ts +34 -1
- package/src/utils.ts +289 -13
package/src/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';
|
@@ -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
|
-
|
1609
|
-
|
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 =
|
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 =
|
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,
|