@govtechsg/oobee 0.10.20 → 0.10.28

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 (42) hide show
  1. package/.github/workflows/docker-test.yml +1 -1
  2. package/DETAILS.md +40 -25
  3. package/Dockerfile +41 -47
  4. package/LICENSE-3RD-PARTY-REPORT.txt +448 -0
  5. package/LICENSE-3RD-PARTY.txt +19913 -0
  6. package/README.md +26 -0
  7. package/__mocks__/mock-report.html +1503 -1360
  8. package/package.json +9 -5
  9. package/scripts/decodeUnzipParse.js +29 -0
  10. package/scripts/install_oobee_dependencies.command +2 -2
  11. package/scripts/install_oobee_dependencies.ps1 +3 -3
  12. package/src/cli.ts +9 -7
  13. package/src/combine.ts +13 -5
  14. package/src/constants/cliFunctions.ts +38 -1
  15. package/src/constants/common.ts +31 -5
  16. package/src/constants/constants.ts +28 -26
  17. package/src/constants/questions.ts +4 -1
  18. package/src/crawlers/commonCrawlerFunc.ts +114 -152
  19. package/src/crawlers/crawlDomain.ts +25 -32
  20. package/src/crawlers/crawlIntelligentSitemap.ts +7 -1
  21. package/src/crawlers/crawlLocalFile.ts +1 -1
  22. package/src/crawlers/crawlSitemap.ts +1 -1
  23. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +546 -472
  24. package/src/crawlers/customAxeFunctions.ts +1 -1
  25. package/src/index.ts +2 -2
  26. package/src/mergeAxeResults.ts +590 -214
  27. package/src/screenshotFunc/pdfScreenshotFunc.ts +3 -3
  28. package/src/static/ejs/partials/components/scanAbout.ejs +65 -0
  29. package/src/static/ejs/partials/components/wcagCompliance.ejs +10 -29
  30. package/src/static/ejs/partials/footer.ejs +10 -13
  31. package/src/static/ejs/partials/scripts/categorySummary.ejs +2 -2
  32. package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  33. package/src/static/ejs/partials/scripts/reportSearch.ejs +1 -0
  34. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +54 -52
  35. package/src/static/ejs/partials/scripts/scanAboutScript.ejs +38 -0
  36. package/src/static/ejs/partials/styles/styles.ejs +26 -1
  37. package/src/static/ejs/partials/summaryMain.ejs +15 -42
  38. package/src/static/ejs/report.ejs +22 -12
  39. package/src/utils.ts +10 -2
  40. package/src/xPathToCss.ts +186 -0
  41. package/a11y-scan-results.zip +0 -0
  42. package/src/types/xpath-to-css.d.ts +0 -3
@@ -195,6 +195,7 @@
195
195
 
196
196
  function resetIssueOccurrence(filteredItems) {
197
197
  for (let category in filteredItems) {
198
+ if (!["mustFix", "goodToFix", "needsReview", "passed"].includes(category)) continue; // skip other props like pagesScanned, etc
198
199
  const issueLabel = filteredItems[category].rules.length === 1 ? 'issue' : 'issues';
199
200
  const occurrenceLabel =
200
201
  filteredItems[category].totalItems === 1 ? 'occurrence' : 'occurrences';
@@ -12,31 +12,29 @@ category summary is clicked %>
12
12
  wcag111: 'https://www.w3.org/TR/WCAG22/#non-text-content',
13
13
  wcag122: 'https://www.w3.org/TR/WCAG22/#captions-prerecorded',
14
14
  wcag131: 'https://www.w3.org/TR/WCAG22/#info-and-relationships',
15
- wcag134: 'https://www.w3.org/TR/WCAG22/#orientation',
15
+ // wcag134: 'https://www.w3.org/TR/WCAG22/#orientation',
16
16
  wcag135: 'https://www.w3.org/TR/WCAG22/#identify-input-purpose',
17
17
  wcag141: 'https://www.w3.org/TR/WCAG22/#use-of-color',
18
18
  wcag142: 'https://www.w3.org/TR/WCAG22/#audio-control',
19
19
  wcag143: 'https://www.w3.org/TR/WCAG22/#contrast-minimum',
20
20
  wcag144: 'https://www.w3.org/TR/WCAG22/#resize-text',
21
- wcag146: 'https://www.w3.org/TR/WCAG21/#contrast-enhanced',
22
- wcag1410: 'https://www.w3.org/TR/WCAG22/#reflow',
21
+ wcag146: 'https://www.w3.org/TR/WCAG22/#contrast-enhanced', // AAA
22
+ // wcag1410: 'https://www.w3.org/TR/WCAG22/#reflow',
23
23
  wcag1412: 'https://www.w3.org/TR/WCAG22/#text-spacing',
24
24
  wcag211: 'https://www.w3.org/TR/WCAG22/#keyboard',
25
25
  wcag221: 'https://www.w3.org/TR/WCAG22/#timing-adjustable',
26
26
  wcag222: 'https://www.w3.org/TR/WCAG22/#pause-stop-hide',
27
- wcag224: 'https://www.w3.org/TR/WCAG21/#interruptions',
27
+ wcag224: 'https://www.w3.org/TR/WCAG22/#interruptions', // AAA
28
28
  wcag241: 'https://www.w3.org/TR/WCAG22/#bypass-blocks',
29
29
  wcag242: 'https://www.w3.org/TR/WCAG22/#page-titled',
30
- wcag243: 'https://www.w3.org/TR/WCAG22/#focus-order',
31
30
  wcag244: 'https://www.w3.org/TR/WCAG22/#link-purpose-in-context',
32
- wcag249: 'https://www.w3.org/TR/WCAG21/#link-purpose-link-only',
31
+ wcag249: 'https://www.w3.org/TR/WCAG22/#link-purpose-link-only', // AAA
33
32
  wcag258: 'https://www.w3.org/TR/WCAG22/#target-size-minimum',
34
33
  wcag311: 'https://www.w3.org/TR/WCAG22/#language-of-page',
35
34
  wcag312: 'https://www.w3.org/TR/WCAG22/#language-of-parts',
36
- wcag315: 'https://www.w3.org/TR/WCAG22/#reading-level',
35
+ wcag315: 'https://www.w3.org/TR/WCAG22/#reading-level', // AAA
36
+ wcag325: 'https://www.w3.org/TR/WCAG22/#change-on-request', // AAA
37
37
  wcag332: 'https://www.w3.org/TR/WCAG22/#labels-or-instructions',
38
- wcag325: 'https://www.w3.org/TR/WCAG21/#change-on-request',
39
- wcag411: 'https://www.w3.org/TR/WCAG22/#parsing',
40
38
  wcag412: 'https://www.w3.org/TR/WCAG22/#name-role-value',
41
39
  };
42
40
 
@@ -385,20 +383,22 @@ category summary is clicked %>
385
383
  var buttonAIId = `${ruleInCategory.rule}-${category}-button-AI-${index}`;
386
384
  var errorAIId = `${ruleInCategory.rule}-${category}-error-AI-${index}`;
387
385
 
386
+ const pageItemsCount = page.items.length || page.itemsCount || 0;
387
+ const normalMode = page.itemsCount === undefined;
388
388
  const accordion = createElementFromString(`
389
- <li>
389
+ <li class="${normalMode ? '' : 'no-chevron'}">
390
390
  <div class="accordion mt-2 ${category}">
391
391
  <div class="accordion-item">
392
392
  <div class="accordion-header" id="${accordionId}-title">
393
- <button
394
- aria-label="Page ${index + 1}: ${page.pageTitle}, ${page.items.length} ${page.items.length === 1 ? 'occurrence' : 'occurrences'}" class="accordion-button collapsed"
395
- type="button"
393
+ ${normalMode ? '<button' : '<div'}
394
+ aria-label="Page ${index + 1}: ${page.pageTitle}, ${pageItemsCount} ${pageItemsCount === 1 ? 'occurrence' : 'occurrences'}" class="accordion-button collapsed"
395
+ ${normalMode ? 'type="button"' : ''}
396
396
 
397
397
  >
398
- <span class="sr-only visually-hidden">${page.items.length} ${page.items.length === 1 ? 'occurrence' : 'occurrences'}</span>
398
+ <span class="sr-only visually-hidden">${pageItemsCount} ${pageItemsCount === 1 ? 'occurrence' : 'occurrences'}</span>
399
399
  <div class="me-3">${page.metadata ? page.metadata : page.pageTitle}</div>
400
- <div class="ms-auto counter">${page.items.length}</div>
401
- </button>
400
+ <div class="ms-auto counter">${pageItemsCount}</div>
401
+ ${normalMode ? '</button>' : '</div>'}
402
402
  </div>
403
403
  <div id="${accordionId}-content" class="accordion-collapse collapse" aria-labelledby="${accordionId}-title">
404
404
  <div class="accordion-body p-3">
@@ -414,7 +414,7 @@ category summary is clicked %>
414
414
  }
415
415
  <div class="page-accordion-content-title">
416
416
  <span>${getFormattedCategoryTitle(category)} elements</span>
417
- <span class="page-items-count">${page.items.length}</span>
417
+ <span class="page-items-count">${pageItemsCount}</span>
418
418
  </div>
419
419
  </div>
420
420
  </div>
@@ -423,45 +423,47 @@ category summary is clicked %>
423
423
  </li>
424
424
  `);
425
425
 
426
- accordion.querySelector('button').addEventListener('click', function(event) {
427
- var accordionBody = accordion.querySelector(".accordion-body");
428
-
429
- // So that It does not keep adding
430
- if (!accordionBody.querySelector(".unbulleted-list"))
431
- {
432
- buildExpandedRuleCategoryContentAccordian(accordionId,category,ruleInCategory,page,index);
433
-
434
- this.setAttribute('data-bs-target', '#' + accordionId+"-content");
426
+ if (normalMode) {
427
+ accordion.querySelector('button').addEventListener('click', function(event) {
428
+ var accordionBody = accordion.querySelector(".accordion-body");
429
+
430
+ // So that It does not keep adding
431
+ if (!accordionBody.querySelector(".unbulleted-list"))
432
+ {
433
+ buildExpandedRuleCategoryContentAccordian(accordionId,category,ruleInCategory,page,index);
434
+
435
+ this.setAttribute('data-bs-target', '#' + accordionId+"-content");
436
+
437
+ // Remove the event listener temporarily
438
+ this.removeEventListener('click', arguments.callee);
439
+
440
+ // Programmatically trigger a click on the button to open the accordion
441
+ this.click();
442
+ }
443
+ });
435
444
 
445
+ accordion.querySelector('button').addEventListener('click', function(event) {
446
+ // Set data attributes
447
+ this.setAttribute('data-bs-toggle', 'collapse');
448
+ this.setAttribute('data-bs-target', '#' + accordionId + "-content");
449
+ this.setAttribute('aria-expanded', 'false');
450
+ this.setAttribute('aria-controls', accordionId + "-content");
451
+
452
+ // Initialize the Collapse plugin on the button element
453
+ var collapse = new bootstrap.Collapse(this, {
454
+ toggle: false // Set to true if you want to toggle the collapsed state on initialization
455
+ });
456
+
436
457
  // Remove the event listener temporarily
437
458
  this.removeEventListener('click', arguments.callee);
438
-
459
+
439
460
  // Programmatically trigger a click on the button to open the accordion
440
- this.click();
441
- }
442
- });
443
-
444
- accordion.querySelector('button').addEventListener('click', function(event) {
445
- // Set data attributes
446
- this.setAttribute('data-bs-toggle', 'collapse');
447
- this.setAttribute('data-bs-target', '#' + accordionId + "-content");
448
- this.setAttribute('aria-expanded', 'false');
449
- this.setAttribute('aria-controls', accordionId + "-content");
450
-
451
- // Initialize the Collapse plugin on the button element
452
- var collapse = new bootstrap.Collapse(this, {
453
- toggle: false // Set to true if you want to toggle the collapsed state on initialization
454
- });
455
-
456
- // Remove the event listener temporarily
457
- this.removeEventListener('click', arguments.callee);
458
-
459
- // Programmatically trigger a click on the button to open the accordion
460
- setTimeout(() => { // Delaying to ensure the content is added before triggering the click
461
- this.click();
462
- }, 0);
463
-
464
- })
461
+ setTimeout(() => { // Delaying to ensure the content is added before triggering the click
462
+ this.click();
463
+ }, 0);
464
+
465
+ })
466
+ }
465
467
 
466
468
  if (isCustomFlow) {
467
469
  const customScreenshotElem = accordion.getElementsByClassName(`custom-flow-screenshot`)[0];
@@ -0,0 +1,38 @@
1
+ <%# functions to handle interaction and ui for advancedScanOptionsSummary in scanAbout.ejs.
2
+ component %>
3
+
4
+ <script>
5
+ let optionsToCheck = scanData.advancedScanOptionsSummaryItems;
6
+
7
+ document.querySelectorAll('#advancedScanOptionsSummary li').forEach(liElement => {
8
+ liElement.classList.add('d-none');
9
+ });
10
+
11
+ function toggleAdvanceScanSummary() {
12
+ const chevron = document.getElementById('advancedScanOptionsSummaryTitle');
13
+ const advancedScanOptionsSummary = document.getElementById('advancedScanOptionsSummary');
14
+
15
+ const isHidden = advancedScanOptionsSummary.classList.toggle('d-none');
16
+
17
+ chevron.classList.toggle('chevron-rotated', !isHidden);
18
+
19
+ if (!isHidden) {
20
+ showScanOptions(optionsToCheck);
21
+ }
22
+ }
23
+
24
+ function showScanOptions(options) {
25
+ document.querySelectorAll('#advancedScanOptionsSummary li').forEach(liElement => {
26
+ liElement.classList.add('d-none');
27
+ });
28
+
29
+ for (const key in options) {
30
+ if (options[key] === true) {
31
+ const liElement = document.getElementById(key);
32
+ if (liElement) {
33
+ liElement.classList.remove('d-none');
34
+ }
35
+ }
36
+ }
37
+ }
38
+ </script>
@@ -755,7 +755,8 @@
755
755
  margin: 1.5rem 0 1rem 0;
756
756
  }
757
757
 
758
- button#wcagModalToggle {
758
+ button#wcagModalToggle,
759
+ button#advancedScanOptionsSummaryTitle {
759
760
  background: none;
760
761
  border: 0;
761
762
  padding: 0;
@@ -962,6 +963,26 @@
962
963
  width: 1.125rem;
963
964
  }
964
965
 
966
+ #advancedScanOptionsSummary li > svg {
967
+ margin-left: 2rem;
968
+ }
969
+
970
+ #advancedScanOptionsSummaryTitle::after {
971
+ content: '';
972
+ display: inline-block;
973
+ width: 12px;
974
+ height: 12px;
975
+ background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 16" fill="none"><path d="M1.03847 16C0.833084 16 0.632306 15.9388 0.461529 15.8241C0.290753 15.7095 0.157649 15.5465 0.0790493 15.3558C0.000449621 15.1651 -0.0201154 14.9553 0.0199549 14.7529C0.0600251 14.5505 0.158931 14.3645 0.304165 14.2186L6.49293 7.99975L0.304165 1.78088C0.109639 1.58514 0.000422347 1.31979 0.000518839 1.04315C0.000615331 0.766523 0.110018 0.501248 0.30468 0.30564C0.499341 0.110032 0.763331 9.70251e-05 1.03862 6.41929e-08C1.31392 -9.68968e-05 1.57798 0.109652 1.77278 0.305123L8.69586 7.26187C8.8906 7.45757 9 7.72299 9 7.99975C9 8.2765 8.8906 8.54192 8.69586 8.73763L1.77278 15.6944C1.67646 15.7914 1.562 15.8684 1.43598 15.9208C1.30996 15.9733 1.17487 16.0002 1.03847 16Z" fill="%23006B8C" transform="rotate(90, 4.5, 8)"/></svg>');
976
+ background-size: contain;
977
+ background-repeat: no-repeat;
978
+ transform: scaleY(1);
979
+ margin-left: 0.5rem;
980
+ }
981
+
982
+ #advancedScanOptionsSummaryTitle.chevron-rotated::after {
983
+ transform: scaleY(-1);
984
+ }
985
+
965
986
  #footer {
966
987
  padding: 0.75rem 1rem;
967
988
  box-shadow: 0 -0.25rem 10px #736ccb1a;
@@ -1175,6 +1196,10 @@
1175
1196
  content: none;
1176
1197
  }
1177
1198
 
1199
+ #expandedRuleCategoryContent .no-chevron .accordion-button::before {
1200
+ background-image: none !important;
1201
+ }
1202
+
1178
1203
  #expandedRuleCategoryContent .accordion-button::before {
1179
1204
  content: '';
1180
1205
  width: 0.75rem;
@@ -2,11 +2,7 @@
2
2
  <main aria-label="Report main content" class="d-flex flex-column flex-grow-1">
3
3
  <div class="row m-0 py-4 px-3 d-flex" style="flex-wrap: nowrap">
4
4
  <div id="aboutScanDiv" class="pe-md-2 d-flex" style="flex: 1; flex-basis: 300px">
5
- <div
6
- id="scanabout-compliance-card"
7
- class="card h-100"
8
- style="width: -webkit-fill-available"
9
- >
5
+ <div id="scanabout-compliance-card" class="card h-100" style="width: -webkit-fill-available">
10
6
  <div class="card-body"><%- include("components/summaryScanAbout") %></div>
11
7
  </div>
12
8
  </div>
@@ -16,36 +12,21 @@
16
12
  <h2>Scan results</h2>
17
13
  <div class="d-flex justify-content-between align-items-center">
18
14
  <span class="fw-bold"> WCAG (A & AA) Passes </span>
19
- <span aria-label="Pass percentage" class="ms-2"><%= wcagPassPercentage %>% of automated checks</span>
15
+ <span aria-label="Pass percentage" class="ms-2">
16
+ <%= wcagPassPercentage %>% of automated checks
17
+ </span>
20
18
  </div>
21
19
  <div class="wcag-compliance-passes-bar mb-5 d-flex">
22
- <svg
23
- width="500"
24
- role="none"
25
- height="6"
26
- fill="none"
27
- xmlns="http://www.w3.org/2000/svg"
28
- style="display: flex; width: 100%; position: absolute"
29
- >
30
- <rect
31
- width="100%"
32
- height="10"
33
- rx="3"
34
- fill="#E7ECEE"
35
- style="justify-content: left"
36
- ></rect>
37
- <rect
38
- width="<%= wcagPassPercentage %>%"
39
- height="6"
40
- rx="3"
41
- fill="#9021a6"
42
- style=""
43
- ></rect>
20
+ <svg width="500" role="none" height="6" fill="none" xmlns="http://www.w3.org/2000/svg"
21
+ style="display: flex; width: 100%; position: absolute">
22
+ <rect width="100%" height="10" rx="3" fill="#E7ECEE" style="justify-content: left"></rect>
23
+ <rect width="<%= wcagPassPercentage %>%" height="6" rx="3" fill="#9021a6" style=""></rect>
44
24
  </svg>
45
25
  </div>
46
26
  <ul class="unbulleted-list">
47
- <% Object.keys(items).forEach((category) => { %> <%-
48
- include("components/summaryScanResults", { category: category }) %> <% }) %>
27
+ <% Object.keys(items).forEach((category)=> { %> <%- include("components/summaryScanResults", { category:
28
+ category }) %>
29
+ <% }) %>
49
30
  </ul>
50
31
  </div>
51
32
  </div>
@@ -55,21 +36,13 @@
55
36
  <h2 class="mb-2">Summary of issues:</h2>
56
37
  <p>
57
38
  Only a subset of
58
- <a
59
- href="https://www.w3.org/WAI/WCAG21/quickref/?currentsidebar=%23col_customize&versions=2.1&levels=aaa"
60
- target="_blank"
61
- >WCAG 2.1</a
62
- >
39
+ <a href="https://www.w3.org/WAI/WCAG22/quickref/?versions=2.2" target=" _blank">WCAG 2.2</a>
63
40
  (Conformance Level A & AA) Success Criteria can be automatically checked so
64
- <a
65
- aria-label="Manual testing guide"
66
- href="http://go.gov.sg/oobee-manual-testing"
67
- target="_blank"
68
- >manual testing</a
69
- >
41
+ <a aria-label="Manual testing guide" href="http://go.gov.sg/oobee-manual-testing" target="_blank">manual
42
+ testing</a>
70
43
  is still required. For more details, please refer to the HTML report.
71
44
  </p>
72
45
  </div>
73
46
  <%- include("components/summaryTable") %>
74
47
  </main>
75
- </div>
48
+ </div>
@@ -17,6 +17,7 @@
17
17
  href="data:image/svg+xml,%3Csvg width='48' height='48' viewBox='0 0 48 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M23.5 6C11.7707 6 10 11.1369 10 19.23V28.77C10 36.8631 11.7707 42 23.5 42C35.2293 42 37 36.8631 37 28.77V19.23C37 11.1369 35.2293 6 23.5 6ZM25.4903 14.5985V35.0562H21.5097V12.9438H27.8925L25.4903 14.5985Z' fill='%239021A6'/%3E%3C/svg%3E
18
18
  "
19
19
  />
20
+ <%- include('partials/scripts/decodeUnzipParse') %>
20
21
  <%- include('partials/styles/bootstrap') %> <%- include('partials/styles/highlightjs') %> <%-
21
22
  include('partials/styles/styles') %>
22
23
  </head>
@@ -25,6 +26,7 @@
25
26
  <%- include('partials/header') %> <%- include('partials/main') %> <%-
26
27
  include('partials/scripts/popper') %> <%- include('partials/scripts/bootstrap') %> <%-
27
28
  include('partials/scripts/highlightjs') %> <%- include('partials/scripts/utils') %> <%-
29
+ include('partials/scripts/scanAboutScript') %> <%-
28
30
  include('partials/scripts/categorySelectorDropdownScript') %> <%-
29
31
  include('partials/scripts/categorySummary') %> <%- include('partials/scripts/ruleOffcanvas') %>
30
32
  <%- include('partials/scripts/screenshotLightbox')%>
@@ -99,7 +101,7 @@
99
101
  const scategoryList = document.getElementById('categorySelector');
100
102
 
101
103
  Object.keys(scanItems).forEach(category => {
102
- if (category !== 'passed') {
104
+ if (["mustFix", "goodToFix", "needsReview"].includes(category)) { // skip other keys like pagesScanned, etc
103
105
  const categoryData = scanItems[category];
104
106
  const listItem = document.createElement('div');
105
107
  listItem.className = 'col-md-4 px-2';
@@ -168,12 +170,10 @@
168
170
  spanInfo.id = `${category}ItemsInformation`;
169
171
  spanInfo.className = 'category-information';
170
172
 
171
- if (category !== 'passed' && categoryData.totalItems !== 0) {
173
+ if (categoryData.totalItems !== 0) {
172
174
  spanInfo.textContent = `${categoryData.rules.length} ${categoryData.rules.length === 1 ? 'issue' : 'issues'} / ${categoryData.totalItems} ${categoryData.totalItems === 1 ? 'occurrence' : 'occurrences'}`;
173
- } else if (category !== 'passed' && categoryData.totalItems === 0) {
175
+ } else if (categoryData.totalItems === 0) {
174
176
  spanInfo.textContent = `0 issues`;
175
- } else {
176
- spanInfo.textContent = `${categoryData.totalItems} ${categoryData.totalItems === 1 ? 'occurrence' : 'occurrences'}`;
177
177
  }
178
178
 
179
179
  button.appendChild(divFlex);
@@ -186,7 +186,8 @@
186
186
  const categoryList = document.getElementById('issueTypeListbox');
187
187
 
188
188
  Object.keys(scanItems).forEach(category => {
189
- if (category !== 'passed') {
189
+ if (["mustFix", "goodToFix", "needsReview"].includes(category)) { // skip other keys like pagesScanned, etc
190
+
190
191
  const categoryData = scanItems[category];
191
192
  const rulesLength = categoryData.rules ? categoryData.rules.length : 0;
192
193
 
@@ -205,12 +206,10 @@
205
206
  spanInfo.id = `${category}ItemsInformation`;
206
207
  spanInfo.className = 'category-information';
207
208
 
208
- if (category !== 'passed' && categoryData.totalItems !== 0) {
209
+ if (categoryData.totalItems !== 0) {
209
210
  spanInfo.textContent = `(${categoryData.rules.length} ${categoryData.rules.length === 1 ? 'issue' : 'issues'})`;
210
- } else if (category !== 'passed' && categoryData.totalItems === 0) {
211
+ } else if (categoryData.totalItems === 0) {
211
212
  spanInfo.textContent = `(0 issues)`;
212
- } else {
213
- spanInfo.textContent = `(${categoryData.totalItems} ${categoryData.totalItems === 1 ? 'occurrence' : 'occurrences'})`;
214
213
  }
215
214
 
216
215
  listItem.appendChild(spanTitle);
@@ -407,9 +406,20 @@
407
406
  };
408
407
 
409
408
  document.addEventListener('DOMContentLoaded', () => {
410
- initTooltips();
411
- scanDataHTML();
409
+ scanDataPromise.then(() => {
410
+ console.log("scanData loaded.");
411
+ scanItemsPromise.then(() => {
412
+ console.log("scanItems loaded.");
413
+ initTooltips();
414
+ scanDataHTML();
415
+ }).error(e => {
416
+ console.error("Failed to load scanItems: ", e);
417
+ });
418
+ }).error(e => {
419
+ console.error("Failed to load scanData: ", e);
420
+ });
412
421
  });
422
+
413
423
  </script>
414
424
  <!-- Checks if js runs -->
415
425
  <script>
package/src/utils.ts CHANGED
@@ -191,8 +191,15 @@ export const cleanUp = async pathToDelete => {
191
191
  // });
192
192
 
193
193
  export const getWcagPassPercentage = (wcagViolations: string[]): string => {
194
- const totalChecks = Object.keys(constants.wcagLinks).length;
195
- const passedChecks = totalChecks - wcagViolations.length;
194
+
195
+ // These AAA rules should not be counted as WCAG Pass Percentage only contains A and AA
196
+ const wcagAAA = ['WCAG 1.4.6', 'WCAG 2.2.4', 'WCAG 2.4.9', 'WCAG 3.1.5', 'WCAG 3.2.5'];
197
+
198
+ const filteredWcagLinks = Object.keys(constants.wcagLinks).filter(key => !wcagAAA.includes(key));
199
+ const filteredWcagViolations = wcagViolations.filter(violation => !wcagAAA.includes(violation));
200
+ const totalChecks = filteredWcagLinks.length;
201
+
202
+ const passedChecks = totalChecks - filteredWcagViolations.length;
196
203
  const passPercentage = (passedChecks / totalChecks) * 100;
197
204
 
198
205
  return passPercentage.toFixed(2); // toFixed returns a string, which is correct here
@@ -241,6 +248,7 @@ export const setHeadlessMode = (browser: string, isHeadless: boolean): void => {
241
248
  } else {
242
249
  process.env.CRAWLEE_HEADLESS = '0';
243
250
  }
251
+
244
252
  };
245
253
 
246
254
  export const setThresholdLimits = setWarnLevel => {
@@ -0,0 +1,186 @@
1
+ /**
2
+ * XPath to CSS
3
+ *
4
+ * Utility function for converting XPath expressions to CSS selectors
5
+ *
6
+ * Originally written in Python by [santiycr](https://github.com/santiycr) for
7
+ * [cssify](https://github.com/santiycr/cssify) and ported to JavaScript by
8
+ * [Dither](https://github.com/Dither). Converted to ES2015 and packaged as an npm module by
9
+ * [svenheden](https://github.com/svenheden)
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import { consoleLogger } from "./logs.js";
15
+
16
+ const isValidXPath = expr => (
17
+ typeof expr != 'undefined' &&
18
+ expr.replace(/[\s-_=]/g,'') !== '' &&
19
+ expr.length === expr.replace(/[-_\w:.]+\(\)\s*=|=\s*[-_\w:.]+\(\)|\sor\s|\sand\s|\[(?:[^\/\]]+[\/\[]\/?.+)+\]|starts-with\(|\[.*last\(\)\s*[-\+<>=].+\]|number\(\)|not\(|count\(|text\(|first\(|normalize-space|[^\/]following-sibling|concat\(|descendant::|parent::|self::|child::|/gi,'').length
20
+ );
21
+
22
+ const getValidationRegex = () => {
23
+ let regex =
24
+ "(?P<node>"+
25
+ "("+
26
+ "^id\\([\"\\']?(?P<idvalue>%(value)s)[\"\\']?\\)"+// special case! `id(idValue)`
27
+ "|"+
28
+ "(?P<nav>//?(?:following-sibling::)?)(?P<tag>%(tag)s)" + // `//div`
29
+ "(\\[("+
30
+ "(?P<matched>(?P<mattr>@?%(attribute)s=[\"\\'](?P<mvalue>%(value)s))[\"\\']"+ // `[@id="well"]` supported and `[text()="yes"]` is not
31
+ "|"+
32
+ "(?P<contained>contains\\((?P<cattr>@?%(attribute)s,\\s*[\"\\'](?P<cvalue>%(value)s)[\"\\']\\))"+// `[contains(@id, "bleh")]` supported and `[contains(text(), "some")]` is not
33
+ ")\\])?"+
34
+ "(\\[\\s*(?P<nth>\\d+|last\\(\\s*\\))\\s*\\])?"+
35
+ ")"+
36
+ ")";
37
+
38
+ const subRegexes = {
39
+ "tag": "([a-zA-Z][a-zA-Z0-9:-]*|\\*)",
40
+ "attribute": "[.a-zA-Z_:][-\\w:.]*(\\(\\))?)",
41
+ "value": "\\s*[\\w/:][-/\\w\\s,:;.]*"
42
+ };
43
+
44
+ Object.keys(subRegexes).forEach(key => {
45
+ regex = regex.replace(new RegExp('%\\(' + key + '\\)s', 'gi'), subRegexes[key]);
46
+ });
47
+
48
+ regex = regex.replace(/\?P<node>|\?P<idvalue>|\?P<nav>|\?P<tag>|\?P<matched>|\?P<mattr>|\?P<mvalue>|\?P<contained>|\?P<cattr>|\?P<cvalue>|\?P<nth>/gi, '');
49
+
50
+ return new RegExp(regex, 'gi');
51
+ };
52
+
53
+ const preParseXpath = expr => (
54
+ expr.replace(/contains\s*\(\s*concat\(["']\s+["']\s*,\s*@class\s*,\s*["']\s+["']\)\s*,\s*["']\s+([a-zA-Z0-9-_]+)\s+["']\)/gi, '@class="$1"')
55
+ );
56
+
57
+ function escapeCssIdSelectors(cssSelector) {
58
+ return cssSelector.replace(/#([^ >]+)/g, (match, id) => {
59
+ // Escape special characters in the id part
60
+ return '#' + id.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&');
61
+ });
62
+ }
63
+
64
+ export const xPathToCss = expr => {
65
+ if (!expr) {
66
+ throw new Error('Missing XPath expression');
67
+ }
68
+
69
+ expr = preParseXpath(expr);
70
+
71
+ if (!isValidXPath(expr)) {
72
+ consoleLogger.error(`Invalid or unsupported XPath: ${expr}`);
73
+ // do not throw error so that this function proceeds to convert xpath that it does not support
74
+ // for example, //*[@id="google_ads_iframe_/4654/dweb/imu1/homepage/landingpage/na_0"]/html/body/div[1]/a
75
+ // becomes #google_ads_iframe_/4654/dweb/imu1/homepage/landingpage/na_0 > html > body > div:first-of-type > div > a
76
+ // which is invalid because the slashes in the id selector are not escaped
77
+ // throw new Error('Invalid or unsupported XPath: ' + expr);
78
+ }
79
+
80
+ const xPathArr = expr.split('|');
81
+ const prog = getValidationRegex();
82
+ const cssSelectors = [];
83
+ let xindex = 0;
84
+
85
+ while (xPathArr[xindex]) {
86
+ const css = [];
87
+ let position = 0;
88
+ let nodes;
89
+
90
+ while (nodes = prog.exec(xPathArr[xindex])) {
91
+ let attr;
92
+
93
+ if (!nodes && position === 0) {
94
+ throw new Error('Invalid or unsupported XPath: ' + expr);
95
+ }
96
+
97
+ const match = {
98
+ node: nodes[5],
99
+ idvalue: nodes[12] || nodes[3],
100
+ nav: nodes[4],
101
+ tag: nodes[5],
102
+ matched: nodes[7],
103
+ mattr: nodes[10] || nodes[14],
104
+ mvalue: nodes[12] || nodes[16],
105
+ contained: nodes[13],
106
+ cattr: nodes[14],
107
+ cvalue: nodes[16],
108
+ nth: nodes[18]
109
+ };
110
+
111
+ let nav = '';
112
+
113
+ if (position != 0 && match['nav']) {
114
+ if (~match['nav'].indexOf('following-sibling::')) {
115
+ nav = ' + ';
116
+ } else {
117
+ nav = (match['nav'] == '//') ? ' ' : ' > ';
118
+ }
119
+ }
120
+
121
+ const tag = (match['tag'] === '*') ? '' : (match['tag'] || '');
122
+
123
+ if (match['contained']) {
124
+ if (match['cattr'].indexOf('@') === 0) {
125
+ attr = '[' + match['cattr'].replace(/^@/, '') + '*="' + match['cvalue'] + '"]';
126
+ } else {
127
+ throw new Error('Invalid or unsupported XPath attribute: ' + match['cattr']);
128
+ }
129
+ } else if (match['matched']) {
130
+ switch (match['mattr']) {
131
+ case '@id':
132
+ attr = '#' + match['mvalue'].replace(/^\s+|\s+$/,'').replace(/\s/g, '#');
133
+ break;
134
+ case '@class':
135
+ attr = '.' + match['mvalue'].replace(/^\s+|\s+$/,'').replace(/\s/g, '.');
136
+ break;
137
+ case 'text()':
138
+ case '.':
139
+ throw new Error('Invalid or unsupported XPath attribute: ' + match['mattr']);
140
+ default:
141
+ if (match['mattr'].indexOf('@') !== 0) {
142
+ throw new Error('Invalid or unsupported XPath attribute: ' + match['mattr']);
143
+ }
144
+ if (match['mvalue'].indexOf(' ') !== -1) {
145
+ match['mvalue'] = '\"' + match['mvalue'].replace(/^\s+|\s+$/,'') + '\"';
146
+ }
147
+ attr = '[' + match['mattr'].replace('@', '') + '="' + match['mvalue'] + '"]';
148
+ break;
149
+ }
150
+ } else if (match['idvalue']) {
151
+ attr = '#' + match['idvalue'].replace(/\s/, '#');
152
+ } else {
153
+ attr = '';
154
+ }
155
+
156
+ let nth = '';
157
+
158
+ if (match['nth']) {
159
+ if (match['nth'].indexOf('last') === -1) {
160
+ if (isNaN(parseInt(match['nth'], 10))) {
161
+ throw new Error('Invalid or unsupported XPath attribute: ' + match['nth']);
162
+ }
163
+ nth = parseInt(match['nth'], 10) !== 1 ? ':nth-of-type(' + match['nth'] + ')' : ':first-of-type';
164
+ } else {
165
+ nth = ':last-of-type';
166
+ }
167
+ }
168
+
169
+ css.push(nav + tag + attr + nth);
170
+ position++;
171
+ }
172
+
173
+ const result = css.join('');
174
+
175
+ if (result === '') {
176
+ throw new Error('Invalid or unsupported XPath');
177
+ }
178
+
179
+ cssSelectors.push(result);
180
+ xindex++;
181
+ }
182
+
183
+ // return cssSelectors.join(', ');
184
+ const originalResult = cssSelectors.join(', ');
185
+ return escapeCssIdSelectors(originalResult);
186
+ };
Binary file
@@ -1,3 +0,0 @@
1
- declare module 'xpath-to-css' {
2
- export default function xPathToCss(xPath: string): string;
3
- }