@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
@@ -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
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** 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 | 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/INTEGRATION.md CHANGED
@@ -30,7 +30,7 @@ In order to use this functionality, the testing framework must support:
30
30
 
31
31
  ### API Reference
32
32
 
33
- #### `async oobeeA11yInit(entryUrl, testLabel, name, email, includeScreenshots, viewportSettings, thresholds, scanAboutMetadata)`
33
+ #### `async oobeeA11yInit({entryUrl, testLabel, name, email, includeScreenshots, viewportSettings, thresholds, scanAboutMetadata, zip, deviceChosen, strategy, ruleset, specifiedMaxConcurrency, followRobots})`
34
34
 
35
35
  Returns an instance of Oobee
36
36
 
@@ -54,6 +54,16 @@ Returns an instance of Oobee
54
54
  - Include additional information in the Scan About section of the report by passing in a JSON object.
55
55
  - `zip` (optional)
56
56
  - Name of the generated zip of Oobee results at the end of scan. Defaults to "oobee-scan-results".
57
+ - `deviceChosen` (optional)
58
+ - Name of the device to scan on. Example: `iPhone 13 Pro Max`
59
+ - `strategy` (optional)
60
+ - The EnqueueStrategy to use. Options: `all`, `same-hostname`, `same-domain`, `same-origin`
61
+ - `ruleset` (optional)
62
+ - The array of rulesets to use. Options: `default`, `disable-oobee`, `enable-wcag-aaa`
63
+ - `specifiedMaxConcurrency` (optional)
64
+ - The maximum number of concurrent requests to be made. Defaults to 25.
65
+ - `followRobots` (optional)
66
+ - Whether to follow robots.txt. Defaults to false.
57
67
 
58
68
  #### Oobee Instance
59
69
 
@@ -78,14 +88,15 @@ Unique identifier for the scan instance
78
88
 
79
89
  `getScripts()`
80
90
 
81
- Get the axe-core script to be injected into the browser
91
+ Get the axe-core script to be injected into the browser, along with other custom Oobee scripts
82
92
 
83
- - `runA11yScan(elementsToScan)`
93
+ - `runA11yScan(elementsToScan, gradingReadabilityFlag)`
84
94
  Runs axe scan on the current page.
85
95
 
86
96
  Parameter(s):
87
97
 
88
98
  - `elementsToScan`: Specifies which element should and which should not be tested
99
+ - `gradingReadabilityFlag`: This is the readability score as a string. If it is non-empty, a readability violation will be added.
89
100
 
90
101
  Returns:
91
102
 
@@ -176,17 +187,22 @@ Create <code>cypress.config.js</code> with the following contents, and change yo
176
187
  // name of the generated zip of the results at the end of scan
177
188
  const resultsZipName = "oobee-scan-results"
178
189
 
179
- const oobeeA11y = await oobeeA11yInit(
180
- "https://govtechsg.github.io", // initial url to start scan
181
- "Demo Cypress Scan", // label for test
182
- "Your Name",
183
- "email@domain.com",
184
- true, // include screenshots of affected elements in the report
185
- viewportSettings,
186
- thresholds,
187
- scanAboutMetadata,
188
- resultsZipName
189
- );
190
+ const oobeeA11y = await oobeeA11yInit({
191
+ entryUrl: "https://govtechsg.github.io", // initial url to start scan
192
+ testLabel: "Demo Cypress Scan", // label for test
193
+ name: "Your Name",
194
+ email: "email@domain.com",
195
+ includeScreenshots: true, // include screenshots of affected elements in the report
196
+ viewportSettings,
197
+ thresholds: { mustFix: undefined, goodToFix: undefined },
198
+ scanAboutMetadata: undefined,
199
+ zip: resultsZipName,
200
+ deviceChosen: "",
201
+ strategy: undefined,
202
+ ruleset: ["enable-wcag-aaa"], // add "disable-oobee" to disable Oobee custom checks
203
+ specifiedMaxConcurrency: undefined,
204
+ followRobots: undefined,
205
+ });
190
206
 
191
207
  export default defineConfig({
192
208
  taskTimeout: 120000, // need to extend as screenshot function requires some time
@@ -198,6 +214,9 @@ Create <code>cypress.config.js</code> with the following contents, and change yo
198
214
  getPurpleA11yScripts() {
199
215
  return oobeeA11y.getScripts();
200
216
  },
217
+ gradeReadability(sentences) {
218
+ return oobeeA11y.gradeReadability(sentences);
219
+ },
201
220
  async pushPurpleA11yScanResults({res, metadata, elementsToClick}) {
202
221
  return await oobeeA11y.pushScanResults(res, metadata, elementsToClick);
203
222
  },
@@ -229,8 +248,26 @@ Create a sub-folder and file <code>cypress/support/e2e.js</code> with the follow
229
248
  Cypress.Commands.add("runPurpleA11yScan", (items={}) => {
230
249
  cy.window().then(async (win) => {
231
250
  const { elementsToScan, elementsToClick, metadata } = items;
232
- const res = await win.runA11yScan(elementsToScan);
233
- cy.task("pushPurpleA11yScanResults", {res, metadata, elementsToClick}).then((count) => { return count });
251
+
252
+ // extract text from the page for readability grading
253
+ const sentences = win.extractText();
254
+ // run readability grading separately as it cannot be done within the browser context
255
+ cy.task("gradeReadability", sentences).then(
256
+ async (gradingReadabilityFlag) => {
257
+ // passing the grading flag to runA11yScan to inject violation as needed
258
+ const res = await win.runA11yScan(
259
+ elementsToScan,
260
+ gradingReadabilityFlag,
261
+ );
262
+ cy.task("pushPurpleA11yScanResults", {
263
+ res,
264
+ metadata,
265
+ elementsToClick,
266
+ }).then((count) => {
267
+ return count;
268
+ });
269
+ },
270
+ );
234
271
  cy.task("finishPurpleA11yTestCase"); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
235
272
  });
236
273
  });
@@ -244,7 +281,7 @@ Create <code>cypress/e2e/spec.cy.js</code> with the following contents:
244
281
  describe("template spec", () => {
245
282
  it("should run oobee A11y", () => {
246
283
  cy.visit(
247
- "https://govtechsg.github.io/purple-banner-embeds/oobee-integrated-scan-example.htm"
284
+ "https://govtechsg.github.io/purple-banner-embeds/purple-integrated-scan-example.htm"
248
285
  );
249
286
  cy.injectPurpleA11yScripts();
250
287
  cy.runPurpleA11yScan();
@@ -338,17 +375,22 @@ Create <code>cypress.config.ts</code> with the following contents, and change yo
338
375
  // name of the generated zip of the results at the end of scan
339
376
  const resultsZipName: string = "oobee-scan-results"
340
377
 
341
- const oobeeA11y = await oobeeA11yInit(
378
+ const oobeeA11y = await oobeeA11yInit({
342
379
  "https://govtechsg.github.io", // initial url to start scan
343
380
  "Demo Cypress Scan", // label for test
344
381
  "Your Name",
345
382
  "email@domain.com",
346
383
  true, // include screenshots of affected elements in the report
347
384
  viewportSettings,
348
- thresholds,
349
- scanAboutMetadata,
350
- resultsZipName
351
- );
385
+ thresholds: { mustFix: undefined, goodToFix: undefined },
386
+ scanAboutMetadata: undefined,
387
+ zip: resultsZipName,
388
+ deviceChosen: "",
389
+ strategy: undefined,
390
+ ruleset: ["enable-wcag-aaa"], // add "disable-oobee" to disable Oobee custom checks
391
+ specifiedMaxConcurrency: undefined,
392
+ followRobots: undefined,
393
+ });
352
394
 
353
395
  export default defineConfig({
354
396
  taskTimeout: 120000, // need to extend as screenshot function requires some time
@@ -360,6 +402,9 @@ Create <code>cypress.config.ts</code> with the following contents, and change yo
360
402
  getPurpleA11yScripts(): string {
361
403
  return oobeeA11y.getScripts();
362
404
  },
405
+ gradeReadability(sentences: string[]): string {
406
+ return oobeeA11y.gradeReadability(sentences);
407
+ },
363
408
  async pushPurpleA11yScanResults({res, metadata, elementsToClick}: { res: any, metadata: any, elementsToClick: any[] }): Promise<{ mustFix: number, goodToFix: number }> {
364
409
  return await oobeeA11y.pushScanResults(res, metadata, elementsToClick);
365
410
  },
@@ -393,8 +438,26 @@ Create a sub-folder and file <code>src/cypress/support/e2e.ts</code> with the fo
393
438
  Cypress.Commands.add("runPurpleA11yScan", (items={}) => {
394
439
  cy.window().then(async (win) => {
395
440
  const { elementsToScan, elementsToClick, metadata } = items;
396
- const res = await win.runA11yScan(elementsToScan);
397
- cy.task("pushPurpleA11yScanResults", {res, metadata, elementsToClick}).then((count) => { return count });
441
+
442
+ // extract text from the page for readability grading
443
+ const sentences = win.extractText();
444
+ // run readability grading separately as it cannot be done within the browser context
445
+ cy.task("gradeReadability", sentences).then(
446
+ async (gradingReadabilityFlag: string) => {
447
+ // passing the grading flag to runA11yScan to inject violation as needed
448
+ const res = await win.runA11yScan(
449
+ elementsToScan,
450
+ gradingReadabilityFlag,
451
+ );
452
+ cy.task("pushPurpleA11yScanResults", {
453
+ res,
454
+ metadata,
455
+ elementsToClick,
456
+ }).then((count) => {
457
+ return count;
458
+ });
459
+ },
460
+ );
398
461
  cy.task("finishPurpleA11yTestCase"); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
399
462
  });
400
463
  });
@@ -443,6 +506,7 @@ declare namespace Cypress {
443
506
 
444
507
  interface Window {
445
508
  runA11yScan: (elementsToScan?: string[]) => Promise<any>;
509
+ extractText: () => string[];
446
510
  }
447
511
  ```
448
512
 
@@ -475,6 +539,7 @@ On your project's root folder, create a Playwright test file <code>oobeeA11y-pla
475
539
 
476
540
  import { chromium } from "playwright";
477
541
  import oobeeA11yInit from "@govtechsg/oobee";
542
+ import { extractText } from "@govtechsg/oobee/dist/crawlers/custom/extractText.js";
478
543
 
479
544
  // viewport used in tests to optimise screenshots
480
545
  const viewportSettings = { width: 1920, height: 1040 };
@@ -483,16 +548,22 @@ On your project's root folder, create a Playwright test file <code>oobeeA11y-pla
483
548
  // additional information to include in the "Scan About" section of the report
484
549
  const scanAboutMetadata = { browser: 'Chrome (Desktop)' };
485
550
 
486
- const oobeeA11y = await oobeeA11yInit(
487
- "https://govtechsg.github.io", // initial url to start scan
488
- "Demo Playwright Scan", // label for test
489
- "Your Name",
490
- "email@domain.com",
491
- true, // include screenshots of affected elements in the report
551
+ const oobeeA11y = await oobeeA11yInit({
552
+ entryUrl: "https://govtechsg.github.io", // initial url to start scan
553
+ testLabel: "Demo Cypress Scan", // label for test
554
+ name: "Your Name",
555
+ email: "email@domain.com",
556
+ includeScreenshots: true, // include screenshots of affected elements in the report
492
557
  viewportSettings,
493
- thresholds,
494
- scanAboutMetadata,
495
- );
558
+ thresholds: { mustFix: undefined, goodToFix: undefined },
559
+ scanAboutMetadata: undefined,
560
+ zip: resultsZipName,
561
+ deviceChosen: "",
562
+ strategy: undefined,
563
+ ruleset: ["enable-wcag-aaa"],
564
+ specifiedMaxConcurrency: undefined,
565
+ followRobots: undefined,
566
+ });
496
567
 
497
568
  (async () => {
498
569
  const browser = await chromium.launch({
@@ -501,18 +572,22 @@ On your project's root folder, create a Playwright test file <code>oobeeA11y-pla
501
572
  const context = await browser.newContext();
502
573
  const page = await context.newPage();
503
574
 
504
- const runPurpleA11yScan = async (elementsToScan) => {
575
+ const runPurpleA11yScan = async (elementsToScan, gradingReadabilityFlag) => {
505
576
  const scanRes = await page.evaluate(
506
- async elementsToScan => await runA11yScan(elementsToScan),
507
- elementsToScan,
577
+ async ({ elementsToScan, gradingReadabilityFlag }) => await runA11yScan(elementsToScan, gradingReadabilityFlag),
578
+ { elementsToScan, gradingReadabilityFlag },
508
579
  );
509
580
  await oobeeA11y.pushScanResults(scanRes);
510
581
  oobeeA11y.testThresholds(); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
511
582
  };
512
583
 
513
- await page.goto('https://govtechsg.github.io/purple-banner-embeds/oobee-integrated-scan-example.htm');
584
+ await page.goto('https://govtechsg.github.io/purple-banner-embeds/purple-integrated-scan-example.htm');
514
585
  await page.evaluate(oobeeA11y.getScripts());
515
- await runPurpleA11yScan();
586
+
587
+ const sentences = await page.evaluate(() => extractText());
588
+ const gradingReadabilityFlag = await oobeeA11y.gradeReadability(sentences);
589
+
590
+ await runPurpleA11yScan([], gradingReadabilityFlag);;
516
591
 
517
592
  await page.getByRole('button', { name: 'Click Me' }).click();
518
593
  // Run a scan on <input> and <button> elements
@@ -566,8 +641,12 @@ Create a sub-folder and Playwright test file <code>src/oobeeA11y-playwright-demo
566
641
 
567
642
  import { Browser, BrowserContext, Page, chromium } from "playwright";
568
643
  import oobeeA11yInit from "@govtechsg/oobee";
644
+ import { extractText } from "@govtechsg/oobee/dist/crawlers/custom/extractText.js";
569
645
 
570
- declare const runA11yScan: (elementsToScan?: string[]) => Promise<any>;
646
+ declare const runA11yScan: (
647
+ elementsToScan?: string[],
648
+ gradingReadabilityFlag?: string,
649
+ ) => Promise<any>;
571
650
 
572
651
  interface ViewportSettings {
573
652
  width: number;
@@ -590,16 +669,22 @@ Create a sub-folder and Playwright test file <code>src/oobeeA11y-playwright-demo
590
669
  // additional information to include in the "Scan About" section of the report
591
670
  const scanAboutMetadata: ScanAboutMetadata = { browser: 'Chrome (Desktop)' };
592
671
 
593
- const oobeeA11y = await oobeeA11yInit(
594
- "https://govtechsg.github.io", // initial url to start scan
595
- "Demo Playwright Scan", // label for test
596
- "Your Name",
597
- "email@domain.com",
598
- true, // include screenshots of affected elements in the report
672
+ const oobeeA11y = await oobeeA11yInit({
673
+ entryUrl: "https://govtechsg.github.io", // initial url to start scan
674
+ testLabel: "Demo Cypress Scan", // label for test
675
+ name: "Your Name",
676
+ email: "email@domain.com",
677
+ includeScreenshots: true, // include screenshots of affected elements in the report
599
678
  viewportSettings,
600
- thresholds,
601
- scanAboutMetadata,
602
- );
679
+ thresholds: { mustFix: undefined, goodToFix: undefined },
680
+ scanAboutMetadata: undefined,
681
+ zip: resultsZipName,
682
+ deviceChosen: "",
683
+ strategy: undefined,
684
+ ruleset: ["enable-wcag-aaa"],
685
+ specifiedMaxConcurrency: undefined,
686
+ followRobots: undefined,
687
+ });
603
688
 
604
689
  (async () => {
605
690
  const browser: Browser = await chromium.launch({
@@ -608,18 +693,22 @@ Create a sub-folder and Playwright test file <code>src/oobeeA11y-playwright-demo
608
693
  const context: BrowserContext = await browser.newContext();
609
694
  const page: Page = await context.newPage();
610
695
 
611
- const runPurpleA11yScan = async (elementsToScan?: string[]) => {
696
+ const runPurpleA11yScan = async (elementsToScan?: string[], gradingReadabilityFlag?: string) => {
612
697
  const scanRes = await page.evaluate(
613
- async elementsToScan => await runA11yScan(elementsToScan),
614
- elementsToScan,
698
+ async ({ elementsToScan, gradingReadabilityFlag }) => await runA11yScan(elementsToScan, gradingReadabilityFlag),
699
+ { elementsToScan, gradingReadabilityFlag },
615
700
  );
616
701
  await oobeeA11y.pushScanResults(scanRes);
617
702
  oobeeA11y.testThresholds(); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
618
703
  };
619
704
 
620
- await page.goto('https://govtechsg.github.io/purple-banner-embeds/oobee-integrated-scan-example.htm');
705
+ await page.goto('https://govtechsg.github.io/purple-banner-embeds/purple-integrated-scan-example.htm');
621
706
  await page.evaluate(oobeeA11y.getScripts());
622
- await runPurpleA11yScan();
707
+
708
+ const sentences = await page.evaluate(() => extractText());
709
+ const gradingReadabilityFlag = await oobeeA11y.gradeReadability(sentences);
710
+
711
+ await runPurpleA11yScan([], gradingReadabilityFlag);
623
712
 
624
713
  await page.getByRole('button', { name: 'Click Me' }).click();
625
714
  // Run a scan on <input> and <button> elements
package/README.md CHANGED
@@ -80,6 +80,21 @@ export PATH="<location of verapdf>:$PATH"
80
80
  verapdf --version
81
81
  ```
82
82
 
83
+ #### Environment variables (Optional)
84
+ | Variable Name | Description | Default |
85
+ | ------------- | ----------- | ------- |
86
+ | OOBEE_VERBOSE | When set to `true`, log output goes to console | `false` |
87
+ | RUNNING_FROM_PH_GUI | Legacy, replaced by OOBEE_VERBOSE | `false` |
88
+ | WARN_LEVEL | Only used in tests. | |
89
+
90
+ #### Environment variables used internally (Do not set)
91
+ Do not set these environment variables or behaviour might change unexpectedly.
92
+ | Variable Name | Description |
93
+ | ------------- | ----------- |
94
+ | CRAWLEE_LOG_LEVEL | https://crawlee.dev/docs/guides/configuration#crawlee_log_level |
95
+ | CRAWLEE_STORAGE_DIR | https://crawlee.dev/docs/guides/configuration#crawlee_storage_dir |
96
+ | CRAWLEE_HEADLESS | https://crawlee.dev/docs/guides/configuration#crawlee_headless |
97
+
83
98
  #### Facing issues?
84
99
 
85
100
  Please refer to [Troubleshooting section](#troubleshooting) for more information.
@@ -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/exclusions.txt CHANGED
@@ -1,3 +1,6 @@
1
1
  \.*login.singpass.gov.sg\.*
2
2
  \.*auth.singpass.gov.sg\.*
3
- \.*form.gov.sg\.*
3
+ \.*form.gov.sg\.*
4
+ \.*login.microsoftonline.com\.*
5
+ \.*id.atlassian.com\.*
6
+ \.*cloudflareaccess.com\.*
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@govtechsg/oobee",
3
3
  "main": "dist/npmIndex.js",
4
- "version": "0.10.34",
4
+ "version": "0.10.39",
5
5
  "type": "module",
6
6
  "author": "Government Technology Agency <info@tech.gov.sg>",
7
7
  "dependencies": {
8
8
  "@json2csv/node": "^7.0.3",
9
9
  "@napi-rs/canvas": "^0.1.53",
10
10
  "axe-core": "^4.10.2",
11
- "axios": "^1.7.4",
11
+ "axios": "^1.8.2",
12
12
  "base64-stream": "^1.0.0",
13
13
  "cheerio": "^1.0.0-rc.12",
14
14
  "crawlee": "^3.11.1",
@@ -333,10 +333,3 @@ To obtain the JSON files, you need to base64-decode the file followed by gunzip.
333
333
  },
334
334
  };
335
335
 
336
- export const configureReportSetting = (isEnabled: boolean): void => {
337
- if (isEnabled) {
338
- process.env.REPORT_BREAKDOWN = '1';
339
- } else {
340
- process.env.REPORT_BREAKDOWN = '0';
341
- }
342
- };
@@ -461,7 +461,7 @@ const checkUrlConnectivityWithBrowser = async (
461
461
 
462
462
  res.content = await page.content();
463
463
 
464
- const contentType = response.headers()['content-type'];
464
+ const contentType = response?.headers?.()['content-type'] || '';
465
465
  if (contentType.includes('xml')) {
466
466
  const responseFromUrl = await requestToUrl(res.url, true, extraHTTPHeaders);
467
467
 
@@ -1776,6 +1776,44 @@ export const submitForm = async (
1776
1776
  }
1777
1777
  }
1778
1778
  };
1779
+
1780
+ export async function initModifiedUserAgent(browser?: string, playwrightDeviceDetailsObject?: object) {
1781
+ const isHeadless = process.env.CRAWLEE_HEADLESS === '1';
1782
+
1783
+ // If headless mode is enabled, ensure the headless flag is set.
1784
+ if (isHeadless && !constants.launchOptionsArgs.includes('--headless=new')) {
1785
+ constants.launchOptionsArgs.push('--headless=new');
1786
+ }
1787
+
1788
+ // Build the launch options using your production settings.
1789
+ // headless is forced to false as in your persistent context, and we merge in getPlaywrightLaunchOptions and device details.
1790
+ const launchOptions = {
1791
+ headless: false,
1792
+ ...getPlaywrightLaunchOptions(browser),
1793
+ ...playwrightDeviceDetailsObject,
1794
+ };
1795
+
1796
+ // Launch a temporary persistent context with an empty userDataDir to mimic your production browser setup.
1797
+ const browserContext = await constants.launcher.launchPersistentContext('', launchOptions);
1798
+ const page = await browserContext.newPage();
1799
+
1800
+ // Retrieve the default user agent.
1801
+ const defaultUA = await page.evaluate(() => navigator.userAgent);
1802
+ await browserContext.close();
1803
+
1804
+ // Modify the UA:
1805
+ // Replace "HeadlessChrome" with "Chrome" if present.
1806
+ let modifiedUA = defaultUA.includes('HeadlessChrome')
1807
+ ? defaultUA.replace('HeadlessChrome', 'Chrome')
1808
+ : defaultUA;
1809
+
1810
+ // Push the modified UA flag into your global launch options.
1811
+ constants.launchOptionsArgs.push(`--user-agent=${modifiedUA}`);
1812
+ // Optionally log the modified UA.
1813
+ // console.log('Modified User Agent:', modifiedUA);
1814
+ }
1815
+
1816
+
1779
1817
  /**
1780
1818
  * @param {string} browser browser name ("chrome" or "edge", null for chromium, the default Playwright browser)
1781
1819
  * @returns playwright launch options object. For more details: https://playwright.dev/docs/api/class-browsertype#browser-type-launch