@govtechsg/oobee 0.10.76 → 0.10.78-alpha1

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 (137) hide show
  1. package/.github/workflows/publish.yml +8 -1
  2. package/INTEGRATION.md +50 -3
  3. package/dist/cli.js +252 -0
  4. package/dist/combine.js +221 -0
  5. package/dist/constants/cliFunctions.js +306 -0
  6. package/dist/constants/common.js +1669 -0
  7. package/dist/constants/constants.js +913 -0
  8. package/dist/constants/errorMeta.json +319 -0
  9. package/dist/constants/itemTypeDescription.js +7 -0
  10. package/dist/constants/oobeeAi.js +121 -0
  11. package/dist/constants/questions.js +151 -0
  12. package/dist/constants/sampleData.js +176 -0
  13. package/dist/crawlers/commonCrawlerFunc.js +428 -0
  14. package/dist/crawlers/crawlDomain.js +613 -0
  15. package/dist/crawlers/crawlIntelligentSitemap.js +135 -0
  16. package/dist/crawlers/crawlLocalFile.js +151 -0
  17. package/dist/crawlers/crawlSitemap.js +303 -0
  18. package/dist/crawlers/custom/escapeCssSelector.js +10 -0
  19. package/dist/crawlers/custom/evaluateAltText.js +11 -0
  20. package/dist/crawlers/custom/extractAndGradeText.js +44 -0
  21. package/dist/crawlers/custom/extractText.js +27 -0
  22. package/dist/crawlers/custom/findElementByCssSelector.js +36 -0
  23. package/dist/crawlers/custom/flagUnlabelledClickableElements.js +963 -0
  24. package/dist/crawlers/custom/framesCheck.js +37 -0
  25. package/dist/crawlers/custom/getAxeConfiguration.js +111 -0
  26. package/dist/crawlers/custom/gradeReadability.js +23 -0
  27. package/dist/crawlers/custom/utils.js +1024 -0
  28. package/dist/crawlers/custom/xPathToCss.js +147 -0
  29. package/dist/crawlers/guards/urlGuard.js +71 -0
  30. package/dist/crawlers/pdfScanFunc.js +276 -0
  31. package/dist/crawlers/runCustom.js +89 -0
  32. package/dist/exclusions.txt +7 -0
  33. package/dist/generateHtmlReport.js +144 -0
  34. package/dist/index.js +62 -0
  35. package/dist/logs.js +84 -0
  36. package/dist/mergeAxeResults.js +1588 -0
  37. package/dist/npmIndex.js +640 -0
  38. package/dist/proxyService.js +360 -0
  39. package/dist/runGenerateJustHtmlReport.js +16 -0
  40. package/dist/screenshotFunc/htmlScreenshotFunc.js +355 -0
  41. package/dist/screenshotFunc/pdfScreenshotFunc.js +645 -0
  42. package/dist/services/s3Uploader.js +127 -0
  43. package/dist/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
  44. package/dist/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
  45. package/dist/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
  46. package/dist/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
  47. package/dist/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
  48. package/dist/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
  49. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
  50. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
  51. package/dist/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
  52. package/dist/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
  53. package/dist/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
  54. package/dist/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
  55. package/dist/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
  56. package/dist/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
  57. package/dist/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
  58. package/dist/static/ejs/partials/components/summaryScanResults.ejs +16 -0
  59. package/dist/static/ejs/partials/components/summaryTable.ejs +20 -0
  60. package/dist/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
  61. package/dist/static/ejs/partials/components/topTen.ejs +6 -0
  62. package/dist/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
  63. package/dist/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
  64. package/dist/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
  65. package/dist/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
  66. package/dist/static/ejs/partials/footer.ejs +24 -0
  67. package/dist/static/ejs/partials/header.ejs +14 -0
  68. package/dist/static/ejs/partials/main.ejs +29 -0
  69. package/dist/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
  70. package/dist/static/ejs/partials/scripts/bootstrap.ejs +8 -0
  71. package/dist/static/ejs/partials/scripts/categorySummary.ejs +141 -0
  72. package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  73. package/dist/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
  74. package/dist/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
  75. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
  76. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
  77. package/dist/static/ejs/partials/scripts/highlightjs.ejs +335 -0
  78. package/dist/static/ejs/partials/scripts/popper.ejs +7 -0
  79. package/dist/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
  80. package/dist/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
  81. package/dist/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
  82. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +957 -0
  83. package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +353 -0
  84. package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
  85. package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
  86. package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
  87. package/dist/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
  88. package/dist/static/ejs/partials/scripts/screenshotLightbox.ejs +75 -0
  89. package/dist/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
  90. package/dist/static/ejs/partials/scripts/summaryTable.ejs +78 -0
  91. package/dist/static/ejs/partials/scripts/topTen.ejs +61 -0
  92. package/dist/static/ejs/partials/scripts/utils.ejs +453 -0
  93. package/dist/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
  94. package/dist/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
  95. package/dist/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
  96. package/dist/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
  97. package/dist/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
  98. package/dist/static/ejs/partials/styles/bootstrap.ejs +12391 -0
  99. package/dist/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
  100. package/dist/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
  101. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
  102. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
  103. package/dist/static/ejs/partials/styles/header.ejs +7 -0
  104. package/dist/static/ejs/partials/styles/highlightjs.ejs +54 -0
  105. package/dist/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
  106. package/dist/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
  107. package/dist/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
  108. package/dist/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
  109. package/dist/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
  110. package/dist/static/ejs/partials/styles/styles.ejs +1607 -0
  111. package/dist/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
  112. package/dist/static/ejs/partials/styles/topTenCard.ejs +44 -0
  113. package/dist/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
  114. package/dist/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
  115. package/dist/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
  116. package/dist/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
  117. package/dist/static/ejs/partials/summaryHeader.ejs +70 -0
  118. package/dist/static/ejs/partials/summaryMain.ejs +49 -0
  119. package/dist/static/ejs/report.ejs +226 -0
  120. package/dist/static/ejs/summary.ejs +47 -0
  121. package/dist/types/types.js +1 -0
  122. package/dist/utils.js +1070 -0
  123. package/examples/oobee-cypress-integration-js/cypress/support/e2e.js +36 -6
  124. package/examples/oobee-cypress-integration-js/cypress.config.js +45 -1
  125. package/examples/oobee-cypress-integration-ts/cypress.config.ts +47 -1
  126. package/examples/oobee-cypress-integration-ts/src/cypress/support/e2e.ts +36 -6
  127. package/examples/oobee-playwright-integration-js/oobee-playwright-demo.js +2 -1
  128. package/examples/oobee-playwright-integration-ts/src/oobee-playwright-demo.ts +2 -1
  129. package/examples/oobee-scan-html-demo.js +51 -0
  130. package/examples/oobee-scan-page-demo.js +40 -0
  131. package/package.json +9 -3
  132. package/src/constants/common.ts +2 -2
  133. package/src/constants/constants.ts +3 -1
  134. package/src/crawlers/crawlDomain.ts +1 -0
  135. package/src/crawlers/runCustom.ts +0 -1
  136. package/src/mergeAxeResults.ts +43 -22
  137. package/src/npmIndex.ts +500 -131
package/src/npmIndex.ts CHANGED
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import printMessage from 'print-message';
4
4
  import axe, { AxeResults, ImpactValue } from 'axe-core';
5
+ import { JSDOM } from 'jsdom';
5
6
  import { fileURLToPath } from 'url';
6
7
  import { EnqueueStrategy } from 'crawlee';
7
8
  import constants, { BrowserTypes, RuleFlags, ScannerTypes } from './constants/constants.js';
@@ -12,8 +13,8 @@ import {
12
13
  submitForm,
13
14
  } from './constants/common.js';
14
15
  import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
15
- import { createAndUpdateResultsFolders } from './utils.js';
16
- import generateArtifacts from './mergeAxeResults.js';
16
+ import { createAndUpdateResultsFolders, getVersion } from './utils.js';
17
+ import generateArtifacts, { createBasicFormHTMLSnippet, sendWcagBreakdownToSentry } from './mergeAxeResults.js';
17
18
  import { takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
18
19
  import { consoleLogger, silentLogger } from './logs.js';
19
20
  import { alertMessageOptions } from './constants/cliFunctions.js';
@@ -25,101 +26,43 @@ import { flagUnlabelledClickableElements } from './crawlers/custom/flagUnlabelle
25
26
  import xPathToCss from './crawlers/custom/xPathToCss.js';
26
27
  import { extractText } from './crawlers/custom/extractText.js';
27
28
  import { gradeReadability } from './crawlers/custom/gradeReadability.js';
29
+ import { BrowserContext, Page } from 'playwright';
30
+ import { filter } from 'jszip';
31
+
32
+ // Define global window properties for Oobee injection functions
33
+ declare global {
34
+ interface Window {
35
+ runA11yScan: (
36
+ elements?: any[],
37
+ gradingReadabilityFlag?: string,
38
+ ) => Promise<{
39
+ pageUrl: string;
40
+ pageTitle: string;
41
+ axeScanResults: AxeResults;
42
+ }>;
43
+ axe: any;
44
+ getAxeConfiguration: any;
45
+ flagUnlabelledClickableElements: any;
46
+ disableOobee: boolean;
47
+ enableWcagAaa: boolean;
48
+ xPathToCss: any;
49
+ evaluateAltText: any;
50
+ escapeCssSelector: any;
51
+ framesCheck: any;
52
+ findElementByCssSelector: any;
53
+ extractText: any;
54
+ }
55
+ }
28
56
 
29
57
  const filename = fileURLToPath(import.meta.url);
30
58
  const dirname = path.dirname(filename);
31
59
 
32
- export const init = async ({
33
- entryUrl,
34
- testLabel,
35
- name,
36
- email,
37
- includeScreenshots = false,
38
- viewportSettings = { width: 1000, height: 660 }, // cypress' default viewport settings
39
- thresholds = { mustFix: undefined, goodToFix: undefined },
40
- scanAboutMetadata = undefined,
41
- zip = 'oobee-scan-results',
42
- deviceChosen,
43
- strategy = EnqueueStrategy.All,
44
- ruleset = [RuleFlags.DEFAULT],
45
- specifiedMaxConcurrency = 25,
46
- followRobots = false,
47
- }: {
48
- entryUrl: string;
49
- testLabel: string;
50
- name: string;
51
- email: string;
52
- includeScreenshots?: boolean;
53
- viewportSettings?: { width: number; height: number };
54
- thresholds?: { mustFix: number; goodToFix: number };
55
- scanAboutMetadata?: {
56
- browser?: string;
57
- viewport?: { width: number; height: number };
58
- };
59
- zip?: string;
60
- deviceChosen?: string;
61
- strategy?: EnqueueStrategy;
62
- ruleset?: RuleFlags[];
63
- specifiedMaxConcurrency?: number;
64
- followRobots?: boolean;
65
- }) => {
66
- consoleLogger.info('Starting Oobee');
67
-
68
- const [date, time] = new Date().toLocaleString('sv').replaceAll(/-|:/g, '').split(' ');
69
- const domain = new URL(entryUrl).hostname;
70
- const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
71
- const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
72
-
73
- const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
74
- const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
75
-
76
- // max numbers of mustFix/goodToFix occurrences before test returns a fail
77
- const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
78
-
79
- process.env.CRAWLEE_STORAGE_DIR = randomToken;
80
-
81
- const scanDetails = {
82
- startTime: new Date(),
83
- endTime: new Date(),
84
- deviceChosen,
85
- crawlType: ScannerTypes.CUSTOM,
86
- requestUrl: entryUrl,
87
- urlsCrawled: { ...constants.urlsCrawledObj },
88
- isIncludeScreenshots: includeScreenshots,
89
- isAllowSubdomains: strategy,
90
- isEnableCustomChecks: ruleset,
91
- isEnableWcagAaa: ruleset,
92
- isSlowScanMode: specifiedMaxConcurrency,
93
- isAdhereRobots: followRobots,
94
- };
95
-
96
- const urlsCrawled = { ...constants.urlsCrawledObj };
97
-
98
- const { dataset } = await createCrawleeSubFolders(randomToken);
99
-
100
- let mustFixIssues = 0;
101
- let goodToFixIssues = 0;
102
-
103
- let isInstanceTerminated = false;
104
-
105
- const throwErrorIfTerminated = () => {
106
- if (isInstanceTerminated) {
107
- throw new Error('This instance of Oobee was terminated. Please start a new instance.');
108
- }
109
- };
110
-
111
- const getAxeScript = () => {
112
- throwErrorIfTerminated();
113
- const axeScript = fs.readFileSync(
114
- path.join(dirname, '../../../axe-core/axe.min.js'),
115
- 'utf-8',
116
- );
117
- return axeScript;
118
- };
60
+ const getAxeScriptContent = () => {
61
+ return axe.source;
62
+ };
119
63
 
120
- const getOobeeFunctions = () => {
121
- throwErrorIfTerminated();
122
- return `
64
+ const getOobeeFunctionsScript = (disableOobee: boolean, enableWcagAaa: boolean) => {
65
+ return `
123
66
  // Fix for missing __name function used by bundler
124
67
  if (typeof __name === 'undefined') {
125
68
  window.__name = function(fn, name) {
@@ -177,7 +120,7 @@ export const init = async ({
177
120
  return !node.dataset.flagged; // fail any element with a data-flagged attribute set to true
178
121
  },
179
122
  },
180
- ...(enableWcagAaa
123
+ ...((enableWcagAaa && !disableOobee)
181
124
  ? [
182
125
  {
183
126
  id: 'oobee-grading-text-contents',
@@ -225,19 +168,23 @@ export const init = async ({
225
168
  helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
226
169
  },
227
170
  },
228
- {
229
- id: 'oobee-grading-text-contents',
230
- selector: 'html',
231
- enabled: true,
232
- any: ['oobee-grading-text-contents'],
233
- tags: ['wcag2aaa', 'wcag315'],
234
- metadata: {
235
- description:
236
- 'Text content should be easy to understand for individuals with education levels up to university graduates. If the text content is difficult to understand, provide supplemental content or a version that is easy to understand.',
237
- help: 'Text content should be clear and plain to ensure that it is easily understood.',
238
- helpUrl: 'https://www.wcag.com/uncategorized/3-1-5-reading-level/',
239
- },
240
- },
171
+ ...((enableWcagAaa && !disableOobee)
172
+ ? [
173
+ {
174
+ id: 'oobee-grading-text-contents',
175
+ selector: 'html',
176
+ enabled: true,
177
+ any: ['oobee-grading-text-contents'],
178
+ tags: ['wcag2aaa', 'wcag315'],
179
+ metadata: {
180
+ description:
181
+ 'Text content should be easy to understand for individuals with education levels up to university graduates. If the text content is difficult to understand, provide supplemental content or a version that is easy to understand.',
182
+ help: 'Text content should be clear and plain to ensure that it is easily understood.',
183
+ helpUrl: 'https://www.wcag.com/uncategorized/3-1-5-reading-level/',
184
+ },
185
+ },
186
+ ]
187
+ : []),
241
188
  ]
242
189
  .filter(rule => (disableOobee ? !rule.id.startsWith('oobee') : true))
243
190
  .concat(
@@ -290,6 +237,31 @@ export const init = async ({
290
237
  const axeScanResults = await (window).axe.run(elementsToScan, {
291
238
  resultTypes: ['violations', 'passes', 'incomplete'],
292
239
  });
240
+
241
+ if (axeScanResults) {
242
+ ['violations', 'incomplete'].forEach(type => {
243
+ if (axeScanResults[type]) {
244
+ axeScanResults[type].forEach(result => {
245
+ if (result.nodes) {
246
+ result.nodes.forEach(node => {
247
+ ['any', 'all', 'none'].forEach(key => {
248
+ if (node[key]) {
249
+ node[key].forEach(check => {
250
+ if (check.message && check.message.indexOf("Axe encountered an error") !== -1) {
251
+ if (check.data) {
252
+ // console.error(check.data);
253
+ console.error("Axe encountered an error: " + (check.data.stack || check.data.message || JSON.stringify(check.data)));
254
+ }
255
+ }
256
+ });
257
+ }
258
+ });
259
+ });
260
+ }
261
+ });
262
+ }
263
+ });
264
+ }
293
265
 
294
266
  // add custom Oobee violations
295
267
  if (!(window).disableOobee) {
@@ -339,6 +311,95 @@ export const init = async ({
339
311
  window.enableWcagAaa=${enableWcagAaa};
340
312
  window.runA11yScan = runA11yScan;
341
313
  `;
314
+ };
315
+
316
+ export const init = async ({
317
+ entryUrl,
318
+ testLabel,
319
+ name,
320
+ email,
321
+ includeScreenshots = false,
322
+ viewportSettings = { width: 1000, height: 660 }, // cypress' default viewport settings
323
+ thresholds = { mustFix: undefined, goodToFix: undefined },
324
+ scanAboutMetadata = undefined,
325
+ zip = 'oobee-scan-results',
326
+ deviceChosen,
327
+ strategy = EnqueueStrategy.All,
328
+ ruleset = [RuleFlags.DEFAULT],
329
+ specifiedMaxConcurrency = 25,
330
+ followRobots = false,
331
+ }: {
332
+ entryUrl: string;
333
+ testLabel: string;
334
+ name: string;
335
+ email: string;
336
+ includeScreenshots?: boolean;
337
+ viewportSettings?: { width: number; height: number };
338
+ thresholds?: { mustFix: number; goodToFix: number };
339
+ scanAboutMetadata?: {
340
+ browser?: string;
341
+ viewport?: { width: number; height: number };
342
+ };
343
+ zip?: string;
344
+ deviceChosen?: string;
345
+ strategy?: EnqueueStrategy;
346
+ ruleset?: RuleFlags[];
347
+ specifiedMaxConcurrency?: number;
348
+ followRobots?: boolean;
349
+ }) => {
350
+ consoleLogger.info('Starting Oobee');
351
+
352
+ const [date, time] = new Date().toLocaleString('sv').replaceAll(/-|:/g, '').split(' ');
353
+ const domain = new URL(entryUrl).hostname;
354
+ const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
355
+ const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
356
+
357
+ const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
358
+ const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
359
+
360
+ // max numbers of mustFix/goodToFix occurrences before test returns a fail
361
+ const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
362
+
363
+ process.env.CRAWLEE_STORAGE_DIR = randomToken;
364
+
365
+ const scanDetails = {
366
+ startTime: new Date(),
367
+ endTime: new Date(),
368
+ deviceChosen,
369
+ crawlType: ScannerTypes.CUSTOM,
370
+ requestUrl: entryUrl,
371
+ urlsCrawled: { ...constants.urlsCrawledObj },
372
+ isIncludeScreenshots: includeScreenshots,
373
+ isAllowSubdomains: strategy,
374
+ isEnableCustomChecks: ruleset,
375
+ isEnableWcagAaa: ruleset,
376
+ isSlowScanMode: specifiedMaxConcurrency,
377
+ isAdhereRobots: followRobots,
378
+ };
379
+
380
+ const urlsCrawled = { ...constants.urlsCrawledObj };
381
+
382
+ const { dataset } = await createCrawleeSubFolders(randomToken);
383
+
384
+ let mustFixIssues = 0;
385
+ let goodToFixIssues = 0;
386
+
387
+ let isInstanceTerminated = false;
388
+
389
+ const throwErrorIfTerminated = () => {
390
+ if (isInstanceTerminated) {
391
+ throw new Error('This instance of Oobee was terminated. Please start a new instance.');
392
+ }
393
+ };
394
+
395
+ const getAxeScript = () => {
396
+ throwErrorIfTerminated();
397
+ return getAxeScriptContent();
398
+ };
399
+
400
+ const getOobeeFunctions = () => {
401
+ throwErrorIfTerminated();
402
+ return getOobeeFunctionsScript(disableOobee, enableWcagAaa);
342
403
  };
343
404
 
344
405
  // Helper script for manually copy-paste testing in Chrome browser
@@ -351,49 +412,66 @@ export const init = async ({
351
412
  res: { pageUrl: string; pageTitle: string; axeScanResults: AxeResults },
352
413
  metadata: string,
353
414
  elementsToClick: string[],
415
+ page?: Page,
416
+ disableScreenshots: boolean = false, // Only for Cypress (or other library that wants to use it's own screenshotting)
354
417
  ) => {
355
418
  throwErrorIfTerminated();
356
- if (includeScreenshots) {
357
- // use chrome by default
358
- const { browserToRun, clonedBrowserDataDir } = getBrowserToRun(randomToken, BrowserTypes.CHROME, false);
359
- const browserContext = await constants.launcher.launchPersistentContext(
360
- clonedBrowserDataDir,
361
- { viewport: viewportSettings, ...getPlaywrightLaunchOptions(browserToRun) },
362
- );
363
- const page = await browserContext.newPage();
364
- await page.goto(res.pageUrl);
419
+ if (includeScreenshots && !disableScreenshots) {
420
+ let browserContext: BrowserContext | undefined;
421
+ let browserToRun: BrowserTypes | undefined;
422
+ let clonedBrowserDataDir: string | undefined;
423
+ let pageToScan: Page;
424
+
425
+ if (page) {
426
+ pageToScan = page;
427
+ } else {
428
+ // use chrome by default
429
+ const browserData = getBrowserToRun(randomToken, BrowserTypes.CHROME, false);
430
+ browserToRun = browserData.browserToRun;
431
+ clonedBrowserDataDir = browserData.clonedBrowserDataDir;
432
+
433
+ browserContext = await constants.launcher.launchPersistentContext(clonedBrowserDataDir, {
434
+ viewport: viewportSettings,
435
+ ...getPlaywrightLaunchOptions(browserToRun),
436
+ });
437
+ const newPage = await browserContext.newPage();
438
+ await newPage.goto(res.pageUrl);
439
+ try {
440
+ await newPage.waitForLoadState('networkidle', { timeout: 10000 });
441
+ } catch (e) {
442
+ console.log('Network idle timeout, continuing with screenshot capture...');
443
+ // Fall back to domcontentloaded if networkidle times out
444
+ await newPage.waitForLoadState('domcontentloaded', { timeout: 5000 });
445
+ } // click on elements to reveal hidden elements so screenshots can be taken
446
+ if (elementsToClick) {
447
+ for (const elem of elementsToClick) {
365
448
  try {
366
- await page.waitForLoadState('networkidle', { timeout: 10000 });
449
+ await newPage.locator(elem).click();
367
450
  } catch (e) {
368
- console.log('Network idle timeout, continuing with screenshot capture...');
369
- // Fall back to domcontentloaded if networkidle times out
370
- await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
371
- } // click on elements to reveal hidden elements so screenshots can be taken
372
- if (elementsToClick) {
373
- for (const elem of elementsToClick) {
374
- try {
375
- await page.locator(elem).click();
376
- } catch (e) {
377
- // do nothing if element is not found or not clickable
451
+ // do nothing if element is not found or not clickable
452
+ }
378
453
  }
379
454
  }
455
+ pageToScan = newPage;
380
456
  }
381
457
 
382
458
  res.axeScanResults.violations = await takeScreenshotForHTMLElements(
383
459
  res.axeScanResults.violations,
384
- page,
460
+ pageToScan,
385
461
  randomToken,
386
462
  3000,
387
463
  );
388
464
  res.axeScanResults.incomplete = await takeScreenshotForHTMLElements(
389
465
  res.axeScanResults.incomplete,
390
- page,
466
+ pageToScan,
391
467
  randomToken,
392
468
  3000,
393
469
  );
394
470
 
395
- await browserContext.close();
396
- deleteClonedProfiles(browserToRun, randomToken);
471
+ if (browserContext && browserToRun) {
472
+ await browserContext.close();
473
+ deleteClonedProfiles(browserToRun, randomToken);
474
+ }
397
475
  }
398
476
  const pageIndex = urlsCrawled.scanned.length + 1;
399
477
  const filteredResults = filterAxeResults(res.axeScanResults, res.pageTitle, {
@@ -509,4 +587,295 @@ export const init = async ({
509
587
  };
510
588
  };
511
589
 
512
- export default init;
590
+ export default init;
591
+
592
+ const processAndSubmitResults = async (
593
+ scanData: { axeScanResults: AxeResults; pageUrl: string; pageTitle: string } | { axeScanResults: AxeResults; pageUrl: string; pageTitle: string }[],
594
+ name: string,
595
+ email: string,
596
+ metadata: string,
597
+ ) => {
598
+ const items = Array.isArray(scanData) ? scanData : [scanData];
599
+ const numberOfPagesScanned = items.length;
600
+
601
+ const allFilteredResults = items.map((item, index) => {
602
+ const filtered = filterAxeResults(item.axeScanResults, item.pageTitle, { pageIndex: index + 1, metadata });
603
+ (filtered as any).url = item.pageUrl;
604
+ return filtered;
605
+ });
606
+
607
+ type Rule = {
608
+ totalItems: number;
609
+ items: any[];
610
+ [key: string]: any;
611
+ };
612
+
613
+ type ResultCategory = {
614
+ totalItems: number;
615
+ rules: Record<string, Rule>;
616
+ };
617
+
618
+ type CategoryKey = 'mustFix' | 'goodToFix' | 'needsReview';
619
+
620
+ const mergedResults: Record<CategoryKey, ResultCategory> = {
621
+ mustFix: { totalItems: 0, rules: {} },
622
+ goodToFix: { totalItems: 0, rules: {} },
623
+ needsReview: { totalItems: 0, rules: {} },
624
+ // omitting passed from being processed to reduce payload size
625
+ // passed: { totalItems: 0, rules: {} },
626
+ };
627
+
628
+ allFilteredResults.forEach(result => {
629
+ (['mustFix', 'goodToFix', 'needsReview'] as CategoryKey[]).forEach(category => {
630
+ const categoryResult = (result as any)[category];
631
+ if (categoryResult) {
632
+ mergedResults[category].totalItems += categoryResult.totalItems;
633
+ Object.entries(categoryResult.rules).forEach(([ruleId, ruleVal]: [string, any]) => {
634
+ if (!mergedResults[category].rules[ruleId]) {
635
+ mergedResults[category].rules[ruleId] = JSON.parse(JSON.stringify(ruleVal));
636
+
637
+ // Map the description to the short description if available
638
+ if (constants.a11yRuleShortDescriptionMap[ruleId]) {
639
+ mergedResults[category].rules[ruleId].description = constants.a11yRuleShortDescriptionMap[ruleId];
640
+ }
641
+
642
+ // Add url to items
643
+ mergedResults[category].rules[ruleId].items.forEach((item: any) => {
644
+ item.url = (result as any).url;
645
+ if (item.displayNeedsReview) {
646
+ delete item.displayNeedsReview;
647
+ }
648
+ });
649
+ } else {
650
+ mergedResults[category].rules[ruleId].totalItems += ruleVal.totalItems;
651
+ const newItems = ruleVal.items.map((item: any) => {
652
+ const newItem = { ...item, url: (result as any).url };
653
+ if (newItem.displayNeedsReview) {
654
+ delete newItem.displayNeedsReview;
655
+ }
656
+ return newItem;
657
+ });
658
+ mergedResults[category].rules[ruleId].items.push(...newItems);
659
+ }
660
+ });
661
+ }
662
+ });
663
+ });
664
+
665
+ const basicFormHTMLSnippet = createBasicFormHTMLSnippet(mergedResults);
666
+ const entryUrl = items[0].pageUrl;
667
+
668
+ await submitForm(
669
+ BrowserTypes.CHROMIUM,
670
+ '',
671
+ entryUrl,
672
+ null,
673
+ ScannerTypes.CUSTOM,
674
+ email,
675
+ name,
676
+ JSON.stringify(basicFormHTMLSnippet),
677
+ numberOfPagesScanned,
678
+ 0,
679
+ 0,
680
+ '{}',
681
+ );
682
+
683
+ // Generate WCAG breakdown for Sentry
684
+ const wcagOccurrencesMap = new Map<string, number>();
685
+
686
+ // Iterate through relevant categories to collect WCAG violation occurrences
687
+ (['mustFix', 'goodToFix'] as CategoryKey[]).forEach(category => {
688
+ const rulesObj = mergedResults[category]?.rules;
689
+ if (rulesObj) {
690
+ Object.values(rulesObj).forEach((rule: any) => {
691
+ const count = rule.totalItems;
692
+ if (rule.conformance && Array.isArray(rule.conformance)) {
693
+ rule.conformance
694
+ .filter((c: string) => /wcag[0-9]{3,4}/.test(c))
695
+ .forEach((c: string) => {
696
+ const current = wcagOccurrencesMap.get(c) || 0;
697
+ wcagOccurrencesMap.set(c, current + count);
698
+ });
699
+ }
700
+ });
701
+ }
702
+ });
703
+
704
+ const oobeeAppVersion = getVersion();
705
+
706
+ await sendWcagBreakdownToSentry(
707
+ oobeeAppVersion,
708
+ wcagOccurrencesMap,
709
+ basicFormHTMLSnippet,
710
+ {
711
+ entryUrl: entryUrl,
712
+ scanType: ScannerTypes.CUSTOM,
713
+ browser: 'chromium', // Defaulting since we might scan HTML without browser or implicit browser
714
+ email: email,
715
+ name: name,
716
+ },
717
+ undefined,
718
+ numberOfPagesScanned,
719
+ );
720
+
721
+ // Return original single result if only one page was scanning to maintain backward compatibility structure
722
+ if (numberOfPagesScanned === 1) {
723
+ const singleResult = allFilteredResults[0];
724
+
725
+ // Clean up displayNeedsReview from single result
726
+ (['mustFix', 'goodToFix', 'needsReview'] as CategoryKey[]).forEach(category => {
727
+ const resultCategory = (singleResult as any)[category];
728
+ if (resultCategory && resultCategory.rules) {
729
+ Object.values(resultCategory.rules).forEach((rule: any) => {
730
+
731
+ // Map the description to the short description if available
732
+ if (constants.a11yRuleShortDescriptionMap[rule.rule]) {
733
+ rule.description = constants.a11yRuleShortDescriptionMap[rule.rule];
734
+ }
735
+
736
+ if (rule.items) {
737
+ rule.items.forEach((item: any) => {
738
+ // Ensure item URL matches the result URL
739
+ item.url = (singleResult as any).url;
740
+
741
+ if (item.displayNeedsReview) {
742
+ delete item.displayNeedsReview;
743
+ }
744
+ });
745
+ }
746
+ });
747
+ }
748
+ });
749
+
750
+ return singleResult;
751
+ }
752
+
753
+ return mergedResults;
754
+ };
755
+
756
+ // This is an experimental feature to scan static HTML code without the need for Playwright browser
757
+ export const scanHTML = async (
758
+ htmlContent: string | string[],
759
+ config: {
760
+ name: string;
761
+ email: string;
762
+ pageUrl?: string; // If array, we will append index
763
+ pageTitle?: string; // If array, we will append index
764
+ metadata?: string;
765
+ ruleset?: RuleFlags[];
766
+ },
767
+ ) => {
768
+ const {
769
+ name,
770
+ email,
771
+ pageUrl = 'raw-html',
772
+ pageTitle = 'HTML Content',
773
+ metadata = '',
774
+ ruleset = [RuleFlags.DEFAULT],
775
+ } = config;
776
+
777
+ const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
778
+ const tags = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];
779
+
780
+ if (enableWcagAaa) {
781
+ tags.push('wcag2aaa');
782
+ }
783
+
784
+ const htmlItems = Array.isArray(htmlContent) ? htmlContent : [htmlContent];
785
+ const scanData = [];
786
+
787
+ for (let i = 0; i < htmlItems.length; i++) {
788
+ const htmlString = htmlItems[i];
789
+ const dom = new JSDOM(htmlString);
790
+
791
+ // Configure axe for node environment
792
+ // eslint-disable-next-line no-await-in-loop
793
+ const axeScanResults = await axe.run(
794
+ dom.window.document.documentElement as unknown as Element,
795
+ {
796
+ runOnly: {
797
+ type: 'tag',
798
+ values: tags,
799
+ },
800
+ resultTypes: ['violations', 'passes', 'incomplete'],
801
+ },
802
+ );
803
+
804
+ scanData.push({
805
+ axeScanResults,
806
+ pageUrl: htmlItems.length > 1 ? `${pageUrl}-${i + 1}` : pageUrl,
807
+ pageTitle: htmlItems.length > 1 ? `${pageTitle} ${i + 1}` : pageTitle,
808
+ });
809
+ }
810
+
811
+ return processAndSubmitResults(scanData, name, email, metadata);
812
+ };
813
+
814
+ export const scanPage = async (
815
+ pages: Page | Page[],
816
+ config: {
817
+ name: string;
818
+ email: string;
819
+ pageTitle?: string;
820
+ metadata?: string;
821
+ ruleset?: RuleFlags[];
822
+ },
823
+ ) => {
824
+ const {
825
+ name,
826
+ email,
827
+ pageTitle,
828
+ metadata = '',
829
+ ruleset = [RuleFlags.DEFAULT],
830
+ } = config;
831
+
832
+ const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
833
+ const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
834
+
835
+ const axeScript = getAxeScriptContent();
836
+ const oobeeFunctions = getOobeeFunctionsScript(disableOobee, enableWcagAaa);
837
+
838
+ const pagesArray = Array.isArray(pages) ? pages : [pages];
839
+ const scanData = [];
840
+
841
+ for (const page of pagesArray) {
842
+ await page.evaluate(`${axeScript}\n${oobeeFunctions}`);
843
+
844
+ // Run the scan inside the page
845
+ const consoleListener = (msg: any) => {
846
+ if (msg.type() === 'error') {
847
+ console.error(`[Browser Console] ${msg.text()}`);
848
+ }
849
+ };
850
+ page.on('console', consoleListener);
851
+
852
+ try {
853
+ const scanResult = await page.evaluate(async () => {
854
+ return window.runA11yScan();
855
+ });
856
+
857
+ scanData.push({
858
+ axeScanResults: scanResult.axeScanResults,
859
+ pageUrl: page.url(),
860
+ pageTitle: await page.title(),
861
+ });
862
+ } finally {
863
+ page.off('console', consoleListener);
864
+ }
865
+ }
866
+
867
+ // Allow override of page title if scanning a single page
868
+ if (!Array.isArray(pages) && pageTitle) {
869
+ scanData[0].pageTitle = pageTitle;
870
+ }
871
+
872
+ return processAndSubmitResults(
873
+ scanData,
874
+ name,
875
+ email,
876
+ metadata,
877
+ );
878
+ };
879
+
880
+ export { RuleFlags };
881
+