@govtechsg/oobee 0.10.36 → 0.10.39
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/DETAILS.md +3 -3
- package/INTEGRATION.md +142 -53
- package/README.md +15 -0
- package/exclusions.txt +4 -1
- package/package.json +2 -2
- package/src/constants/cliFunctions.ts +0 -7
- package/src/constants/common.ts +39 -1
- package/src/constants/constants.ts +9 -8
- package/src/crawlers/commonCrawlerFunc.ts +66 -219
- package/src/crawlers/crawlDomain.ts +6 -2
- package/src/crawlers/crawlLocalFile.ts +2 -0
- package/src/crawlers/crawlSitemap.ts +5 -3
- package/src/crawlers/custom/escapeCssSelector.ts +10 -0
- package/src/crawlers/custom/evaluateAltText.ts +13 -0
- package/src/crawlers/custom/extractAndGradeText.ts +0 -2
- package/src/crawlers/custom/extractText.ts +28 -0
- package/src/crawlers/custom/findElementByCssSelector.ts +46 -0
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +1006 -901
- package/src/crawlers/custom/framesCheck.ts +51 -0
- package/src/crawlers/custom/getAxeConfiguration.ts +126 -0
- package/src/crawlers/custom/gradeReadability.ts +30 -0
- package/src/crawlers/custom/xPathToCss.ts +178 -0
- package/src/mergeAxeResults.ts +467 -129
- package/src/npmIndex.ts +130 -62
- package/src/static/ejs/partials/components/ruleOffcanvas.ejs +1 -1
- package/src/static/ejs/partials/components/scanAbout.ejs +1 -1
- package/src/static/ejs/partials/footer.ejs +3 -3
- package/src/static/ejs/partials/scripts/reportSearch.ejs +112 -74
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +2 -2
- package/src/static/ejs/partials/summaryMain.ejs +3 -3
- package/src/static/ejs/report.ejs +3 -3
- package/src/xPathToCssCypress.ts +178 -0
- package/src/crawlers/customAxeFunctions.ts +0 -82
package/src/mergeAxeResults.ts
CHANGED
@@ -13,7 +13,7 @@ import zlib from 'zlib';
|
|
13
13
|
import { Base64Encode } from 'base64-stream';
|
14
14
|
import { pipeline } from 'stream/promises';
|
15
15
|
import constants, { ScannerTypes } from './constants/constants.js';
|
16
|
-
import { urlWithoutAuth
|
16
|
+
import { urlWithoutAuth } from './constants/common.js';
|
17
17
|
import {
|
18
18
|
createScreenshotsFolder,
|
19
19
|
getStoragePath,
|
@@ -34,19 +34,21 @@ export type ItemsInfo = {
|
|
34
34
|
displayNeedsReview?: boolean;
|
35
35
|
};
|
36
36
|
|
37
|
-
type PageInfo = {
|
38
|
-
items
|
37
|
+
export type PageInfo = {
|
38
|
+
items?: ItemsInfo[];
|
39
39
|
itemsCount?: number;
|
40
40
|
pageTitle: string;
|
41
|
-
url
|
41
|
+
url: string;
|
42
|
+
actualUrl: string;
|
42
43
|
pageImagePath?: string;
|
43
44
|
pageIndex?: number;
|
44
|
-
metadata
|
45
|
+
metadata?: string;
|
45
46
|
};
|
46
47
|
|
47
48
|
export type RuleInfo = {
|
48
49
|
totalItems: number;
|
49
50
|
pagesAffected: PageInfo[];
|
51
|
+
pagesAffectedCount: number;
|
50
52
|
rule: string;
|
51
53
|
description: string;
|
52
54
|
axeImpact: string;
|
@@ -74,7 +76,6 @@ type AllIssues = {
|
|
74
76
|
deviceChosen: string;
|
75
77
|
formatAboutStartTime: (dateString: any) => string;
|
76
78
|
isCustomFlow: boolean;
|
77
|
-
viewport: string;
|
78
79
|
pagesScanned: PageInfo[];
|
79
80
|
pagesNotScanned: PageInfo[];
|
80
81
|
totalPagesScanned: number;
|
@@ -85,14 +86,17 @@ type AllIssues = {
|
|
85
86
|
topTenIssues: Array<any>;
|
86
87
|
wcagViolations: string[];
|
87
88
|
customFlowLabel: string;
|
88
|
-
|
89
|
+
oobeeAppVersion: string;
|
89
90
|
items: {
|
90
91
|
mustFix: Category;
|
91
92
|
goodToFix: Category;
|
92
93
|
needsReview: Category;
|
93
94
|
passed: Category;
|
94
95
|
};
|
95
|
-
cypressScanAboutMetadata:
|
96
|
+
cypressScanAboutMetadata: {
|
97
|
+
browser?: string;
|
98
|
+
viewport?: { width: number; height: number };
|
99
|
+
};
|
96
100
|
wcagLinks: { [key: string]: string };
|
97
101
|
[key: string]: any;
|
98
102
|
advancedScanOptionsSummaryItems: { [key: string]: boolean };
|
@@ -160,13 +164,11 @@ const writeCsv = async (allIssues, storagePath) => {
|
|
160
164
|
pagesAffected,
|
161
165
|
helpUrl: learnMore,
|
162
166
|
} = rule;
|
163
|
-
|
164
|
-
const clausesArr = conformance.filter(
|
165
|
-
clause => !['wcag2a', 'wcag2aa', 'wcag2aaa'].includes(clause),
|
166
|
-
);
|
167
|
-
pagesAffected.sort((a, b) => a.url.localeCompare(b.url));
|
167
|
+
|
168
168
|
// format clauses as a string
|
169
|
-
const wcagConformance =
|
169
|
+
const wcagConformance = conformance.join(',');
|
170
|
+
|
171
|
+
pagesAffected.sort((a, b) => a.url.localeCompare(b.url));
|
170
172
|
|
171
173
|
pagesAffected.forEach(affectedPage => {
|
172
174
|
const { url, items } = affectedPage;
|
@@ -239,8 +241,8 @@ const writeCsv = async (allIssues, storagePath) => {
|
|
239
241
|
issueId: 'error-pages-skipped',
|
240
242
|
issueDescription: 'Page was skipped during the scan',
|
241
243
|
wcagConformance: '',
|
242
|
-
url: page.url || '',
|
243
|
-
pageTitle: '',
|
244
|
+
url: page.url || page || '',
|
245
|
+
pageTitle: 'Error',
|
244
246
|
context: '',
|
245
247
|
howToFix: '',
|
246
248
|
axeImpact: '',
|
@@ -544,86 +546,93 @@ const writeLargeScanItemsJsonToFile = async (obj: object, filePath: string) => {
|
|
544
546
|
|
545
547
|
keys.forEach((key, i) => {
|
546
548
|
const value = obj[key];
|
547
|
-
queueWrite(` "${key}": {\n`);
|
548
|
-
|
549
|
-
const { rules, ...otherProperties } = value;
|
550
|
-
|
551
|
-
// Write other properties
|
552
|
-
Object.entries(otherProperties).forEach(([propKey, propValue], j) => {
|
553
|
-
const propValueString =
|
554
|
-
propValue === null ||
|
555
|
-
typeof propValue === 'function' ||
|
556
|
-
typeof propValue === 'undefined'
|
557
|
-
? 'null'
|
558
|
-
: JSON.stringify(propValue);
|
559
|
-
queueWrite(` "${propKey}": ${propValueString}`);
|
560
|
-
if (j < Object.keys(otherProperties).length - 1 || (rules && rules.length >= 0)) {
|
561
|
-
queueWrite(',\n');
|
562
|
-
} else {
|
563
|
-
queueWrite('\n');
|
564
|
-
}
|
565
|
-
});
|
566
|
-
|
567
|
-
if (rules && Array.isArray(rules)) {
|
568
|
-
queueWrite(' "rules": [\n');
|
569
|
-
|
570
|
-
rules.forEach((rule, j) => {
|
571
|
-
queueWrite(' {\n');
|
572
|
-
const { pagesAffected, ...otherRuleProperties } = rule;
|
573
|
-
|
574
|
-
Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
|
575
|
-
const ruleValueString =
|
576
|
-
ruleValue === null ||
|
577
|
-
typeof ruleValue === 'function' ||
|
578
|
-
typeof ruleValue === 'undefined'
|
579
|
-
? 'null'
|
580
|
-
: JSON.stringify(ruleValue);
|
581
|
-
queueWrite(` "${ruleKey}": ${ruleValueString}`);
|
582
|
-
if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
|
583
|
-
queueWrite(',\n');
|
584
|
-
} else {
|
585
|
-
queueWrite('\n');
|
586
|
-
}
|
587
|
-
});
|
588
549
|
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
550
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
551
|
+
queueWrite(` "${key}": ${JSON.stringify(value)}`);
|
552
|
+
} else {
|
553
|
+
queueWrite(` "${key}": {\n`);
|
554
|
+
|
555
|
+
const { rules, ...otherProperties } = value;
|
556
|
+
|
557
|
+
// Write other properties
|
558
|
+
Object.entries(otherProperties).forEach(([propKey, propValue], j) => {
|
559
|
+
const propValueString =
|
560
|
+
propValue === null ||
|
561
|
+
typeof propValue === 'function' ||
|
562
|
+
typeof propValue === 'undefined'
|
563
|
+
? 'null'
|
564
|
+
: JSON.stringify(propValue);
|
565
|
+
queueWrite(` "${propKey}": ${propValueString}`);
|
566
|
+
if (j < Object.keys(otherProperties).length - 1 || (rules && rules.length >= 0)) {
|
567
|
+
queueWrite(',\n');
|
568
|
+
} else {
|
569
|
+
queueWrite('\n');
|
570
|
+
}
|
571
|
+
});
|
599
572
|
|
600
|
-
|
573
|
+
if (rules && Array.isArray(rules)) {
|
574
|
+
queueWrite(' "rules": [\n');
|
575
|
+
|
576
|
+
rules.forEach((rule, j) => {
|
577
|
+
queueWrite(' {\n');
|
578
|
+
const { pagesAffected, ...otherRuleProperties } = rule;
|
579
|
+
|
580
|
+
Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
|
581
|
+
const ruleValueString =
|
582
|
+
ruleValue === null ||
|
583
|
+
typeof ruleValue === 'function' ||
|
584
|
+
typeof ruleValue === 'undefined'
|
585
|
+
? 'null'
|
586
|
+
: JSON.stringify(ruleValue);
|
587
|
+
queueWrite(` "${ruleKey}": ${ruleValueString}`);
|
588
|
+
if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
|
601
589
|
queueWrite(',\n');
|
602
590
|
} else {
|
603
591
|
queueWrite('\n');
|
604
592
|
}
|
605
593
|
});
|
606
594
|
|
607
|
-
|
608
|
-
|
595
|
+
if (pagesAffected && Array.isArray(pagesAffected)) {
|
596
|
+
queueWrite(' "pagesAffected": [\n');
|
609
597
|
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
598
|
+
pagesAffected.forEach((page, p) => {
|
599
|
+
const pageJson = JSON.stringify(page, null, 2)
|
600
|
+
.split('\n')
|
601
|
+
.map((line, idx) => (idx === 0 ? ` ${line}` : ` ${line}`))
|
602
|
+
.join('\n');
|
603
|
+
|
604
|
+
queueWrite(pageJson);
|
605
|
+
|
606
|
+
if (p < pagesAffected.length - 1) {
|
607
|
+
queueWrite(',\n');
|
608
|
+
} else {
|
609
|
+
queueWrite('\n');
|
610
|
+
}
|
611
|
+
});
|
612
|
+
|
613
|
+
queueWrite(' ]');
|
614
|
+
}
|
615
|
+
|
616
|
+
queueWrite('\n }');
|
617
|
+
if (j < rules.length - 1) {
|
618
|
+
queueWrite(',\n');
|
619
|
+
} else {
|
620
|
+
queueWrite('\n');
|
621
|
+
}
|
622
|
+
});
|
623
|
+
|
624
|
+
queueWrite(' ]');
|
625
|
+
}
|
626
|
+
queueWrite('\n }');
|
627
|
+
}
|
628
|
+
|
629
|
+
if (i < keys.length - 1) {
|
630
|
+
queueWrite(',\n');
|
631
|
+
} else {
|
632
|
+
queueWrite('\n');
|
633
|
+
}
|
617
634
|
|
618
|
-
queueWrite(' ]');
|
619
|
-
}
|
620
635
|
|
621
|
-
queueWrite('\n }');
|
622
|
-
if (i < keys.length - 1) {
|
623
|
-
queueWrite(',\n');
|
624
|
-
} else {
|
625
|
-
queueWrite('\n');
|
626
|
-
}
|
627
636
|
});
|
628
637
|
|
629
638
|
queueWrite('}\n');
|
@@ -727,6 +736,12 @@ const writeJsonAndBase64Files = async (
|
|
727
736
|
scanItemsSummaryBase64FilePath: string;
|
728
737
|
scanItemsMiniReportJsonFilePath: string;
|
729
738
|
scanItemsMiniReportBase64FilePath: string;
|
739
|
+
scanIssuesSummaryJsonFilePath: string;
|
740
|
+
scanIssuesSummaryBase64FilePath: string;
|
741
|
+
scanPagesDetailJsonFilePath: string;
|
742
|
+
scanPagesDetailBase64FilePath: string;
|
743
|
+
scanPagesSummaryJsonFilePath: string;
|
744
|
+
scanPagesSummaryBase64FilePath: string;
|
730
745
|
scanDataJsonFileSize: number;
|
731
746
|
scanItemsJsonFileSize: number;
|
732
747
|
}> => {
|
@@ -734,7 +749,33 @@ const writeJsonAndBase64Files = async (
|
|
734
749
|
const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } =
|
735
750
|
await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
|
736
751
|
const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } =
|
737
|
-
await writeJsonFileAndCompressedJsonFile(items, storagePath, 'scanItems');
|
752
|
+
await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...items }, storagePath, 'scanItems');
|
753
|
+
|
754
|
+
// Add pagesAffectedCount to each rule in scanItemsMiniReport (items) and sort them in descending order of pagesAffectedCount
|
755
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach((category) => {
|
756
|
+
if (items[category].rules && Array.isArray(items[category].rules)) {
|
757
|
+
items[category].rules.forEach((rule) => {
|
758
|
+
rule.pagesAffectedCount = Array.isArray(rule.pagesAffected)
|
759
|
+
? rule.pagesAffected.length
|
760
|
+
: 0;
|
761
|
+
});
|
762
|
+
|
763
|
+
// Sort in descending order of pagesAffectedCount
|
764
|
+
items[category].rules.sort((a, b) => (b.pagesAffectedCount || 0) - (a.pagesAffectedCount || 0));
|
765
|
+
}
|
766
|
+
});
|
767
|
+
|
768
|
+
// Refactor scanIssuesSummary to reuse the scanItemsMiniReport structure by stripping out pagesAffected
|
769
|
+
const scanIssuesSummary = {
|
770
|
+
mustFix: items.mustFix.rules.map(({ pagesAffected, ...ruleInfo }) => ruleInfo),
|
771
|
+
goodToFix: items.goodToFix.rules.map(({ pagesAffected, ...ruleInfo }) => ruleInfo),
|
772
|
+
needsReview: items.needsReview.rules.map(({ pagesAffected, ...ruleInfo }) => ruleInfo),
|
773
|
+
passed: items.passed.rules.map(({ pagesAffected, ...ruleInfo }) => ruleInfo),
|
774
|
+
};
|
775
|
+
|
776
|
+
// Write out the scanIssuesSummary JSON using the new structure
|
777
|
+
const { jsonFilePath: scanIssuesSummaryJsonFilePath, base64FilePath: scanIssuesSummaryBase64FilePath } =
|
778
|
+
await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanIssuesSummary }, storagePath, 'scanIssuesSummary');
|
738
779
|
|
739
780
|
// scanItemsSummary
|
740
781
|
// the below mutates the original items object, since it is expensive to clone
|
@@ -794,8 +835,7 @@ const writeJsonAndBase64Files = async (
|
|
794
835
|
const {
|
795
836
|
jsonFilePath: scanItemsMiniReportJsonFilePath,
|
796
837
|
base64FilePath: scanItemsMiniReportBase64FilePath,
|
797
|
-
} = await writeJsonFileAndCompressedJsonFile(summaryItemsMini, storagePath, 'scanItemsSummaryMiniReport');
|
798
|
-
|
838
|
+
} = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItemsMini }, storagePath, 'scanItemsSummaryMiniReport');
|
799
839
|
const summaryItems = {
|
800
840
|
mustFix: {
|
801
841
|
totalItems: items.mustFix?.totalItems || 0,
|
@@ -820,7 +860,249 @@ const writeJsonAndBase64Files = async (
|
|
820
860
|
const {
|
821
861
|
jsonFilePath: scanItemsSummaryJsonFilePath,
|
822
862
|
base64FilePath: scanItemsSummaryBase64FilePath,
|
823
|
-
} = await writeJsonFileAndCompressedJsonFile(summaryItems, storagePath, 'scanItemsSummary');
|
863
|
+
} = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItems }, storagePath, 'scanItemsSummary');
|
864
|
+
|
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
|
+
const {
|
1098
|
+
jsonFilePath: scanPagesDetailJsonFilePath,
|
1099
|
+
base64FilePath: scanPagesDetailBase64FilePath
|
1100
|
+
} = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanPagesDetail }, storagePath, 'scanPagesDetail');
|
1101
|
+
|
1102
|
+
const {
|
1103
|
+
jsonFilePath: scanPagesSummaryJsonFilePath,
|
1104
|
+
base64FilePath: scanPagesSummaryBase64FilePath
|
1105
|
+
} = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanPagesSummary }, storagePath, 'scanPagesSummary');
|
824
1106
|
|
825
1107
|
return {
|
826
1108
|
scanDataJsonFilePath,
|
@@ -831,6 +1113,12 @@ const writeJsonAndBase64Files = async (
|
|
831
1113
|
scanItemsSummaryBase64FilePath,
|
832
1114
|
scanItemsMiniReportJsonFilePath,
|
833
1115
|
scanItemsMiniReportBase64FilePath,
|
1116
|
+
scanIssuesSummaryJsonFilePath,
|
1117
|
+
scanIssuesSummaryBase64FilePath,
|
1118
|
+
scanPagesDetailJsonFilePath,
|
1119
|
+
scanPagesDetailBase64FilePath,
|
1120
|
+
scanPagesSummaryJsonFilePath,
|
1121
|
+
scanPagesSummaryBase64FilePath,
|
834
1122
|
scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
|
835
1123
|
scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
|
836
1124
|
};
|
@@ -1015,6 +1303,8 @@ const getTopTenIssues = allIssues => {
|
|
1015
1303
|
const categories = ['mustFix', 'goodToFix'];
|
1016
1304
|
const rulesWithCounts = [];
|
1017
1305
|
|
1306
|
+
// This is no longer required and shall not be maintained in future
|
1307
|
+
/*
|
1018
1308
|
const conformanceLevels = {
|
1019
1309
|
wcag2a: 'A',
|
1020
1310
|
wcag2aa: 'AA',
|
@@ -1022,20 +1312,24 @@ const getTopTenIssues = allIssues => {
|
|
1022
1312
|
wcag22aa: 'AA',
|
1023
1313
|
wcag2aaa: 'AAA',
|
1024
1314
|
};
|
1315
|
+
*/
|
1025
1316
|
|
1026
1317
|
categories.forEach(category => {
|
1027
1318
|
const rules = allIssues.items[category]?.rules || [];
|
1028
1319
|
|
1029
1320
|
rules.forEach(rule => {
|
1321
|
+
// This is not needed anymore since we want to have the clause number too
|
1322
|
+
/*
|
1030
1323
|
const wcagLevel = rule.conformance[0];
|
1031
1324
|
const aLevel = conformanceLevels[wcagLevel] || wcagLevel;
|
1325
|
+
*/
|
1032
1326
|
|
1033
1327
|
rulesWithCounts.push({
|
1034
1328
|
category,
|
1035
1329
|
ruleId: rule.rule,
|
1036
1330
|
description: rule.description,
|
1037
1331
|
axeImpact: rule.axeImpact,
|
1038
|
-
conformance:
|
1332
|
+
conformance: rule.conformance,
|
1039
1333
|
totalItems: rule.totalItems,
|
1040
1334
|
});
|
1041
1335
|
});
|
@@ -1047,48 +1341,71 @@ const getTopTenIssues = allIssues => {
|
|
1047
1341
|
};
|
1048
1342
|
|
1049
1343
|
const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
1344
|
+
// Create a map that will sum items only from mustFix, goodToFix, and needsReview.
|
1050
1345
|
const urlOccurrencesMap = new Map<string, number>();
|
1051
1346
|
|
1052
|
-
|
1347
|
+
// Iterate over all categories; update the map only if the category is not "passed"
|
1348
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach((category) => {
|
1349
|
+
// Accumulate totalItems regardless of category.
|
1053
1350
|
allIssues.totalItems += allIssues.items[category].totalItems;
|
1054
1351
|
|
1055
1352
|
allIssues.items[category].rules = Object.entries(allIssues.items[category].rules)
|
1056
|
-
.map(ruleEntry => {
|
1353
|
+
.map((ruleEntry) => {
|
1057
1354
|
const [rule, ruleInfo] = ruleEntry as [string, RuleInfo];
|
1058
1355
|
ruleInfo.pagesAffected = Object.entries(ruleInfo.pagesAffected)
|
1059
|
-
.map(pageEntry => {
|
1356
|
+
.map((pageEntry) => {
|
1060
1357
|
if (isCustomFlow) {
|
1061
1358
|
const [pageIndex, pageInfo] = pageEntry as unknown as [number, PageInfo];
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1359
|
+
// Only update the occurrences map if not passed.
|
1360
|
+
if (category !== 'passed') {
|
1361
|
+
urlOccurrencesMap.set(
|
1362
|
+
pageInfo.url!,
|
1363
|
+
(urlOccurrencesMap.get(pageInfo.url!) || 0) + pageInfo.items.length
|
1364
|
+
);
|
1365
|
+
}
|
1066
1366
|
return { pageIndex, ...pageInfo };
|
1367
|
+
} else {
|
1368
|
+
const [url, pageInfo] = pageEntry as unknown as [string, PageInfo];
|
1369
|
+
if (category !== 'passed') {
|
1370
|
+
urlOccurrencesMap.set(
|
1371
|
+
url,
|
1372
|
+
(urlOccurrencesMap.get(url) || 0) + pageInfo.items.length
|
1373
|
+
);
|
1374
|
+
}
|
1375
|
+
return { url, ...pageInfo };
|
1067
1376
|
}
|
1068
|
-
const [url, pageInfo] = pageEntry as unknown as [string, PageInfo];
|
1069
|
-
urlOccurrencesMap.set(url, (urlOccurrencesMap.get(url) || 0) + pageInfo.items.length);
|
1070
|
-
return { url, ...pageInfo };
|
1071
1377
|
})
|
1378
|
+
// Sort pages so that those with the most items come first
|
1072
1379
|
.sort((page1, page2) => page2.items.length - page1.items.length);
|
1073
1380
|
return { rule, ...ruleInfo };
|
1074
1381
|
})
|
1382
|
+
// Sort the rules by totalItems (descending)
|
1075
1383
|
.sort((rule1, rule2) => rule2.totalItems - rule1.totalItems);
|
1076
1384
|
});
|
1077
1385
|
|
1078
|
-
|
1079
|
-
|
1080
|
-
issue.totalOccurrences = urlOccurrencesMap.get(issue.url) || 0;
|
1081
|
-
});
|
1082
|
-
};
|
1083
|
-
|
1084
|
-
allIssues.topFiveMostIssues.sort((page1, page2) => page2.totalIssues - page1.totalIssues);
|
1386
|
+
// Sort top pages (assumes topFiveMostIssues is already populated)
|
1387
|
+
allIssues.topFiveMostIssues.sort((p1, p2) => p2.totalIssues - p1.totalIssues);
|
1085
1388
|
allIssues.topTenPagesWithMostIssues = allIssues.topFiveMostIssues.slice(0, 10);
|
1086
1389
|
allIssues.topFiveMostIssues = allIssues.topFiveMostIssues.slice(0, 5);
|
1087
|
-
|
1390
|
+
|
1391
|
+
// Update each issue in topTenPagesWithMostIssues with the computed occurrences,
|
1392
|
+
// excluding passed items.
|
1393
|
+
updateIssuesWithOccurrences(allIssues.topTenPagesWithMostIssues, urlOccurrencesMap);
|
1394
|
+
|
1395
|
+
// Get and assign the topTenIssues (using your existing helper)
|
1088
1396
|
const topTenIssues = getTopTenIssues(allIssues);
|
1089
1397
|
allIssues.topTenIssues = topTenIssues;
|
1090
1398
|
};
|
1091
1399
|
|
1400
|
+
// Helper: Update totalOccurrences for each issue using our urlOccurrencesMap.
|
1401
|
+
// For pages that have only passed items, the map will return undefined, so default to 0.
|
1402
|
+
function updateIssuesWithOccurrences(issuesList: any[], urlOccurrencesMap: Map<string, number>) {
|
1403
|
+
issuesList.forEach((issue) => {
|
1404
|
+
issue.totalOccurrences = urlOccurrencesMap.get(issue.url) || 0;
|
1405
|
+
});
|
1406
|
+
}
|
1407
|
+
|
1408
|
+
|
1092
1409
|
const createRuleIdJson = allIssues => {
|
1093
1410
|
const compiledRuleJson = {};
|
1094
1411
|
|
@@ -1117,7 +1434,7 @@ const createRuleIdJson = allIssues => {
|
|
1117
1434
|
return compiledRuleJson;
|
1118
1435
|
};
|
1119
1436
|
|
1120
|
-
const moveElemScreenshots = (randomToken, storagePath) => {
|
1437
|
+
const moveElemScreenshots = (randomToken: string, storagePath: string) => {
|
1121
1438
|
const currentScreenshotsPath = `${randomToken}/elemScreenshots`;
|
1122
1439
|
const resultsScreenshotsPath = `${storagePath}/elemScreenshots`;
|
1123
1440
|
if (fs.existsSync(currentScreenshotsPath)) {
|
@@ -1126,20 +1443,33 @@ const moveElemScreenshots = (randomToken, storagePath) => {
|
|
1126
1443
|
};
|
1127
1444
|
|
1128
1445
|
const generateArtifacts = async (
|
1129
|
-
randomToken,
|
1130
|
-
urlScanned,
|
1131
|
-
scanType,
|
1132
|
-
viewport,
|
1133
|
-
pagesScanned,
|
1134
|
-
pagesNotScanned,
|
1135
|
-
customFlowLabel,
|
1136
|
-
cypressScanAboutMetadata
|
1137
|
-
|
1138
|
-
|
1446
|
+
randomToken: string,
|
1447
|
+
urlScanned: string,
|
1448
|
+
scanType: ScannerTypes,
|
1449
|
+
viewport: string,
|
1450
|
+
pagesScanned: PageInfo[],
|
1451
|
+
pagesNotScanned: PageInfo[],
|
1452
|
+
customFlowLabel: string,
|
1453
|
+
cypressScanAboutMetadata: {
|
1454
|
+
browser?: string;
|
1455
|
+
viewport: { width: number; height: number };
|
1456
|
+
},
|
1457
|
+
scanDetails: {
|
1458
|
+
startTime: Date;
|
1459
|
+
endTime: Date;
|
1460
|
+
deviceChosen: string;
|
1461
|
+
isIncludeScreenshots: boolean;
|
1462
|
+
isAllowSubdomains: string;
|
1463
|
+
isEnableCustomChecks: string[];
|
1464
|
+
isEnableWcagAaa: string[];
|
1465
|
+
isSlowScanMode: number;
|
1466
|
+
isAdhereRobots: boolean;
|
1467
|
+
},
|
1468
|
+
zip: string = undefined, // optional
|
1139
1469
|
generateJsonFiles = false,
|
1140
1470
|
) => {
|
1141
1471
|
const intermediateDatasetsPath = `${randomToken}/datasets/${randomToken}`;
|
1142
|
-
const
|
1472
|
+
const oobeeAppVersion = getVersion();
|
1143
1473
|
const storagePath = getStoragePath(randomToken);
|
1144
1474
|
|
1145
1475
|
urlScanned =
|
@@ -1147,7 +1477,7 @@ const generateArtifacts = async (
|
|
1147
1477
|
? urlScanned
|
1148
1478
|
: urlWithoutAuth(urlScanned);
|
1149
1479
|
|
1150
|
-
const formatAboutStartTime = dateString => {
|
1480
|
+
const formatAboutStartTime = (dateString: string) => {
|
1151
1481
|
const utcStartTimeDate = new Date(dateString);
|
1152
1482
|
const formattedStartTime = utcStartTimeDate.toLocaleTimeString('en-GB', {
|
1153
1483
|
year: 'numeric',
|
@@ -1200,7 +1530,7 @@ const generateArtifacts = async (
|
|
1200
1530
|
topTenIssues: [],
|
1201
1531
|
wcagViolations: [],
|
1202
1532
|
customFlowLabel,
|
1203
|
-
|
1533
|
+
oobeeAppVersion,
|
1204
1534
|
items: {
|
1205
1535
|
mustFix: {
|
1206
1536
|
description: itemTypeDescription.mustFix,
|
@@ -1325,6 +1655,12 @@ const generateArtifacts = async (
|
|
1325
1655
|
scanItemsSummaryBase64FilePath,
|
1326
1656
|
scanItemsMiniReportJsonFilePath,
|
1327
1657
|
scanItemsMiniReportBase64FilePath,
|
1658
|
+
scanIssuesSummaryJsonFilePath,
|
1659
|
+
scanIssuesSummaryBase64FilePath,
|
1660
|
+
scanPagesDetailJsonFilePath,
|
1661
|
+
scanPagesDetailBase64FilePath,
|
1662
|
+
scanPagesSummaryJsonFilePath,
|
1663
|
+
scanPagesSummaryBase64FilePath,
|
1328
1664
|
scanDataJsonFileSize,
|
1329
1665
|
scanItemsJsonFileSize,
|
1330
1666
|
} = await writeJsonAndBase64Files(allIssues, storagePath);
|
@@ -1355,6 +1691,14 @@ const generateArtifacts = async (
|
|
1355
1691
|
scanItemsBase64FilePath,
|
1356
1692
|
scanItemsSummaryJsonFilePath,
|
1357
1693
|
scanItemsSummaryBase64FilePath,
|
1694
|
+
scanItemsMiniReportJsonFilePath,
|
1695
|
+
scanItemsMiniReportBase64FilePath,
|
1696
|
+
scanIssuesSummaryJsonFilePath,
|
1697
|
+
scanIssuesSummaryBase64FilePath,
|
1698
|
+
scanPagesDetailJsonFilePath,
|
1699
|
+
scanPagesDetailBase64FilePath,
|
1700
|
+
scanPagesSummaryJsonFilePath,
|
1701
|
+
scanPagesSummaryBase64FilePath,
|
1358
1702
|
]);
|
1359
1703
|
}
|
1360
1704
|
|
@@ -1378,13 +1722,7 @@ const generateArtifacts = async (
|
|
1378
1722
|
`Results directory is at ${storagePath}`,
|
1379
1723
|
];
|
1380
1724
|
|
1381
|
-
if (process.env.
|
1382
|
-
messageToDisplay.push(
|
1383
|
-
'Reports have been further broken down according to their respective impact level.',
|
1384
|
-
);
|
1385
|
-
}
|
1386
|
-
|
1387
|
-
if (process.send && process.env.OOBEE_VERBOSE && process.env.REPORT_BREAKDOWN != '1') {
|
1725
|
+
if (process.send && process.env.OOBEE_VERBOSE) {
|
1388
1726
|
const zipFileNameMessage = {
|
1389
1727
|
type: 'zipFileName',
|
1390
1728
|
payload: `${constants.cliZipFileName}`,
|