@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
@@ -0,0 +1,176 @@
1
+ // File used to hold sample data for unit testing
2
+ export const sampleXmlSitemap = `
3
+ <?xml version="1.0" encoding="UTF-8"?>
4
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
5
+ <!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
6
+ <url>
7
+ <loc>http://www.google.com/</loc>
8
+ <lastmod>2023-01-17T02:49:19+00:00</lastmod>
9
+ <priority>1.00</priority>
10
+ </url>
11
+ <url>
12
+ <loc>http://www.google.com/intl/en/policies/privacy/</loc>
13
+ <lastmod>2020-05-26T07:00:00+00:00</lastmod>
14
+ <priority>0.80</priority>
15
+ </url>
16
+ <!-- <url>
17
+ <loc>http://www.google.com/intl/en/policies/terms/</loc>
18
+ <lastmod>2020-05-26T07:00:00+00:00</lastmod>
19
+ <priority>0.80</priority>
20
+ </url> -->
21
+ </urlset>`;
22
+ export const sampleXmlSitemapLinks = [
23
+ 'http://www.google.com/',
24
+ 'http://www.google.com/intl/en/policies/privacy/',
25
+ ];
26
+ // Source: https://www.feedforall.com/sample-feed.xml
27
+ export const sampleRssFeed = `<?xml version="1.0" encoding="windows-1252"?>
28
+ <rss version="2.0">
29
+ <channel>
30
+ <title>Sample Feed - Favorite RSS Related Software &amp; Resources</title>
31
+ <description>Take a look at some of FeedForAll&apos;s favorite software and resources for learning more about RSS.</description>
32
+ <link>http://www.feedforall.com</link>
33
+ <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
34
+ <copyright>Copyright 2004 NotePage, Inc.</copyright>
35
+ <docs>http://blogs.law.harvard.edu/tech/rss</docs>
36
+ <language>en-us</language>
37
+ <lastBuildDate>Mon, 1 Nov 2004 13:17:17 -0500</lastBuildDate>
38
+ <managingEditor>marketing@feedforall.com</managingEditor>
39
+ <pubDate>Tue, 26 Oct 2004 14:06:44 -0500</pubDate>
40
+ <webMaster>webmaster@feedforall.com</webMaster>
41
+ <generator>FeedForAll Beta1 (0.0.1.8)</generator>
42
+ <image>
43
+ <url>http://www.feedforall.com/feedforall-temp.gif</url>
44
+ <title>FeedForAll Sample Feed</title>
45
+ <link>http://www.feedforall.com/industry-solutions.htm</link>
46
+ <description>FeedForAll Sample Feed</description>
47
+ <width>144</width>
48
+ <height>117</height>
49
+ </image>
50
+ <item>
51
+ <title>RSS Resources</title>
52
+ <description>Be sure to take a look at some of our favorite RSS Resources&lt;br&gt;
53
+ &lt;a href=&quot;http://www.rss-specifications.com&quot;&gt;RSS Specifications&lt;/a&gt;&lt;br&gt;
54
+ &lt;a href=&quot;http://www.blog-connection.com&quot;&gt;Blog Connection&lt;/a&gt;&lt;br&gt;
55
+ &lt;br&gt;</description>
56
+ <link>http://www.feedforall.com</link>
57
+ <pubDate>Tue, 26 Oct 2004 14:01:01 -0500</pubDate>
58
+ </item>
59
+ <item>
60
+ <title>Recommended Desktop Feed Reader Software</title>
61
+ <description>&lt;b&gt;FeedDemon&lt;/b&gt; enables you to quickly read and gather information from hundreds of web sites - without having to visit them. Don&apos;t waste any more time checking your favorite web sites for updates. Instead, use FeedDemon and make them come to you. &lt;br&gt;
62
+ More &lt;a href=&quot;http://store.esellerate.net/a.asp?c=1_SKU5139890208_AFL403073819&quot;&gt;FeedDemon Information&lt;/a&gt;</description>
63
+ <link>http://www.feedforall.com/feedforall-partners.htm</link>
64
+ <pubDate>Tue, 26 Oct 2004 14:03:25 -0500</pubDate>
65
+ </item>
66
+ <item>
67
+ <title>Recommended Web Based Feed Reader Software</title>
68
+ <description>&lt;b&gt;FeedScout&lt;/b&gt; enables you to view RSS/ATOM/RDF feeds from different sites directly in Internet Explorer. You can even set your Home Page to show favorite feeds. Feed Scout is a plug-in for Internet Explorer, so you won&apos;t have to learn anything except for how to press 2 new buttons on Internet Explorer toolbar. &lt;br&gt;
69
+ More &lt;a href=&quot;http://www.bytescout.com/feedscout.html&quot;&gt;Information on FeedScout&lt;/a&gt;&lt;br&gt;
70
+ &lt;br&gt;
71
+ &lt;br&gt;
72
+ &lt;b&gt;SurfPack&lt;/b&gt; can feature search tools, horoscopes, current weather conditions, LiveJournal diaries, humor, web modules and other dynamically updated content. &lt;br&gt;
73
+ More &lt;a href=&quot;http://www.surfpack.com/&quot;&gt;Information on SurfPack&lt;/a&gt;&lt;br&gt;</description>
74
+ <link>http://www.feedforall.com/feedforall-partners.htm</link>
75
+ <pubDate>Tue, 26 Oct 2004 14:06:44 -0500</pubDate>
76
+ </item>
77
+ </channel>
78
+ </rss>`;
79
+ export const sampleRssFeedLinks = [
80
+ 'http://www.feedforall.com',
81
+ 'http://www.feedforall.com/industry-solutions.htm',
82
+ 'http://www.feedforall.com/feedforall-partners.htm',
83
+ ];
84
+ // Source: https://www.ietf.org/rfc/rfc4287.txt
85
+ export const sampleAtomFeed = `<?xml version="1.0" encoding="utf-8"?>
86
+ <feed xmlns="http://www.w3.org/2005/Atom">
87
+ <title type="text">dive into mark</title>
88
+ <subtitle type="html">
89
+ A &lt;em&gt;lot&lt;/em&gt; of effort
90
+ went into making this effortless
91
+ </subtitle>
92
+ <updated>2005-07-31T12:29:29Z</updated>
93
+ <id>tag:example.org,2003:3</id>
94
+ <link rel="alternate" type="text/html"
95
+ hreflang="en" href="http://example.org/"/>
96
+ <link rel="self" type="application/atom+xml"
97
+ href="http://example.org/feed.atom"/>
98
+ <rights>Copyright (c) 2003, Mark Pilgrim</rights>
99
+ <generator uri="http://www.example.com/" version="1.0">
100
+ Example Toolkit
101
+ </generator>
102
+ <entry>
103
+ <title>Atom draft-07 snapshot</title>
104
+ <link rel="alternate" type="text/html"
105
+ href="http://example.org/2005/04/02/atom"/>
106
+ <link rel="enclosure" type="audio/mpeg" length="1337"
107
+ href="http://example.org/audio/ph34r_my_podcast.mp3"/>
108
+ <id>tag:example.org,2003:3.2397</id>
109
+ <updated>2005-07-31T12:29:29Z</updated>
110
+ <published>2003-12-13T08:29:29-04:00</published>
111
+ <author>
112
+ <name>Mark Pilgrim</name>
113
+ <uri>http://example.org/</uri>
114
+ <email>f8dy@example.com</email>
115
+ </author>
116
+ <contributor>
117
+ <name>Sam Ruby</name>
118
+ </contributor>
119
+ <contributor>
120
+ <name>Joe Gregorio</name>
121
+ </contributor>
122
+ <content type="xhtml" xml:lang="en"
123
+ xml:base="http://diveintomark.org/">
124
+ <div xmlns="http://www.w3.org/1999/xhtml">
125
+ <p><i>[Update: The Atom draft is finished.]</i></p>
126
+ </div>
127
+ </content>
128
+ </entry>
129
+ </feed>`;
130
+ export const sampleAtomFeedLinks = [
131
+ 'http://example.org/',
132
+ 'http://example.org/feed.atom',
133
+ 'http://example.org/2005/04/02/atom',
134
+ 'http://example.org/audio/ph34r_my_podcast.mp3',
135
+ ];
136
+ // Following format stated in https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap#text
137
+ export const sampleTxtSitemap = `https://www.example.com/file1.html
138
+ https://www.example.com/file2.html
139
+ https://www.example.com/file3.html`;
140
+ export const sampleTxtSitemapLinks = [
141
+ 'https://www.example.com/file1.html',
142
+ 'https://www.example.com/file2.html',
143
+ 'https://www.example.com/file3.html',
144
+ ];
145
+ export const sampleNonStandardXmlSitemap = `<?xml version="1.0" encoding="UTF-8"?>
146
+ <contents>
147
+ <url>https://www.example.com/file1.html</url>
148
+ <link href='https://www.example.com/file2.html' />
149
+ <link>https://www.example.com/file3.html</link>
150
+ </contents>`;
151
+ export const sampleNonStandardXmlSitemapLinks = [
152
+ 'https://www.example.com/file1.html',
153
+ 'https://www.example.com/file2.html',
154
+ 'https://www.example.com/file3.html',
155
+ ];
156
+ export const sampleRssSitemap2 = `<?xml version="1.0" encoding="utf-8"?>
157
+ <feed xmlns="http://www.w3.org/2005/Atom">
158
+
159
+ <title>Example Feed</title>
160
+ <link href="http://example.org/"/>
161
+ <updated>2003-12-13T18:30:02Z</updated>
162
+ <author>
163
+ <name>John Doe</name>
164
+ </author>
165
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
166
+
167
+ <entry>
168
+ <title>Atom-Powered Robots Run Amok</title>
169
+ <link href="http://example.org/2003/12/13/atom03"/>
170
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
171
+ <updated>2003-12-13T18:30:02Z</updated>
172
+ <summary>Some text.</summary>
173
+ </entry>
174
+
175
+ </feed>
176
+ `;
@@ -0,0 +1,428 @@
1
+ import { Dataset, RequestQueue, log, playwrightUtils } from 'crawlee';
2
+ import axe from 'axe-core';
3
+ import { axeScript, disallowedListOfPatterns, guiInfoStatusTypes, RuleFlags, saflyIconSelector, } from '../constants/constants.js';
4
+ import { consoleLogger, guiInfoLog } from '../logs.js';
5
+ import { takeScreenshotForHTMLElements } from '../screenshotFunc/htmlScreenshotFunc.js';
6
+ import { isFilePath } from '../constants/common.js';
7
+ import { extractAndGradeText } from './custom/extractAndGradeText.js';
8
+ import { evaluateAltText } from './custom/evaluateAltText.js';
9
+ import { escapeCssSelector } from './custom/escapeCssSelector.js';
10
+ import { framesCheck } from './custom/framesCheck.js';
11
+ import { findElementByCssSelector } from './custom/findElementByCssSelector.js';
12
+ import { getAxeConfiguration } from './custom/getAxeConfiguration.js';
13
+ import { flagUnlabelledClickableElements } from './custom/flagUnlabelledClickableElements.js';
14
+ import xPathToCss from './custom/xPathToCss.js';
15
+ import { getStoragePath } from '../utils.js';
16
+ import path from 'path';
17
+ const truncateHtml = (html, maxBytes = 1024, suffix = '…') => {
18
+ const encoder = new TextEncoder();
19
+ if (encoder.encode(html).length <= maxBytes)
20
+ return html;
21
+ let left = 0;
22
+ let right = html.length;
23
+ let result = '';
24
+ while (left <= right) {
25
+ const mid = Math.floor((left + right) / 2);
26
+ const truncated = html.slice(0, mid) + suffix;
27
+ const bytes = encoder.encode(truncated).length;
28
+ if (bytes <= maxBytes) {
29
+ result = truncated;
30
+ left = mid + 1;
31
+ }
32
+ else {
33
+ right = mid - 1;
34
+ }
35
+ }
36
+ return result;
37
+ };
38
+ export const filterAxeResults = (results, pageTitle, customFlowDetails) => {
39
+ const { violations, passes, incomplete, url } = results;
40
+ let totalItems = 0;
41
+ const mustFix = { totalItems: 0, rules: {} };
42
+ const goodToFix = { totalItems: 0, rules: {} };
43
+ const passed = { totalItems: 0, rules: {} };
44
+ const needsReview = { totalItems: 0, rules: {} };
45
+ const process = (item, displayNeedsReview) => {
46
+ const { id: rule, help: description, helpUrl, tags, nodes } = item;
47
+ if (rule === 'frame-tested')
48
+ return;
49
+ const conformance = tags.filter(tag => tag.startsWith('wcag') || tag === 'best-practice');
50
+ // handle rare cases where conformance level is not the first element
51
+ const wcagRegex = /^wcag\d+a+$/;
52
+ if (conformance[0] !== 'best-practice' && !wcagRegex.test(conformance[0])) {
53
+ conformance.sort((a, b) => {
54
+ if (wcagRegex.test(a) && !wcagRegex.test(b)) {
55
+ return -1;
56
+ }
57
+ if (!wcagRegex.test(a) && wcagRegex.test(b)) {
58
+ return 1;
59
+ }
60
+ return 0;
61
+ });
62
+ }
63
+ const addTo = (category, node) => {
64
+ const { html, failureSummary, screenshotPath, target, impact: axeImpact } = node;
65
+ if (!(rule in category.rules)) {
66
+ category.rules[rule] = {
67
+ description,
68
+ axeImpact,
69
+ helpUrl,
70
+ conformance,
71
+ totalItems: 0,
72
+ items: [],
73
+ };
74
+ }
75
+ const message = displayNeedsReview
76
+ ? failureSummary.slice(failureSummary.indexOf('\n') + 1).trim()
77
+ : failureSummary;
78
+ let finalHtml = html;
79
+ if (html.includes('</script>')) {
80
+ finalHtml = html.replaceAll('</script>', '&lt;/script>');
81
+ }
82
+ finalHtml = truncateHtml(finalHtml);
83
+ const xpath = target.length === 1 && typeof target[0] === 'string' ? target[0] : null;
84
+ // add in screenshot path
85
+ category.rules[rule].items.push({
86
+ html: finalHtml,
87
+ message,
88
+ screenshotPath,
89
+ xpath: xpath || undefined,
90
+ displayNeedsReview: displayNeedsReview || undefined,
91
+ });
92
+ category.rules[rule].totalItems += 1;
93
+ category.totalItems += 1;
94
+ totalItems += 1;
95
+ };
96
+ nodes.forEach(node => {
97
+ const hasWcagA = conformance.some(tag => /^wcag\d*a$/.test(tag));
98
+ const hasWcagAA = conformance.some(tag => /^wcag\d*aa$/.test(tag));
99
+ // const hasWcagAAA = conformance.some(tag => /^wcag\d*aaa$/.test(tag));
100
+ if (displayNeedsReview) {
101
+ addTo(needsReview, node);
102
+ }
103
+ else if (hasWcagA || hasWcagAA) {
104
+ addTo(mustFix, node);
105
+ }
106
+ else {
107
+ addTo(goodToFix, node);
108
+ }
109
+ });
110
+ };
111
+ violations.forEach(item => process(item, false));
112
+ incomplete.forEach(item => process(item, true));
113
+ passes.forEach((item) => {
114
+ const { id: rule, help: description, impact: axeImpact, helpUrl, tags, nodes } = item;
115
+ if (rule === 'frame-tested')
116
+ return;
117
+ const conformance = tags.filter(tag => tag.startsWith('wcag') || tag === 'best-practice');
118
+ nodes.forEach(node => {
119
+ const { html } = node;
120
+ if (!(rule in passed.rules)) {
121
+ passed.rules[rule] = {
122
+ description,
123
+ axeImpact,
124
+ helpUrl,
125
+ conformance,
126
+ totalItems: 0,
127
+ items: [],
128
+ };
129
+ }
130
+ const finalHtml = truncateHtml(html);
131
+ passed.rules[rule].items.push({ html: finalHtml, screenshotPath: '', message: '', xpath: '' });
132
+ passed.totalItems += 1;
133
+ passed.rules[rule].totalItems += 1;
134
+ totalItems += 1;
135
+ });
136
+ });
137
+ return {
138
+ url,
139
+ pageTitle: customFlowDetails ? `${customFlowDetails.pageIndex}: ${pageTitle}` : pageTitle,
140
+ pageIndex: customFlowDetails ? customFlowDetails.pageIndex : undefined,
141
+ metadata: customFlowDetails?.metadata
142
+ ? `${customFlowDetails.pageIndex}: ${customFlowDetails.metadata}`
143
+ : undefined,
144
+ pageImagePath: customFlowDetails ? customFlowDetails.pageImagePath : undefined,
145
+ totalItems,
146
+ mustFix,
147
+ goodToFix,
148
+ needsReview,
149
+ passed,
150
+ };
151
+ };
152
+ export const runAxeScript = async ({ includeScreenshots, page, randomToken, customFlowDetails = null, selectors = [], ruleset = [], }) => {
153
+ const browserContext = page.context();
154
+ const requestUrl = page.url();
155
+ try {
156
+ // Checking for DOM mutations before proceeding to scan
157
+ await page.evaluate(() => {
158
+ return new Promise(resolve => {
159
+ let timeout;
160
+ let mutationCount = 0;
161
+ const MAX_MUTATIONS = 500;
162
+ const MAX_SAME_MUTATION_LIMIT = 10;
163
+ const mutationHash = {};
164
+ const observer = new MutationObserver(mutationsList => {
165
+ clearTimeout(timeout);
166
+ mutationCount += 1;
167
+ if (mutationCount > MAX_MUTATIONS) {
168
+ observer.disconnect();
169
+ resolve('Too many mutations detected');
170
+ }
171
+ // To handle scenario where DOM elements are constantly changing and unable to exit
172
+ mutationsList.forEach(mutation => {
173
+ let mutationKey;
174
+ if (mutation.target instanceof Element) {
175
+ Array.from(mutation.target.attributes).forEach(attr => {
176
+ mutationKey = `${mutation.target.nodeName}-${attr.name}`;
177
+ if (mutationKey) {
178
+ if (!mutationHash[mutationKey]) {
179
+ mutationHash[mutationKey] = 1;
180
+ }
181
+ else {
182
+ mutationHash[mutationKey] += 1;
183
+ }
184
+ if (mutationHash[mutationKey] >= MAX_SAME_MUTATION_LIMIT) {
185
+ observer.disconnect();
186
+ resolve(`Repeated mutation detected for ${mutationKey}`);
187
+ }
188
+ }
189
+ });
190
+ }
191
+ });
192
+ timeout = setTimeout(() => {
193
+ observer.disconnect();
194
+ resolve('DOM stabilized after mutations.');
195
+ }, 1000);
196
+ });
197
+ timeout = setTimeout(() => {
198
+ observer.disconnect();
199
+ resolve('No mutations detected, exit from idle state');
200
+ }, 1000);
201
+ observer.observe(document, { childList: true, subtree: true, attributes: true });
202
+ });
203
+ });
204
+ }
205
+ catch (e) {
206
+ // do nothing, just continue
207
+ }
208
+ // Omit logging of browser console errors to reduce unnecessary verbosity
209
+ /*
210
+ page.on('console', msg => {
211
+ const type = msg.type();
212
+ if (type === 'error') {
213
+ consoleLogger.error(msg.text());
214
+ } else {
215
+ consoleLogger.info(msg.text());
216
+ }
217
+ });
218
+ */
219
+ const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
220
+ const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
221
+ const gradingReadabilityFlag = await extractAndGradeText(page); // Ensure flag is obtained before proceeding
222
+ await playwrightUtils.injectFile(page, axeScript);
223
+ const results = await page.evaluate(async ({ selectors, saflyIconSelector, disableOobee, enableWcagAaa, gradingReadabilityFlag, evaluateAltTextFunctionString, escapeCssSelectorFunctionString, framesCheckFunctionString, findElementByCssSelectorFunctionString, getAxeConfigurationFunctionString, flagUnlabelledClickableElementsFunctionString, xPathToCssFunctionString, }) => {
224
+ try {
225
+ // Load functions into the browser context
226
+ eval(evaluateAltTextFunctionString);
227
+ eval(escapeCssSelectorFunctionString);
228
+ eval(framesCheckFunctionString);
229
+ eval(findElementByCssSelectorFunctionString);
230
+ eval(flagUnlabelledClickableElementsFunctionString);
231
+ eval(xPathToCssFunctionString);
232
+ eval(getAxeConfigurationFunctionString);
233
+ // remove so that axe does not scan
234
+ document.querySelector(saflyIconSelector)?.remove();
235
+ const oobeeAccessibleLabelFlaggedXpaths = disableOobee
236
+ ? []
237
+ : (await flagUnlabelledClickableElements()).map(item => item.xpath);
238
+ const oobeeAccessibleLabelFlaggedCssSelectors = oobeeAccessibleLabelFlaggedXpaths
239
+ .map(xpath => {
240
+ try {
241
+ const cssSelector = xPathToCss(xpath);
242
+ return cssSelector;
243
+ }
244
+ catch (e) {
245
+ console.error('Error converting XPath to CSS: ', xpath, e);
246
+ return '';
247
+ }
248
+ })
249
+ .filter(item => item !== '');
250
+ const axeConfig = getAxeConfiguration({
251
+ enableWcagAaa,
252
+ gradingReadabilityFlag,
253
+ disableOobee,
254
+ });
255
+ axe.configure(axeConfig);
256
+ // removed needsReview condition
257
+ const defaultResultTypes = ['violations', 'passes', 'incomplete'];
258
+ return axe
259
+ .run(selectors, {
260
+ resultTypes: defaultResultTypes,
261
+ })
262
+ .then(results => {
263
+ if (disableOobee) {
264
+ return results;
265
+ }
266
+ // handle css id selectors that start with a digit
267
+ const escapedCssSelectors = oobeeAccessibleLabelFlaggedCssSelectors.map(escapeCssSelector);
268
+ // Add oobee violations to Axe's report
269
+ const oobeeAccessibleLabelViolations = {
270
+ id: 'oobee-accessible-label',
271
+ impact: 'serious',
272
+ tags: ['wcag2a', 'wcag211', 'wcag412'],
273
+ description: 'Ensures clickable elements have an accessible label.',
274
+ help: 'Clickable elements (i.e. elements with mouse-click interaction) must have accessible labels.',
275
+ helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
276
+ nodes: escapedCssSelectors
277
+ .map((cssSelector) => ({
278
+ html: findElementByCssSelector(cssSelector),
279
+ target: [cssSelector],
280
+ impact: 'serious',
281
+ failureSummary: 'Fix any of the following:\n The clickable element does not have an accessible label.',
282
+ any: [
283
+ {
284
+ id: 'oobee-accessible-label',
285
+ data: null,
286
+ relatedNodes: [],
287
+ impact: 'serious',
288
+ message: 'The clickable element does not have an accessible label.',
289
+ },
290
+ ],
291
+ all: [],
292
+ none: [],
293
+ }))
294
+ .filter(item => item.html),
295
+ };
296
+ results.violations = [...results.violations, oobeeAccessibleLabelViolations];
297
+ return results;
298
+ })
299
+ .catch(e => {
300
+ console.error('Error at axe.run', e);
301
+ throw e;
302
+ });
303
+ }
304
+ catch (e) {
305
+ console.error(e);
306
+ throw e;
307
+ }
308
+ }, {
309
+ selectors,
310
+ saflyIconSelector,
311
+ disableOobee,
312
+ enableWcagAaa,
313
+ gradingReadabilityFlag,
314
+ evaluateAltTextFunctionString: evaluateAltText.toString(),
315
+ escapeCssSelectorFunctionString: escapeCssSelector.toString(),
316
+ framesCheckFunctionString: framesCheck.toString(),
317
+ findElementByCssSelectorFunctionString: findElementByCssSelector.toString(),
318
+ getAxeConfigurationFunctionString: getAxeConfiguration.toString(),
319
+ flagUnlabelledClickableElementsFunctionString: flagUnlabelledClickableElements.toString(),
320
+ xPathToCssFunctionString: xPathToCss.toString(),
321
+ });
322
+ if (includeScreenshots) {
323
+ results.violations = await takeScreenshotForHTMLElements(results.violations, page, randomToken);
324
+ results.incomplete = await takeScreenshotForHTMLElements(results.incomplete, page, randomToken);
325
+ }
326
+ let pageTitle = null;
327
+ try {
328
+ pageTitle = await page.evaluate(() => document.title);
329
+ }
330
+ catch (e) {
331
+ consoleLogger.info(`Error while getting page title: ${e}`);
332
+ if (page.isClosed()) {
333
+ consoleLogger.info(`Page was closed for ${requestUrl}, creating new page`);
334
+ page = await browserContext.newPage();
335
+ await page.goto(requestUrl, { waitUntil: 'domcontentloaded' });
336
+ pageTitle = await page.evaluate(() => document.title);
337
+ }
338
+ }
339
+ return filterAxeResults(results, pageTitle, customFlowDetails);
340
+ };
341
+ export const createCrawleeSubFolders = async (randomToken) => {
342
+ const crawleeDir = path.join(getStoragePath(randomToken), "crawlee");
343
+ const dataset = await Dataset.open(crawleeDir);
344
+ const requestQueue = await RequestQueue.open(crawleeDir);
345
+ return { dataset, requestQueue };
346
+ };
347
+ export const preNavigationHooks = (extraHTTPHeaders) => {
348
+ return [
349
+ async (crawlingContext, gotoOptions) => {
350
+ if (extraHTTPHeaders) {
351
+ crawlingContext.request.headers = extraHTTPHeaders;
352
+ }
353
+ gotoOptions = { waitUntil: 'networkidle', timeout: 30000 };
354
+ },
355
+ ];
356
+ };
357
+ export const postNavigationHooks = [
358
+ async (_crawlingContext) => {
359
+ guiInfoLog(guiInfoStatusTypes.COMPLETED, {});
360
+ },
361
+ ];
362
+ export const failedRequestHandler = async ({ request }) => {
363
+ guiInfoLog(guiInfoStatusTypes.ERROR, { numScanned: 0, urlScanned: request.url });
364
+ log.error(`Failed Request - ${request.url}: ${request.errorMessages}`);
365
+ };
366
+ export const isUrlPdf = (url) => {
367
+ if (isFilePath(url)) {
368
+ return /\.pdf$/i.test(url);
369
+ }
370
+ const parsedUrl = new URL(url);
371
+ return /\.pdf($|\?|#)/i.test(parsedUrl.pathname) || /\.pdf($|\?|#)/i.test(parsedUrl.href);
372
+ };
373
+ export async function shouldSkipClickDueToDisallowedHref(page, element) {
374
+ return await page.evaluate(({ el, disallowedPrefixes }) => {
375
+ function isDisallowedHref(href) {
376
+ if (!href)
377
+ return false;
378
+ href = href.toLowerCase();
379
+ return disallowedPrefixes.some((prefix) => href.startsWith(prefix));
380
+ }
381
+ const castEl = el;
382
+ // Check descendant <a href="">
383
+ const descendants = castEl.querySelectorAll('a[href]');
384
+ for (const a of descendants) {
385
+ const href = a.getAttribute('href');
386
+ if (isDisallowedHref(href)) {
387
+ return true;
388
+ }
389
+ }
390
+ // Check self and ancestors for disallowed <a>
391
+ let current = castEl;
392
+ while (current) {
393
+ if (current.tagName === 'A' &&
394
+ isDisallowedHref(current.getAttribute('href'))) {
395
+ return true;
396
+ }
397
+ current = current.parentElement;
398
+ }
399
+ return false;
400
+ }, {
401
+ el: element,
402
+ disallowedPrefixes: disallowedListOfPatterns,
403
+ });
404
+ }
405
+ /**
406
+ * Check if response should be skipped based on content headers.
407
+ * @param response - Playwright Response object
408
+ * @param requestUrl - Optional: request URL for logging
409
+ * @returns true if the content should be skipped
410
+ */
411
+ export const shouldSkipDueToUnsupportedContent = (response, requestUrl = '') => {
412
+ if (!response)
413
+ return false;
414
+ const headers = response.headers();
415
+ const contentDisposition = headers['content-disposition'] || '';
416
+ const contentType = headers['content-type'] || '';
417
+ if (contentDisposition.includes('attachment')) {
418
+ // consoleLogger.info(`Skipping attachment (content-disposition) at ${requestUrl}`);
419
+ return true;
420
+ }
421
+ if (contentType.startsWith('application/') ||
422
+ contentType.includes('octet-stream') ||
423
+ (!contentType.startsWith('text/') && !contentType.includes('html'))) {
424
+ // consoleLogger.info(`Skipping non-processible content-type "${contentType}" at ${requestUrl}`);
425
+ return true;
426
+ }
427
+ return false;
428
+ };