@govtechsg/oobee 0.10.33 → 0.10.36

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.
@@ -6,5 +6,5 @@
6
6
  "javascript",
7
7
  "typescript",
8
8
  ],
9
- "editor.formatOnSave": true,
9
+ "editor.formatOnSave": false,
10
10
  }
package/DETAILS.md CHANGED
@@ -14,37 +14,53 @@ Details of each issue and severity rating provided by the current scan engine.
14
14
 
15
15
  ## Conformance Covered
16
16
 
17
+ #### Definitions of Conformance Level, Must Fix, Good To Fix, Manual Review Required Required
18
+
19
+ In Oobee, issues are grouped into one of three categories:
20
+ - **Must Fix** issues includes WCAG A & AA success criteria (excluding those requiring review).
21
+ - **Good To Fix** issues includes WCAG Level AAA success criteria issues and all best practice rules that do not necessarily conform to WCAG success criterion but are industry accepted practices that improve the user experience.
22
+ - **Manual Review Required Required** occurrences could potentially be false positive, requiring human validation for accuracy.
23
+
17
24
  Note: Level AAA are disabled by default. Please specify `enable-wcag-aaa` in ruleset flag to enable AAA rules.
18
25
 
19
- | Conformance | Level |
20
- |-------------|-------|
21
- | WCAG 1.1.1 | A |
22
- | WCAG 1.2.2 | A |
23
- | WCAG 1.3.1 | A |
24
- | WCAG 1.3.5 | AA |
25
- | WCAG 1.4.1 | A |
26
- | WCAG 1.4.2 | A |
27
- | WCAG 1.4.3 | AA |
28
- | WCAG 1.4.4 | AA |
29
- | WCAG 1.4.6 | AAA |
30
- | WCAG 1.4.12 | AA |
31
- | WCAG 2.1.1 | A |
32
- | WCAG 2.2.1 | A |
33
- | WCAG 2.2.2 | A |
34
- | WCAG 2.2.4 | AAA |
35
- | WCAG 2.4.1 | A |
36
- | WCAG 2.4.2 | A |
37
- | WCAG 2.4.4 | A |
38
- | WCAG 2.4.9 | AAA |
39
- | WCAG 2.5.8 | AA |
40
- | WCAG 3.1.1 | A |
41
- | WCAG 3.1.2 | AA |
42
- | WCAG 3.1.5 | AAA |
43
- | WCAG 3.2.5 | AAA |
44
- | WCAG 3.3.2 | A |
45
- | WCAG 4.1.2 | A |
46
-
47
- ### Summary
26
+ #### WCAG Level of Conformance
27
+
28
+ - **Level A**: The minimum level of accessibility, addressing the most critical issues.
29
+ - **Level AA**: Builds on Level A, adding more accessibility features. This is the standard most organizations aim for, as it provides reasonable accessibility for most users.
30
+ - **Level AAA**: The highest level of accessibility, encompassing all Level A and AA criteria plus additional stringent requirements. While ideal, it's often not practical or possible for all content. Examples include providing sign language interpretation for all pre-recorded videos.
31
+
32
+ #### Breakdown of WCAG Clauses and Best Practice
33
+
34
+ | Conformance | Level | Must Fix | Good to Fix | Exclusive to Manual Review Required |
35
+ |-------------|-------|----------|-------------|--------------|
36
+ | WCAG 1.1.1 | A | Yes | | |
37
+ | WCAG 1.2.2 | A | Yes | | |
38
+ | WCAG 1.3.1 | A | Yes | | |
39
+ | WCAG 1.3.5 | AA | Yes | | |
40
+ | WCAG 1.4.1 | A | Yes | | |
41
+ | WCAG 1.4.2 | A | Yes | | |
42
+ | WCAG 1.4.3 | AA | Yes | | |
43
+ | WCAG 1.4.4 | AA | Yes | | |
44
+ | WCAG 1.4.6 | AAA | Yes | | |
45
+ | WCAG 1.4.12 | AA | Yes | | |
46
+ | WCAG 2.1.1 | A | Yes | | |
47
+ | WCAG 2.2.1 | A | Yes | | |
48
+ | WCAG 2.2.2 | A | Yes | | |
49
+ | WCAG 2.2.4 | AAA | | Yes | |
50
+ | WCAG 2.4.1 | A | Yes | | |
51
+ | WCAG 2.4.2 | A | Yes | | |
52
+ | WCAG 2.4.4 | A | Yes | | |
53
+ | WCAG 2.4.9 | AAA | | Yes | |
54
+ | WCAG 2.5.8 | AA | Yes | | |
55
+ | WCAG 3.1.1 | A | Yes | | |
56
+ | WCAG 3.1.2 | AA | Yes | | |
57
+ | WCAG 3.1.5 | AAA | | | Yes |
58
+ | WCAG 3.2.5 | AAA | | Yes | |
59
+ | WCAG 3.3.2 | A | Yes | | |
60
+ | WCAG 4.1.2 | A | Yes | | |
61
+ | Best Practice| | | Yes | |
62
+
63
+ ### Summary of WCAG Clauses Supported
48
64
  | Level | Count |
49
65
  |-------|-------|
50
66
  | A | 15 |
@@ -61,7 +77,7 @@ Note: Level AAA are disabled by default. Please specify `enable-wcag-aaa` in ru
61
77
  | aria-braille-equivalent | Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent | Must Fix | WCAG 4.1.2 |
62
78
  | aria-command-name | Ensures every ARIA button, link and menuitem has an accessible name | Must Fix | WCAG 4.1.2 |
63
79
  | aria-conditional-attr | Ensures ARIA attributes are used as described in the specification of the element's role | Must Fix | WCAG 4.1.2 |
64
- | aria-deprecated-role | Ensures elements do not use deprecated roles | Good To Fix | WCAG 4.1.2 |
80
+ | aria-deprecated-role | Ensures elements do not use deprecated roles | Must Fix | WCAG 4.1.2 |
65
81
  | aria-hidden-body | Ensures aria-hidden="true" is not present on the document body. | Must Fix | WCAG 4.1.2 |
66
82
  | aria-hidden-focus | Ensures aria-hidden elements are not focusable nor contain focusable elements | Must Fix | WCAG 4.1.2 |
67
83
  | aria-input-field-name | Ensures every ARIA input field has an accessible name | Must Fix | WCAG 4.1.2 |
@@ -89,7 +105,7 @@ Note: Level AAA are disabled by default. Please specify `enable-wcag-aaa` in ru
89
105
  | frame-title | Ensures `<iframe>` and `<frame>` elements have an accessible name | Must Fix | WCAG 4.1.2 |
90
106
  | html-has-lang | Ensures every HTML document has a lang attribute | Must Fix | WCAG 3.1.1 |
91
107
  | html-lang-valid | Ensures the lang attribute of the `<html>` element has a valid value | Must Fix | WCAG 3.1.1 |
92
- | html-xml-lang-mismatch | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Good to Fix | WCAG 3.1.1 |
108
+ | html-xml-lang-mismatch | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Must Fix | WCAG 3.1.1 |
93
109
  | image-alt | Ensures `<img>` elements have alternate text or a role of none or presentation | Must Fix | WCAG 1.1.1 |
94
110
  | input-button-name | Ensures input buttons have discernible text | Must Fix | WCAG 4.1.2 |
95
111
  | input-image-alt | Ensures `<input type="image">` elements have alternate text | Must Fix | WCAG 1.1.1, WCAG 4.1.2 |
@@ -101,12 +117,12 @@ Note: Level AAA are disabled by default. Please specify `enable-wcag-aaa` in ru
101
117
  | marquee | Ensures `<marquee>` elements are not used | Must Fix | WCAG 2.2.2 |
102
118
  | meta-refresh | Ensures `<meta http-equiv="refresh">` is not used for delayed refresh | Must Fix | WCAG 2.2.1 |
103
119
  | nested-interactive | Ensures interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies | Must Fix | WCAG 4.1.2 |
104
- | no-autoplay-audio | Ensures `<video>` or `<audio>` elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio | Good to Fix | WCAG 1.4.2 |
120
+ | no-autoplay-audio | Ensures `<video>` or `<audio>` elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio | Must Fix | WCAG 1.4.2 |
105
121
  | object-alt | Ensures `<object>` elements have alternate text | Must Fix | WCAG 1.1.1 |
106
122
  | role-img-alt | Ensures [role="img"] elements have alternate text | Must Fix | WCAG 1.1.1 |
107
123
  | scrollable-region-focusable | Ensure elements that have scrollable content are accessible by keyboard | Must Fix | WCAG 2.1.1 |
108
124
  | select-name | Ensures select element has an accessible name | Must Fix | WCAG 4.1.2 |
109
- | server-side-image-map | Ensures that server-side image maps are not used | Good to Fix | WCAG 2.1.1 |
125
+ | server-side-image-map | Ensures that server-side image maps are not used | Must Fix | WCAG 2.1.1 |
110
126
  | svg-img-alt | Ensures `<svg>` elements with an img, graphics-document or graphics-symbol role have an accessible text | Must Fix | WCAG 1.1.1 |
111
127
  | td-headers-attr | Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table | Must Fix | WCAG 1.3.1 |
112
128
  | th-has-data-cells | Ensure that `<th>` elements and elements with role=columnheader/rowheader have data cells they describe | Must Fix | WCAG 1.3.1 |
@@ -139,26 +155,26 @@ Note: Level AAA are disabled by default. Please specify `enable-wcag-aaa` in ru
139
155
 
140
156
  | Issue ID | Issue Description | Severity | Conformance |
141
157
  | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------- |
142
- | color-contrast-enhanced | Ensure the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds | Must Fix | WCAG 1.4.6 |
158
+ | color-contrast-enhanced | Ensure the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds | Good to Fix | WCAG 1.4.6 |
143
159
  | identical-links-same-purpose | Ensure that links with the same accessible name serve a similar purpose | Good to Fix | WCAG 2.4.9 |
144
160
  | meta-refresh-no-exceptions | Ensure <meta http-equiv="refresh"> is not used for delayed refresh | Good to Fix | WCAG 2.2.4, WCAG 3.2.5 |
145
- | oobee-grading-text-contents | Text content should be clear and plain to ensure that it is easily understood. | Needs Review | WCAG 3.1.5 |
161
+ | oobee-grading-text-contents | Text content should be clear and plain to ensure that it is easily understood. | Manual Review Required Required | WCAG 3.1.5 |
146
162
 
147
163
  ## Best Practice
148
164
 
149
165
  | Issue ID | Issue Description | Severity |
150
166
  | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
151
- | accesskeys | Ensures every accesskey attribute value is unique | Must Fix |
167
+ | accesskeys | Ensures every accesskey attribute value is unique | Good to Fix |
152
168
  | aria-allowed-role | Ensures role attribute has an appropriate value for the element | Good to Fix |
153
- | aria-dialog-name | Ensures every ARIA dialog and alertdialog node has an accessible name | Must Fix |
154
- | aria-text | Ensures role="text" is used on elements with no focusable descendants | Must Fix |
155
- | aria-treeitem-name | Ensures every ARIA treeitem node has an accessible name | Must Fix |
169
+ | aria-dialog-name | Ensures every ARIA dialog and alertdialog node has an accessible name | Good to Fix |
170
+ | aria-text | Ensures role="text" is used on elements with no focusable descendants | Good to Fix |
171
+ | aria-treeitem-name | Ensures every ARIA treeitem node has an accessible name | Good to Fix |
156
172
  | empty-heading | Ensures headings have discernible text | Good to Fix |
157
173
  | empty-table-header | Ensures table headers have discernible text | Good to Fix |
158
- | frame-tested | Ensures `<iframe>` and `<frame>` elements contain the axe-core script | Must Fix |
174
+ | frame-tested | Ensures `<iframe>` and `<frame>` elements contain the axe-core script | Good to Fix |
159
175
  | heading-order | Ensures the order of headings is semantically correct | Good to Fix |
160
176
  | image-redundant-alt | Ensure image alternative is not repeated as text | Good to Fix |
161
- | label-title-only | Ensures that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Must Fix |
177
+ | label-title-only | Ensures that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Good to Fix |
162
178
  | landmark-banner-is-top-level | Ensures the banner landmark is at top level | Good to Fix |
163
179
  | landmark-complementary-is-top-level | Ensures the complementary landmark or aside is at top level | Good to Fix |
164
180
  | landmark-contentinfo-is-top-level | Ensures the contentinfo landmark is at top level | Good to Fix |
@@ -174,5 +190,5 @@ Note: Level AAA are disabled by default. Please specify `enable-wcag-aaa` in ru
174
190
  | region | Ensures all page content is contained by landmarks | Good to Fix |
175
191
  | scope-attr-valid | Ensures the scope attribute is used correctly on tables | Good to Fix |
176
192
  | skip-link | Ensure all skip links have a focusable target | Good to Fix |
177
- | tabindex | Ensures tabindex attribute values are not greater than 0 | Must Fix |
193
+ | tabindex | Ensures tabindex attribute values are not greater than 0 | Good to Fix |
178
194
  | table-duplicate-name | Ensure the `<caption>` element does not contain the same text as the summary attribute | Good to Fix |
package/Dockerfile CHANGED
@@ -1,6 +1,6 @@
1
1
  # Use Microsoft Playwright image as base image
2
2
  # Node version is v22
3
- FROM mcr.microsoft.com/playwright:v1.50.0-noble
3
+ FROM mcr.microsoft.com/playwright:v1.50.1-noble
4
4
 
5
5
  # Installation of packages for oobee and runner
6
6
  RUN apt-get update && apt-get install -y zip git
@@ -1493,7 +1493,7 @@
1493
1493
  <div class="container-fluid">
1494
1494
  <div class="row text-center pt-2 pb-2">
1495
1495
  <div class="col-sm-6 text-sm-left">
1496
- <a href="mailto:enquiries_HATS@tech.gov.sg">Help us improve</a>
1496
+ <a href="mailto:oobee@wogaa.gov.sg">Help us improve</a>
1497
1497
  <hr class="d-sm-none" />
1498
1498
  </div>
1499
1499
  <div class="col-sm-6 text-sm-right">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@govtechsg/oobee",
3
3
  "main": "dist/npmIndex.js",
4
- "version": "0.10.33",
4
+ "version": "0.10.36",
5
5
  "type": "module",
6
6
  "author": "Government Technology Agency <info@tech.gov.sg>",
7
7
  "dependencies": {
@@ -178,14 +178,14 @@ export const axeScript = path.join(dirname, '../../node_modules/axe-core/axe.min
178
178
  export class UrlsCrawled {
179
179
  toScan: string[] = [];
180
180
  scanned: { url: string; actualUrl: string; pageTitle: string }[] = [];
181
- invalid: string[] = [];
181
+ invalid: { url: string; actualUrl: string; pageTitle: string }[] = [];
182
182
  scannedRedirects: { fromUrl: string; toUrl: string }[] = [];
183
183
  notScannedRedirects: { fromUrl: string; toUrl: string }[] = [];
184
184
  outOfDomain: string[] = [];
185
- blacklisted: string[] = [];
185
+ blacklisted: { url: string; actualUrl: string; pageTitle: string }[] = [];
186
186
  error: { url: string }[] = [];
187
187
  exceededRequests: string[] = [];
188
- forbidden: string[] = [];
188
+ forbidden: { url: string; actualUrl: string; pageTitle: string }[] = [];
189
189
  userExcluded: { url: string; actualUrl: string; pageTitle: string }[] = [];
190
190
  everything: string[] = [];
191
191
 
@@ -1,10 +1,10 @@
1
1
  const itemTypeDescription = {
2
2
  mustFix:
3
- 'Issues that need to be addressed promptly, as they create significant barriers for persons with disabilities and can prevent them from accessing essential content or features.',
3
+ 'Must Fix issues includes WCAG A & AA success criteria (excluding those requiring review).',
4
4
  goodToFix:
5
- 'Issues that could pose certain challenges for persons with disabilities (PWDs), but are unlikely to completely hinder their access to essential content or features.',
5
+ 'Good to Fix issues includes WCAG Level AAA success criteria issues and all best practice rules that do not necessarily conform to WCAG success criterion but are industry accepted practices that improve the user experience.',
6
6
  needsReview:
7
- 'Occurrences could potentially be false positives, requiring human validation for accuracy.',
7
+ 'Manual Review Required occurrences could potentially be false positive, requiring human validation for accuracy.',
8
8
  passed: 'Occurrences that passed the automated checks.',
9
9
  };
10
10
 
@@ -138,9 +138,15 @@ export const filterAxeResults = (
138
138
 
139
139
  nodes.forEach(node => {
140
140
  const { impact } = node;
141
+ const hasWcag2a = conformance.includes('wcag2a');
142
+ const hasWcag2aa = conformance.includes('wcag2aa');
143
+ const hasWcag2aaa = conformance.includes('wcag2aaa');
144
+
141
145
  if (displayNeedsReview) {
142
146
  addTo(needsReview, node);
143
- } else if (impact === 'critical' || impact === 'serious') {
147
+ } else if (hasWcag2aaa) {
148
+ addTo(goodToFix, node);
149
+ } else if (hasWcag2a || hasWcag2aa) {
144
150
  addTo(mustFix, node);
145
151
  } else {
146
152
  addTo(goodToFix, node);
@@ -638,7 +638,12 @@ const crawlDomain = async ({
638
638
  numScanned: urlsCrawled.scanned.length,
639
639
  urlScanned: request.url,
640
640
  });
641
- urlsCrawled.blacklisted.push(request.url);
641
+ urlsCrawled.blacklisted.push({
642
+ url: request.url,
643
+ pageTitle: request.url,
644
+ actualUrl: actualUrl, // i.e. actualUrl
645
+ });
646
+
642
647
  return;
643
648
  }
644
649
  const { pdfFileName, url } = handlePdfDownload(
@@ -662,7 +667,12 @@ const crawlDomain = async ({
662
667
  numScanned: urlsCrawled.scanned.length,
663
668
  urlScanned: request.url,
664
669
  });
665
- urlsCrawled.blacklisted.push(request.url);
670
+ urlsCrawled.blacklisted.push({
671
+ url: request.url,
672
+ pageTitle: request.url,
673
+ actualUrl: actualUrl, // i.e. actualUrl
674
+ });
675
+
666
676
  return;
667
677
  }
668
678
 
@@ -671,7 +681,12 @@ const crawlDomain = async ({
671
681
  numScanned: urlsCrawled.scanned.length,
672
682
  urlScanned: request.url,
673
683
  });
674
- urlsCrawled.blacklisted.push(request.url);
684
+ urlsCrawled.blacklisted.push({
685
+ url: request.url,
686
+ pageTitle: request.url,
687
+ actualUrl: actualUrl, // i.e. actualUrl
688
+ });
689
+
675
690
  return;
676
691
  }
677
692
 
@@ -691,7 +706,12 @@ const crawlDomain = async ({
691
706
  numScanned: urlsCrawled.scanned.length,
692
707
  urlScanned: request.url,
693
708
  });
694
- urlsCrawled.forbidden.push(request.url);
709
+ urlsCrawled.forbidden.push({
710
+ url: request.url,
711
+ pageTitle: request.url,
712
+ actualUrl: actualUrl, // i.e. actualUrl
713
+ });
714
+
695
715
  return;
696
716
  }
697
717
 
@@ -700,7 +720,12 @@ const crawlDomain = async ({
700
720
  numScanned: urlsCrawled.scanned.length,
701
721
  urlScanned: request.url,
702
722
  });
703
- urlsCrawled.invalid.push(request.url);
723
+ urlsCrawled.invalid.push({
724
+ url: request.url,
725
+ pageTitle: request.url,
726
+ actualUrl: actualUrl, // i.e. actualUrl
727
+ });
728
+
704
729
  return;
705
730
  }
706
731
 
@@ -779,7 +804,12 @@ const crawlDomain = async ({
779
804
  numScanned: urlsCrawled.scanned.length,
780
805
  urlScanned: request.url,
781
806
  });
782
- urlsCrawled.blacklisted.push(request.url);
807
+ urlsCrawled.blacklisted.push({
808
+ url: request.url,
809
+ pageTitle: request.url,
810
+ actualUrl: actualUrl, // i.e. actualUrl
811
+ });
812
+
783
813
  }
784
814
 
785
815
  if (followRobots) await getUrlsFromRobotsTxt(request.url, browser);
@@ -250,7 +250,12 @@ const crawlSitemap = async (
250
250
  numScanned: urlsCrawled.scanned.length,
251
251
  urlScanned: request.url,
252
252
  });
253
- urlsCrawled.blacklisted.push(request.url);
253
+ urlsCrawled.blacklisted.push({
254
+ url: request.url,
255
+ pageTitle: request.url,
256
+ actualUrl: actualUrl, // i.e. actualUrl
257
+ });
258
+
254
259
  return;
255
260
  }
256
261
  // pushes download promise into pdfDownloads
@@ -297,7 +302,12 @@ const crawlSitemap = async (
297
302
  numScanned: urlsCrawled.scanned.length,
298
303
  urlScanned: request.url,
299
304
  });
300
- urlsCrawled.invalid.push(request.url);
305
+ urlsCrawled.invalid.push({
306
+ url: request.url,
307
+ pageTitle: request.url,
308
+ actualUrl: actualUrl, // i.e. actualUrl
309
+ });
310
+
301
311
  return;
302
312
  }
303
313
 
@@ -874,7 +874,6 @@ export const flagUnlabelledClickableElements = async (page: Page) => {
874
874
  }
875
875
 
876
876
  function flagElements() {
877
- console.time('Accessibility Check Time');
878
877
 
879
878
  const currentFlaggedElementsByDocument: Record<string, HTMLElement[]> = {}; // Temporary object to hold current flagged elements
880
879
 
@@ -1014,7 +1013,6 @@ export const flagUnlabelledClickableElements = async (page: Page) => {
1014
1013
  previousFlaggedXPathsByDocument = { ...flaggedXPathsByDocument };
1015
1014
 
1016
1015
  cleanupFlaggedElements();
1017
- console.timeEnd('Accessibility Check Time');
1018
1016
  return previousAllFlaggedElementsXPaths;
1019
1017
  }
1020
1018
 
@@ -295,7 +295,11 @@ export const handlePdfDownload = (
295
295
  numScanned: urlsCrawled.scanned.length,
296
296
  urlScanned: request.url,
297
297
  });
298
- urlsCrawled.invalid.push(url);
298
+ urlsCrawled.invalid.push({
299
+ url: request.url,
300
+ pageTitle: url,
301
+ actualUrl: url, // i.e. actualUrl
302
+ });
299
303
  }
300
304
  resolve();
301
305
  });
@@ -725,6 +725,8 @@ const writeJsonAndBase64Files = async (
725
725
  scanItemsBase64FilePath: string;
726
726
  scanItemsSummaryJsonFilePath: string;
727
727
  scanItemsSummaryBase64FilePath: string;
728
+ scanItemsMiniReportJsonFilePath: string;
729
+ scanItemsMiniReportBase64FilePath: string;
728
730
  scanDataJsonFileSize: number;
729
731
  scanItemsJsonFileSize: number;
730
732
  }> => {
@@ -777,7 +779,7 @@ const writeJsonAndBase64Files = async (
777
779
  topTenIssues,
778
780
  } = rest;
779
781
 
780
- const summaryItems = {
782
+ const summaryItemsMini = {
781
783
  ...items,
782
784
  pagesScanned,
783
785
  topTenPagesWithMostIssues,
@@ -789,6 +791,32 @@ const writeJsonAndBase64Files = async (
789
791
  topTenIssues,
790
792
  };
791
793
 
794
+ const {
795
+ jsonFilePath: scanItemsMiniReportJsonFilePath,
796
+ base64FilePath: scanItemsMiniReportBase64FilePath,
797
+ } = await writeJsonFileAndCompressedJsonFile(summaryItemsMini, storagePath, 'scanItemsSummaryMiniReport');
798
+
799
+ const summaryItems = {
800
+ mustFix: {
801
+ totalItems: items.mustFix?.totalItems || 0,
802
+ totalRuleIssues: items.mustFix?.totalRuleIssues || 0,
803
+ },
804
+ goodToFix: {
805
+ totalItems: items.goodToFix?.totalItems || 0,
806
+ totalRuleIssues: items.goodToFix?.totalRuleIssues || 0,
807
+ },
808
+ needsReview: {
809
+ totalItems: items.needsReview?.totalItems || 0,
810
+ totalRuleIssues: items.needsReview?.totalRuleIssues || 0,
811
+ },
812
+ topTenPagesWithMostIssues,
813
+ wcagLinks,
814
+ wcagPassPercentage,
815
+ totalPagesScanned,
816
+ totalPagesNotScanned,
817
+ topTenIssues,
818
+ };
819
+
792
820
  const {
793
821
  jsonFilePath: scanItemsSummaryJsonFilePath,
794
822
  base64FilePath: scanItemsSummaryBase64FilePath,
@@ -801,6 +829,8 @@ const writeJsonAndBase64Files = async (
801
829
  scanItemsBase64FilePath,
802
830
  scanItemsSummaryJsonFilePath,
803
831
  scanItemsSummaryBase64FilePath,
832
+ scanItemsMiniReportJsonFilePath,
833
+ scanItemsMiniReportBase64FilePath,
804
834
  scanDataJsonFileSize: fs.statSync(scanDataJsonFilePath).size,
805
835
  scanItemsJsonFileSize: fs.statSync(scanItemsJsonFilePath).size,
806
836
  };
@@ -1052,8 +1082,8 @@ const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
1052
1082
  };
1053
1083
 
1054
1084
  allIssues.topFiveMostIssues.sort((page1, page2) => page2.totalIssues - page1.totalIssues);
1055
- allIssues.topFiveMostIssues = allIssues.topFiveMostIssues.slice(0, 5);
1056
1085
  allIssues.topTenPagesWithMostIssues = allIssues.topFiveMostIssues.slice(0, 10);
1086
+ allIssues.topFiveMostIssues = allIssues.topFiveMostIssues.slice(0, 5);
1057
1087
  updateIssuesWithOccurrences(allIssues.topTenPagesWithMostIssues);
1058
1088
  const topTenIssues = getTopTenIssues(allIssues);
1059
1089
  allIssues.topTenIssues = topTenIssues;
@@ -1234,7 +1264,7 @@ const generateArtifacts = async (
1234
1264
  '',
1235
1265
  `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
1266
  `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'}`,
1267
+ `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
1268
  `Passed: ${allIssues.items.passed.totalItems} ${allIssues.items.passed.totalItems === 1 ? 'occurrence' : 'occurrences'}`,
1239
1269
  ]);
1240
1270
 
@@ -1244,7 +1274,7 @@ const generateArtifacts = async (
1244
1274
  createScreenshotsFolder(randomToken);
1245
1275
  }
1246
1276
 
1247
- allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations);
1277
+ allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
1248
1278
  consoleLogger.info(
1249
1279
  `advancedScanOptionsSummaryItems is ${allIssues.advancedScanOptionsSummaryItems}`,
1250
1280
  );
@@ -1293,6 +1323,8 @@ const generateArtifacts = async (
1293
1323
  scanItemsBase64FilePath,
1294
1324
  scanItemsSummaryJsonFilePath,
1295
1325
  scanItemsSummaryBase64FilePath,
1326
+ scanItemsMiniReportJsonFilePath,
1327
+ scanItemsMiniReportBase64FilePath,
1296
1328
  scanDataJsonFileSize,
1297
1329
  scanItemsJsonFileSize,
1298
1330
  } = await writeJsonAndBase64Files(allIssues, storagePath);
@@ -1306,12 +1338,13 @@ const generateArtifacts = async (
1306
1338
  storagePath,
1307
1339
  );
1308
1340
  await writeSummaryHTML(allIssues, storagePath);
1341
+
1309
1342
  await writeHTML(
1310
1343
  allIssues,
1311
1344
  storagePath,
1312
1345
  'report',
1313
1346
  scanDataBase64FilePath,
1314
- resultsTooBig ? scanItemsSummaryBase64FilePath : scanItemsBase64FilePath,
1347
+ resultsTooBig ? scanItemsMiniReportBase64FilePath : scanItemsBase64FilePath,
1315
1348
  );
1316
1349
 
1317
1350
  if (!generateJsonFiles) {
@@ -1,7 +1,7 @@
1
1
  <% const formattedCategoryTitles = {
2
2
  mustFix: "Must Fix",
3
3
  goodToFix: "Good to Fix",
4
- needsReview: "Needs Review",
4
+ needsReview: "Manual Review Required",
5
5
  passed: "Passed"
6
6
  } %>
7
7
  <li class="d-flex align-items-center mb-3 justify-content-between">
@@ -21,7 +21,7 @@
21
21
  </div>
22
22
  <div class="modal-body">
23
23
  <div>
24
- Only 20 WCAG 2.2 Success Criteria (A & AA) can be checked reasonably through automated testing:
24
+ Only <a href="https://go.gov.sg/oobee-details" target="_blank">20 WCAG 2.2</a> Success Criteria (A & AA) can be checked reasonably through automated testing:
25
25
  </div>
26
26
  <div class="accordion my-3" id="wcagLinksAccordion">
27
27
  <div class="accordion-item">
@@ -41,7 +41,8 @@
41
41
  </div>
42
42
  </div>
43
43
  <div>
44
- <strong>Manual testing is still recommended</strong> as they involve subjective judgements
44
+ <strong><a aria-label="Manual testing guide" href="https://go.gov.sg/a11y-manual-testing" target="_blank">Manual
45
+ testing</a> is still recommended</strong> as they involve subjective judgements
45
46
  and human interpretation, which cannot be fully automated.
46
47
  </div>
47
48
  </div>
@@ -1,17 +1,23 @@
1
1
  <footer aria-label="Report footer" id="footer" class="card">
2
2
  <div class="row mx-0">
3
3
  <div class="col-sm-6 text-sm-start">
4
- <% const urlScannedField='64d49b567c3c460011feb8b5' ; const encodedUrlScanned=encodeURIComponent(urlScanned);
5
- const versionNumberField='64db1f79141a46001243b77a' ; const
6
- encodedVersionNumber=encodeURIComponent(phAppVersion); const
7
- feedbackForm=`https://form.gov.sg/64d1fcde4d0bb70012010995/?${urlScannedField}=${encodedUrlScanned}&${versionNumberField}=${encodedVersionNumber}`;
8
- %>
9
- <a href="<%=feedbackForm%>" target="_blank">Help us improve</a>
4
+ <%
5
+ const encodedUrlScanned = encodeURIComponent(urlScanned);
6
+ const encodedVersionNumber = encodeURIComponent(phAppVersion);
7
+
8
+ // Use %0A for line breaks in the body
9
+ const mailtoAddress = `oobee@wogaa.gov.sg`;
10
+ const mailtoSubject = encodeURIComponent(`Support Request - Oobee Version ${phAppVersion}`);
11
+ const mailtoBody = encodeURIComponent(`URL Scanned: ${urlScanned}\nVersion: ${phAppVersion}`);
12
+ const feedbackEmail = `mailto:${mailtoAddress}?subject=${mailtoSubject}&body=${mailtoBody}`;
13
+ %>
14
+
15
+ <a href="<%=feedbackEmail%>" aria-label="Send feedback to <%=mailtoAddress%>" target="_blank">Help us improve</a>
10
16
  <hr class="d-sm-none" />
11
17
  </div>
12
18
  <div class="col-sm-6 text-sm-end">
13
19
  Created by
14
- <a href="https://form.gov.sg/64d1fcde4d0bb70012010995" target="_blank">GovTech Accessibility Enabling Team</a> |
20
+ <a href="https://go.gov.sg/a11y" target="_blank">GovTech Accessibility Enabling Team</a> |
15
21
  <a href="https://go.gov.sg/oobee-report-third-party-licenses" target="_blank">Third-Party Licenses</a>
16
22
  </div>
17
23
  </div>
@@ -414,7 +414,7 @@
414
414
  const titles = {
415
415
  mustFix: 'Must Fix',
416
416
  goodToFix: 'Good to Fix',
417
- needsReview: 'Needs Review',
417
+ needsReview: 'Manual Review Required',
418
418
  passed: 'Passed',
419
419
  };
420
420
 
@@ -13,14 +13,15 @@
13
13
  <div class="d-flex justify-content-between align-items-center">
14
14
  <span class="fw-bold"> WCAG (A & AA) Passes </span>
15
15
  <span aria-label="Pass percentage" class="ms-2">
16
- <%= wcagPassPercentage %>% of automated checks
16
+ <%= wcagPassPercentage.totalWcagChecksAA - wcagPassPercentage.totalWcagViolationsAA %> / <%= wcagPassPercentage.totalWcagChecksAA %> of automated checks
17
17
  </span>
18
18
  </div>
19
19
  <div class="wcag-compliance-passes-bar mb-5 d-flex">
20
20
  <svg width="500" role="none" height="6" fill="none" xmlns="http://www.w3.org/2000/svg"
21
21
  style="display: flex; width: 100%; position: absolute">
22
22
  <rect width="100%" height="10" rx="3" fill="#E7ECEE" style="justify-content: left"></rect>
23
- <rect width="<%= wcagPassPercentage %>%" height="6" rx="3" fill="#9021a6" style=""></rect>
23
+ <rect width="<%= wcagPassPercentage.passPercentageAA %>%" height="6" rx="3" fill="#9021a6" style="">
24
+ </rect>
24
25
  </svg>
25
26
  </div>
26
27
  <ul class="unbulleted-list">
@@ -35,10 +36,10 @@
35
36
  <div class="mx-3">
36
37
  <h2 class="mb-2">Summary of issues:</h2>
37
38
  <p>
38
- Only a subset of
39
- <a href="https://www.w3.org/WAI/WCAG22/quickref/?versions=2.2" target=" _blank">WCAG 2.2</a>
40
- (Conformance Level A & AA) Success Criteria can be automatically checked so
41
- <a aria-label="Manual testing guide" href="http://go.gov.sg/oobee-manual-testing" target="_blank">manual
39
+ Only
40
+ <a href="https://go.gov.sg/oobee-details" target=" _blank">20 WCAG 2.2</a>
41
+ Success Criteria (A & AA) can be automatically checked so
42
+ <a aria-label="Manual testing guide" href="https://go.gov.sg/a11y-manual-testing" target="_blank">manual
42
43
  testing</a>
43
44
  is still required. For more details, please refer to the HTML report.
44
45
  </p>
@@ -47,9 +47,9 @@
47
47
  // Scan DATA FUNCTION TO REPLACE NA
48
48
  const scanDataWCAGCompliance = () => {
49
49
  const passPecentage = document.getElementById('passPercentage');
50
- passPecentage.innerHTML = scanData.wcagPassPercentage + '% of automated checks';
50
+ passPecentage.innerHTML = (scanData.wcagPassPercentage.totalWcagChecksAA - scanData.wcagPassPercentage.totalWcagViolationsAA) + ' / ' + scanData.wcagPassPercentage.totalWcagChecksAA + ' of automated checks';
51
51
  const wcagBarProgess = document.getElementById('wcag-compliance-passes-bar-progress');
52
- wcagBarProgess.style.width = `${scanData.wcagPassPercentage}%`; // Set this to your desired width
52
+ wcagBarProgess.style.width = `${scanData.wcagPassPercentage.passPercentageAA}%`; // Set this to your desired width
53
53
 
54
54
  const wcagLinksList = document.getElementById('wcagLinksList');
55
55
 
@@ -94,7 +94,7 @@
94
94
  const formattedCategoryTitles = {
95
95
  mustFix: 'Must Fix',
96
96
  goodToFix: 'Good to Fix',
97
- needsReview: 'Needs Review',
97
+ needsReview: 'Manual Review Required',
98
98
  passed: 'Passed',
99
99
  };
100
100
 
package/src/utils.ts CHANGED
@@ -190,19 +190,42 @@ export const cleanUp = async pathToDelete => {
190
190
  // timeZoneName: "longGeneric",
191
191
  // });
192
192
 
193
- export const getWcagPassPercentage = (wcagViolations: string[]): string => {
193
+ export const getWcagPassPercentage = (
194
+ wcagViolations: string[],
195
+ showEnableWcagAaa: boolean
196
+ ): {
197
+ passPercentageAA: string;
198
+ totalWcagChecksAA: number;
199
+ totalWcagViolationsAA: number;
200
+ passPercentageAAandAAA: string;
201
+ totalWcagChecksAAandAAA: number;
202
+ totalWcagViolationsAAandAAA: number;
203
+ } => {
194
204
 
195
205
  // These AAA rules should not be counted as WCAG Pass Percentage only contains A and AA
196
- const wcagAAA = ['WCAG 1.4.6', 'WCAG 2.2.4', 'WCAG 2.4.9', 'WCAG 3.1.5', 'WCAG 3.2.5'];
197
-
198
- const filteredWcagLinks = Object.keys(constants.wcagLinks).filter(key => !wcagAAA.includes(key));
199
- const filteredWcagViolations = wcagViolations.filter(violation => !wcagAAA.includes(violation));
200
- const totalChecks = filteredWcagLinks.length;
201
-
202
- const passedChecks = totalChecks - filteredWcagViolations.length;
203
- const passPercentage = (passedChecks / totalChecks) * 100;
204
-
205
- return passPercentage.toFixed(2); // toFixed returns a string, which is correct here
206
+ const wcagAAALinks = ['WCAG 1.4.6', 'WCAG 2.2.4', 'WCAG 2.4.9', 'WCAG 3.1.5', 'WCAG 3.2.5'];
207
+ const wcagAAA = ['wcag146', 'wcag224', 'wcag249', 'wcag315', 'wcag325'];
208
+
209
+ const wcagLinksAAandAAA = constants.wcagLinks;
210
+
211
+ const wcagViolationsAAandAAA = showEnableWcagAaa ? wcagViolations.length : null;
212
+ const totalChecksAAandAAA = showEnableWcagAaa ? Object.keys(wcagLinksAAandAAA).length : null;
213
+ const passedChecksAAandAAA = showEnableWcagAaa ? totalChecksAAandAAA - wcagViolationsAAandAAA : null;
214
+ const passPercentageAAandAAA = showEnableWcagAaa ? (totalChecksAAandAAA === 0 ? 0 : (passedChecksAAandAAA / totalChecksAAandAAA) * 100) : null;
215
+
216
+ const wcagViolationsAA = wcagViolations.filter(violation => !wcagAAA.includes(violation)).length;
217
+ const totalChecksAA = Object.keys(wcagLinksAAandAAA).filter(key => !wcagAAALinks.includes(key)).length;
218
+ const passedChecksAA = totalChecksAA - wcagViolationsAA;
219
+ const passPercentageAA = totalChecksAA === 0 ? 0 : (passedChecksAA / totalChecksAA) * 100;
220
+
221
+ return {
222
+ passPercentageAA: passPercentageAA.toFixed(2), // toFixed returns a string, which is correct here
223
+ totalWcagChecksAA: totalChecksAA,
224
+ totalWcagViolationsAA: wcagViolationsAA,
225
+ passPercentageAAandAAA: passPercentageAAandAAA ? passPercentageAAandAAA.toFixed(2) : null, // toFixed returns a string, which is correct here
226
+ totalWcagChecksAAandAAA: totalChecksAAandAAA,
227
+ totalWcagViolationsAAandAAA: wcagViolationsAAandAAA,
228
+ };
206
229
  };
207
230
 
208
231
  export const getFormattedTime = inputDate => {