@govtechsg/oobee 0.10.34 → 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.
Files changed (40) hide show
  1. package/.vscode/settings.json +1 -1
  2. package/DETAILS.md +58 -42
  3. package/INTEGRATION.md +142 -53
  4. package/README.md +15 -0
  5. package/__mocks__/mock-report.html +1 -1
  6. package/exclusions.txt +4 -1
  7. package/package.json +2 -2
  8. package/src/constants/cliFunctions.ts +0 -7
  9. package/src/constants/common.ts +39 -1
  10. package/src/constants/constants.ts +9 -8
  11. package/src/constants/itemTypeDescription.ts +3 -3
  12. package/src/crawlers/commonCrawlerFunc.ts +67 -214
  13. package/src/crawlers/crawlDomain.ts +6 -2
  14. package/src/crawlers/crawlLocalFile.ts +2 -0
  15. package/src/crawlers/crawlSitemap.ts +5 -3
  16. package/src/crawlers/custom/escapeCssSelector.ts +10 -0
  17. package/src/crawlers/custom/evaluateAltText.ts +13 -0
  18. package/src/crawlers/custom/extractAndGradeText.ts +0 -2
  19. package/src/crawlers/custom/extractText.ts +28 -0
  20. package/src/crawlers/custom/findElementByCssSelector.ts +46 -0
  21. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +1006 -901
  22. package/src/crawlers/custom/framesCheck.ts +51 -0
  23. package/src/crawlers/custom/getAxeConfiguration.ts +126 -0
  24. package/src/crawlers/custom/gradeReadability.ts +30 -0
  25. package/src/crawlers/custom/xPathToCss.ts +178 -0
  26. package/src/mergeAxeResults.ts +503 -132
  27. package/src/npmIndex.ts +130 -62
  28. package/src/static/ejs/partials/components/ruleOffcanvas.ejs +1 -1
  29. package/src/static/ejs/partials/components/scanAbout.ejs +1 -1
  30. package/src/static/ejs/partials/components/summaryScanResults.ejs +1 -1
  31. package/src/static/ejs/partials/components/wcagCompliance.ejs +3 -2
  32. package/src/static/ejs/partials/footer.ejs +13 -7
  33. package/src/static/ejs/partials/scripts/reportSearch.ejs +112 -74
  34. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +2 -2
  35. package/src/static/ejs/partials/scripts/utils.ejs +1 -1
  36. package/src/static/ejs/partials/summaryMain.ejs +6 -6
  37. package/src/static/ejs/report.ejs +5 -5
  38. package/src/utils.ts +29 -10
  39. package/src/xPathToCssCypress.ts +178 -0
  40. package/src/crawlers/customAxeFunctions.ts +0 -82
@@ -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, prepareData } from './constants/common.js';
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: ItemsInfo[];
37
+ export type PageInfo = {
38
+ items?: ItemsInfo[];
39
39
  itemsCount?: number;
40
40
  pageTitle: string;
41
- url?: string;
41
+ url: string;
42
+ actualUrl: string;
42
43
  pageImagePath?: string;
43
44
  pageIndex?: number;
44
- metadata: string;
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
- phAppVersion: string;
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: string;
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
- // we filter out the below as it represents the A/AA/AAA level, not the clause itself
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 = clausesArr.join(',');
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
-
589
- if (pagesAffected && Array.isArray(pagesAffected)) {
590
- queueWrite(' "pagesAffected": [\n');
591
-
592
- pagesAffected.forEach((page, p) => {
593
- const pageJson = JSON.stringify(page, null, 2)
594
- .split('\n')
595
- .map((line, idx) => (idx === 0 ? ` ${line}` : ` ${line}`))
596
- .join('\n');
597
549
 
598
- queueWrite(pageJson);
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
- if (p < pagesAffected.length - 1) {
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
- queueWrite(' ]');
608
- }
595
+ if (pagesAffected && Array.isArray(pagesAffected)) {
596
+ queueWrite(' "pagesAffected": [\n');
609
597
 
610
- queueWrite('\n }');
611
- if (j < rules.length - 1) {
612
- queueWrite(',\n');
613
- } else {
614
- queueWrite('\n');
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');
@@ -725,6 +734,14 @@ const writeJsonAndBase64Files = async (
725
734
  scanItemsBase64FilePath: string;
726
735
  scanItemsSummaryJsonFilePath: string;
727
736
  scanItemsSummaryBase64FilePath: string;
737
+ scanItemsMiniReportJsonFilePath: string;
738
+ scanItemsMiniReportBase64FilePath: string;
739
+ scanIssuesSummaryJsonFilePath: string;
740
+ scanIssuesSummaryBase64FilePath: string;
741
+ scanPagesDetailJsonFilePath: string;
742
+ scanPagesDetailBase64FilePath: string;
743
+ scanPagesSummaryJsonFilePath: string;
744
+ scanPagesSummaryBase64FilePath: string;
728
745
  scanDataJsonFileSize: number;
729
746
  scanItemsJsonFileSize: number;
730
747
  }> => {
@@ -732,7 +749,33 @@ const writeJsonAndBase64Files = async (
732
749
  const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } =
733
750
  await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
734
751
  const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } =
735
- 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');
736
779
 
737
780
  // scanItemsSummary
738
781
  // the below mutates the original items object, since it is expensive to clone
@@ -777,7 +820,7 @@ const writeJsonAndBase64Files = async (
777
820
  topTenIssues,
778
821
  } = rest;
779
822
 
780
- const summaryItems = {
823
+ const summaryItemsMini = {
781
824
  ...items,
782
825
  pagesScanned,
783
826
  topTenPagesWithMostIssues,
@@ -789,10 +832,277 @@ const writeJsonAndBase64Files = async (
789
832
  topTenIssues,
790
833
  };
791
834
 
835
+ const {
836
+ jsonFilePath: scanItemsMiniReportJsonFilePath,
837
+ base64FilePath: scanItemsMiniReportBase64FilePath,
838
+ } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...summaryItemsMini }, storagePath, 'scanItemsSummaryMiniReport');
839
+ const summaryItems = {
840
+ mustFix: {
841
+ totalItems: items.mustFix?.totalItems || 0,
842
+ totalRuleIssues: items.mustFix?.totalRuleIssues || 0,
843
+ },
844
+ goodToFix: {
845
+ totalItems: items.goodToFix?.totalItems || 0,
846
+ totalRuleIssues: items.goodToFix?.totalRuleIssues || 0,
847
+ },
848
+ needsReview: {
849
+ totalItems: items.needsReview?.totalItems || 0,
850
+ totalRuleIssues: items.needsReview?.totalRuleIssues || 0,
851
+ },
852
+ topTenPagesWithMostIssues,
853
+ wcagLinks,
854
+ wcagPassPercentage,
855
+ totalPagesScanned,
856
+ totalPagesNotScanned,
857
+ topTenIssues,
858
+ };
859
+
792
860
  const {
793
861
  jsonFilePath: scanItemsSummaryJsonFilePath,
794
862
  base64FilePath: scanItemsSummaryBase64FilePath,
795
- } = 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');
796
1106
 
797
1107
  return {
798
1108
  scanDataJsonFilePath,
@@ -801,6 +1111,14 @@ const writeJsonAndBase64Files = async (
801
1111
  scanItemsBase64FilePath,
802
1112
  scanItemsSummaryJsonFilePath,
803
1113
  scanItemsSummaryBase64FilePath,
1114
+ scanItemsMiniReportJsonFilePath,
1115
+ scanItemsMiniReportBase64FilePath,
1116
+ scanIssuesSummaryJsonFilePath,
1117
+ scanIssuesSummaryBase64FilePath,
1118
+ scanPagesDetailJsonFilePath,
1119
+ scanPagesDetailBase64FilePath,
1120
+ scanPagesSummaryJsonFilePath,
1121
+ scanPagesSummaryBase64FilePath,
804
1122
  scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
805
1123
  scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
806
1124
  };
@@ -985,6 +1303,8 @@ const getTopTenIssues = allIssues => {
985
1303
  const categories = ['mustFix', 'goodToFix'];
986
1304
  const rulesWithCounts = [];
987
1305
 
1306
+ // This is no longer required and shall not be maintained in future
1307
+ /*
988
1308
  const conformanceLevels = {
989
1309
  wcag2a: 'A',
990
1310
  wcag2aa: 'AA',
@@ -992,20 +1312,24 @@ const getTopTenIssues = allIssues => {
992
1312
  wcag22aa: 'AA',
993
1313
  wcag2aaa: 'AAA',
994
1314
  };
1315
+ */
995
1316
 
996
1317
  categories.forEach(category => {
997
1318
  const rules = allIssues.items[category]?.rules || [];
998
1319
 
999
1320
  rules.forEach(rule => {
1321
+ // This is not needed anymore since we want to have the clause number too
1322
+ /*
1000
1323
  const wcagLevel = rule.conformance[0];
1001
1324
  const aLevel = conformanceLevels[wcagLevel] || wcagLevel;
1325
+ */
1002
1326
 
1003
1327
  rulesWithCounts.push({
1004
1328
  category,
1005
1329
  ruleId: rule.rule,
1006
1330
  description: rule.description,
1007
1331
  axeImpact: rule.axeImpact,
1008
- conformance: aLevel,
1332
+ conformance: rule.conformance,
1009
1333
  totalItems: rule.totalItems,
1010
1334
  });
1011
1335
  });
@@ -1017,48 +1341,71 @@ const getTopTenIssues = allIssues => {
1017
1341
  };
1018
1342
 
1019
1343
  const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
1344
+ // Create a map that will sum items only from mustFix, goodToFix, and needsReview.
1020
1345
  const urlOccurrencesMap = new Map<string, number>();
1021
1346
 
1022
- ['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
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.
1023
1350
  allIssues.totalItems += allIssues.items[category].totalItems;
1024
1351
 
1025
1352
  allIssues.items[category].rules = Object.entries(allIssues.items[category].rules)
1026
- .map(ruleEntry => {
1353
+ .map((ruleEntry) => {
1027
1354
  const [rule, ruleInfo] = ruleEntry as [string, RuleInfo];
1028
1355
  ruleInfo.pagesAffected = Object.entries(ruleInfo.pagesAffected)
1029
- .map(pageEntry => {
1356
+ .map((pageEntry) => {
1030
1357
  if (isCustomFlow) {
1031
1358
  const [pageIndex, pageInfo] = pageEntry as unknown as [number, PageInfo];
1032
- urlOccurrencesMap.set(
1033
- pageInfo.url!,
1034
- (urlOccurrencesMap.get(pageInfo.url!) || 0) + pageInfo.items.length,
1035
- );
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
+ }
1036
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 };
1037
1376
  }
1038
- const [url, pageInfo] = pageEntry as unknown as [string, PageInfo];
1039
- urlOccurrencesMap.set(url, (urlOccurrencesMap.get(url) || 0) + pageInfo.items.length);
1040
- return { url, ...pageInfo };
1041
1377
  })
1378
+ // Sort pages so that those with the most items come first
1042
1379
  .sort((page1, page2) => page2.items.length - page1.items.length);
1043
1380
  return { rule, ...ruleInfo };
1044
1381
  })
1382
+ // Sort the rules by totalItems (descending)
1045
1383
  .sort((rule1, rule2) => rule2.totalItems - rule1.totalItems);
1046
1384
  });
1047
1385
 
1048
- const updateIssuesWithOccurrences = (issuesList: Array<any>) => {
1049
- issuesList.forEach(issue => {
1050
- issue.totalOccurrences = urlOccurrencesMap.get(issue.url) || 0;
1051
- });
1052
- };
1053
-
1054
- allIssues.topFiveMostIssues.sort((page1, page2) => page2.totalIssues - page1.totalIssues);
1055
- allIssues.topFiveMostIssues = allIssues.topFiveMostIssues.slice(0, 5);
1386
+ // Sort top pages (assumes topFiveMostIssues is already populated)
1387
+ allIssues.topFiveMostIssues.sort((p1, p2) => p2.totalIssues - p1.totalIssues);
1056
1388
  allIssues.topTenPagesWithMostIssues = allIssues.topFiveMostIssues.slice(0, 10);
1057
- updateIssuesWithOccurrences(allIssues.topTenPagesWithMostIssues);
1389
+ allIssues.topFiveMostIssues = allIssues.topFiveMostIssues.slice(0, 5);
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)
1058
1396
  const topTenIssues = getTopTenIssues(allIssues);
1059
1397
  allIssues.topTenIssues = topTenIssues;
1060
1398
  };
1061
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
+
1062
1409
  const createRuleIdJson = allIssues => {
1063
1410
  const compiledRuleJson = {};
1064
1411
 
@@ -1087,7 +1434,7 @@ const createRuleIdJson = allIssues => {
1087
1434
  return compiledRuleJson;
1088
1435
  };
1089
1436
 
1090
- const moveElemScreenshots = (randomToken, storagePath) => {
1437
+ const moveElemScreenshots = (randomToken: string, storagePath: string) => {
1091
1438
  const currentScreenshotsPath = `${randomToken}/elemScreenshots`;
1092
1439
  const resultsScreenshotsPath = `${storagePath}/elemScreenshots`;
1093
1440
  if (fs.existsSync(currentScreenshotsPath)) {
@@ -1096,20 +1443,33 @@ const moveElemScreenshots = (randomToken, storagePath) => {
1096
1443
  };
1097
1444
 
1098
1445
  const generateArtifacts = async (
1099
- randomToken,
1100
- urlScanned,
1101
- scanType,
1102
- viewport,
1103
- pagesScanned,
1104
- pagesNotScanned,
1105
- customFlowLabel,
1106
- cypressScanAboutMetadata,
1107
- scanDetails,
1108
- zip = undefined, // optional
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
1109
1469
  generateJsonFiles = false,
1110
1470
  ) => {
1111
1471
  const intermediateDatasetsPath = `${randomToken}/datasets/${randomToken}`;
1112
- const phAppVersion = getVersion();
1472
+ const oobeeAppVersion = getVersion();
1113
1473
  const storagePath = getStoragePath(randomToken);
1114
1474
 
1115
1475
  urlScanned =
@@ -1117,7 +1477,7 @@ const generateArtifacts = async (
1117
1477
  ? urlScanned
1118
1478
  : urlWithoutAuth(urlScanned);
1119
1479
 
1120
- const formatAboutStartTime = dateString => {
1480
+ const formatAboutStartTime = (dateString: string) => {
1121
1481
  const utcStartTimeDate = new Date(dateString);
1122
1482
  const formattedStartTime = utcStartTimeDate.toLocaleTimeString('en-GB', {
1123
1483
  year: 'numeric',
@@ -1170,7 +1530,7 @@ const generateArtifacts = async (
1170
1530
  topTenIssues: [],
1171
1531
  wcagViolations: [],
1172
1532
  customFlowLabel,
1173
- phAppVersion,
1533
+ oobeeAppVersion,
1174
1534
  items: {
1175
1535
  mustFix: {
1176
1536
  description: itemTypeDescription.mustFix,
@@ -1234,7 +1594,7 @@ const generateArtifacts = async (
1234
1594
  '',
1235
1595
  `Must Fix: ${allIssues.items.mustFix.rules.length} ${Object.keys(allIssues.items.mustFix.rules).length === 1 ? 'issue' : 'issues'} / ${allIssues.items.mustFix.totalItems} ${allIssues.items.mustFix.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
1236
1596
  `Good to Fix: ${allIssues.items.goodToFix.rules.length} ${Object.keys(allIssues.items.goodToFix.rules).length === 1 ? 'issue' : 'issues'} / ${allIssues.items.goodToFix.totalItems} ${allIssues.items.goodToFix.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
1237
- `Needs Review: ${allIssues.items.needsReview.rules.length} ${Object.keys(allIssues.items.needsReview.rules).length === 1 ? 'issue' : 'issues'} / ${allIssues.items.needsReview.totalItems} ${allIssues.items.needsReview.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
1597
+ `Manual Review Required: ${allIssues.items.needsReview.rules.length} ${Object.keys(allIssues.items.needsReview.rules).length === 1 ? 'issue' : 'issues'} / ${allIssues.items.needsReview.totalItems} ${allIssues.items.needsReview.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
1238
1598
  `Passed: ${allIssues.items.passed.totalItems} ${allIssues.items.passed.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
1239
1599
  ]);
1240
1600
 
@@ -1244,7 +1604,7 @@ const generateArtifacts = async (
1244
1604
  createScreenshotsFolder(randomToken);
1245
1605
  }
1246
1606
 
1247
- allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations);
1607
+ allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
1248
1608
  consoleLogger.info(
1249
1609
  `advancedScanOptionsSummaryItems is ${allIssues.advancedScanOptionsSummaryItems}`,
1250
1610
  );
@@ -1293,6 +1653,14 @@ const generateArtifacts = async (
1293
1653
  scanItemsBase64FilePath,
1294
1654
  scanItemsSummaryJsonFilePath,
1295
1655
  scanItemsSummaryBase64FilePath,
1656
+ scanItemsMiniReportJsonFilePath,
1657
+ scanItemsMiniReportBase64FilePath,
1658
+ scanIssuesSummaryJsonFilePath,
1659
+ scanIssuesSummaryBase64FilePath,
1660
+ scanPagesDetailJsonFilePath,
1661
+ scanPagesDetailBase64FilePath,
1662
+ scanPagesSummaryJsonFilePath,
1663
+ scanPagesSummaryBase64FilePath,
1296
1664
  scanDataJsonFileSize,
1297
1665
  scanItemsJsonFileSize,
1298
1666
  } = await writeJsonAndBase64Files(allIssues, storagePath);
@@ -1306,12 +1674,13 @@ const generateArtifacts = async (
1306
1674
  storagePath,
1307
1675
  );
1308
1676
  await writeSummaryHTML(allIssues, storagePath);
1677
+
1309
1678
  await writeHTML(
1310
1679
  allIssues,
1311
1680
  storagePath,
1312
1681
  'report',
1313
1682
  scanDataBase64FilePath,
1314
- resultsTooBig ? scanItemsSummaryBase64FilePath : scanItemsBase64FilePath,
1683
+ resultsTooBig ? scanItemsMiniReportBase64FilePath : scanItemsBase64FilePath,
1315
1684
  );
1316
1685
 
1317
1686
  if (!generateJsonFiles) {
@@ -1322,6 +1691,14 @@ const generateArtifacts = async (
1322
1691
  scanItemsBase64FilePath,
1323
1692
  scanItemsSummaryJsonFilePath,
1324
1693
  scanItemsSummaryBase64FilePath,
1694
+ scanItemsMiniReportJsonFilePath,
1695
+ scanItemsMiniReportBase64FilePath,
1696
+ scanIssuesSummaryJsonFilePath,
1697
+ scanIssuesSummaryBase64FilePath,
1698
+ scanPagesDetailJsonFilePath,
1699
+ scanPagesDetailBase64FilePath,
1700
+ scanPagesSummaryJsonFilePath,
1701
+ scanPagesSummaryBase64FilePath,
1325
1702
  ]);
1326
1703
  }
1327
1704
 
@@ -1345,13 +1722,7 @@ const generateArtifacts = async (
1345
1722
  `Results directory is at ${storagePath}`,
1346
1723
  ];
1347
1724
 
1348
- if (process.env.REPORT_BREAKDOWN === '1') {
1349
- messageToDisplay.push(
1350
- 'Reports have been further broken down according to their respective impact level.',
1351
- );
1352
- }
1353
-
1354
- if (process.send && process.env.OOBEE_VERBOSE && process.env.REPORT_BREAKDOWN != '1') {
1725
+ if (process.send && process.env.OOBEE_VERBOSE) {
1355
1726
  const zipFileNameMessage = {
1356
1727
  type: 'zipFileName',
1357
1728
  payload: `${constants.cliZipFileName}`,