@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.
- package/.github/workflows/publish.yml +8 -1
- package/INTEGRATION.md +50 -3
- package/dist/cli.js +252 -0
- package/dist/combine.js +221 -0
- package/dist/constants/cliFunctions.js +306 -0
- package/dist/constants/common.js +1669 -0
- package/dist/constants/constants.js +913 -0
- package/dist/constants/errorMeta.json +319 -0
- package/dist/constants/itemTypeDescription.js +7 -0
- package/dist/constants/oobeeAi.js +121 -0
- package/dist/constants/questions.js +151 -0
- package/dist/constants/sampleData.js +176 -0
- package/dist/crawlers/commonCrawlerFunc.js +428 -0
- package/dist/crawlers/crawlDomain.js +613 -0
- package/dist/crawlers/crawlIntelligentSitemap.js +135 -0
- package/dist/crawlers/crawlLocalFile.js +151 -0
- package/dist/crawlers/crawlSitemap.js +303 -0
- package/dist/crawlers/custom/escapeCssSelector.js +10 -0
- package/dist/crawlers/custom/evaluateAltText.js +11 -0
- package/dist/crawlers/custom/extractAndGradeText.js +44 -0
- package/dist/crawlers/custom/extractText.js +27 -0
- package/dist/crawlers/custom/findElementByCssSelector.js +36 -0
- package/dist/crawlers/custom/flagUnlabelledClickableElements.js +963 -0
- package/dist/crawlers/custom/framesCheck.js +37 -0
- package/dist/crawlers/custom/getAxeConfiguration.js +111 -0
- package/dist/crawlers/custom/gradeReadability.js +23 -0
- package/dist/crawlers/custom/utils.js +1024 -0
- package/dist/crawlers/custom/xPathToCss.js +147 -0
- package/dist/crawlers/guards/urlGuard.js +71 -0
- package/dist/crawlers/pdfScanFunc.js +276 -0
- package/dist/crawlers/runCustom.js +89 -0
- package/dist/exclusions.txt +7 -0
- package/dist/generateHtmlReport.js +144 -0
- package/dist/index.js +62 -0
- package/dist/logs.js +84 -0
- package/dist/mergeAxeResults.js +1588 -0
- package/dist/npmIndex.js +640 -0
- package/dist/proxyService.js +360 -0
- package/dist/runGenerateJustHtmlReport.js +16 -0
- package/dist/screenshotFunc/htmlScreenshotFunc.js +355 -0
- package/dist/screenshotFunc/pdfScreenshotFunc.js +645 -0
- package/dist/services/s3Uploader.js +127 -0
- package/dist/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
- package/dist/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
- package/dist/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
- package/dist/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
- package/dist/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
- package/dist/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
- package/dist/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
- package/dist/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
- package/dist/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
- package/dist/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
- package/dist/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
- package/dist/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
- package/dist/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
- package/dist/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
- package/dist/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
- package/dist/static/ejs/partials/components/summaryScanResults.ejs +16 -0
- package/dist/static/ejs/partials/components/summaryTable.ejs +20 -0
- package/dist/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
- package/dist/static/ejs/partials/components/topTen.ejs +6 -0
- package/dist/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
- package/dist/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
- package/dist/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
- package/dist/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
- package/dist/static/ejs/partials/footer.ejs +24 -0
- package/dist/static/ejs/partials/header.ejs +14 -0
- package/dist/static/ejs/partials/main.ejs +29 -0
- package/dist/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
- package/dist/static/ejs/partials/scripts/bootstrap.ejs +8 -0
- package/dist/static/ejs/partials/scripts/categorySummary.ejs +141 -0
- package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
- package/dist/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
- package/dist/static/ejs/partials/scripts/highlightjs.ejs +335 -0
- package/dist/static/ejs/partials/scripts/popper.ejs +7 -0
- package/dist/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
- package/dist/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
- package/dist/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
- package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +957 -0
- package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +353 -0
- package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
- package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
- package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
- package/dist/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
- package/dist/static/ejs/partials/scripts/screenshotLightbox.ejs +75 -0
- package/dist/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
- package/dist/static/ejs/partials/scripts/summaryTable.ejs +78 -0
- package/dist/static/ejs/partials/scripts/topTen.ejs +61 -0
- package/dist/static/ejs/partials/scripts/utils.ejs +453 -0
- package/dist/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
- package/dist/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
- package/dist/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
- package/dist/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
- package/dist/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
- package/dist/static/ejs/partials/styles/bootstrap.ejs +12391 -0
- package/dist/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
- package/dist/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
- package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
- package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
- package/dist/static/ejs/partials/styles/header.ejs +7 -0
- package/dist/static/ejs/partials/styles/highlightjs.ejs +54 -0
- package/dist/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
- package/dist/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
- package/dist/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
- package/dist/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
- package/dist/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
- package/dist/static/ejs/partials/styles/styles.ejs +1607 -0
- package/dist/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
- package/dist/static/ejs/partials/styles/topTenCard.ejs +44 -0
- package/dist/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
- package/dist/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
- package/dist/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
- package/dist/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
- package/dist/static/ejs/partials/summaryHeader.ejs +70 -0
- package/dist/static/ejs/partials/summaryMain.ejs +49 -0
- package/dist/static/ejs/report.ejs +226 -0
- package/dist/static/ejs/summary.ejs +47 -0
- package/dist/types/types.js +1 -0
- package/dist/utils.js +1070 -0
- package/examples/oobee-cypress-integration-js/cypress/support/e2e.js +36 -6
- package/examples/oobee-cypress-integration-js/cypress.config.js +45 -1
- package/examples/oobee-cypress-integration-ts/cypress.config.ts +47 -1
- package/examples/oobee-cypress-integration-ts/src/cypress/support/e2e.ts +36 -6
- package/examples/oobee-playwright-integration-js/oobee-playwright-demo.js +2 -1
- package/examples/oobee-playwright-integration-ts/src/oobee-playwright-demo.ts +2 -1
- package/examples/oobee-scan-html-demo.js +51 -0
- package/examples/oobee-scan-page-demo.js +40 -0
- package/package.json +9 -3
- package/src/constants/common.ts +2 -2
- package/src/constants/constants.ts +3 -1
- package/src/crawlers/crawlDomain.ts +1 -0
- package/src/crawlers/runCustom.ts +0 -1
- package/src/mergeAxeResults.ts +43 -22
- 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 & Resources</title>
|
|
31
|
+
<description>Take a look at some of FeedForAll'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<br>
|
|
53
|
+
<a href="http://www.rss-specifications.com">RSS Specifications</a><br>
|
|
54
|
+
<a href="http://www.blog-connection.com">Blog Connection</a><br>
|
|
55
|
+
<br></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><b>FeedDemon</b> enables you to quickly read and gather information from hundreds of web sites - without having to visit them. Don't waste any more time checking your favorite web sites for updates. Instead, use FeedDemon and make them come to you. <br>
|
|
62
|
+
More <a href="http://store.esellerate.net/a.asp?c=1_SKU5139890208_AFL403073819">FeedDemon Information</a></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><b>FeedScout</b> 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't have to learn anything except for how to press 2 new buttons on Internet Explorer toolbar. <br>
|
|
69
|
+
More <a href="http://www.bytescout.com/feedscout.html">Information on FeedScout</a><br>
|
|
70
|
+
<br>
|
|
71
|
+
<br>
|
|
72
|
+
<b>SurfPack</b> can feature search tools, horoscopes, current weather conditions, LiveJournal diaries, humor, web modules and other dynamically updated content. <br>
|
|
73
|
+
More <a href="http://www.surfpack.com/">Information on SurfPack</a><br></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 <em>lot</em> 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>', '</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
|
+
};
|