@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.
- package/.github/workflows/docker-test.yml +1 -1
- package/DETAILS.md +40 -25
- package/Dockerfile +41 -47
- package/LICENSE-3RD-PARTY-REPORT.txt +448 -0
- package/LICENSE-3RD-PARTY.txt +19913 -0
- package/README.md +26 -0
- package/__mocks__/mock-report.html +1503 -1360
- package/package.json +9 -5
- package/scripts/decodeUnzipParse.js +29 -0
- package/scripts/install_oobee_dependencies.command +2 -2
- package/scripts/install_oobee_dependencies.ps1 +3 -3
- package/src/cli.ts +9 -7
- package/src/combine.ts +13 -5
- package/src/constants/cliFunctions.ts +38 -1
- package/src/constants/common.ts +31 -5
- package/src/constants/constants.ts +28 -26
- package/src/constants/questions.ts +4 -1
- package/src/crawlers/commonCrawlerFunc.ts +114 -152
- package/src/crawlers/crawlDomain.ts +25 -32
- package/src/crawlers/crawlIntelligentSitemap.ts +7 -1
- package/src/crawlers/crawlLocalFile.ts +1 -1
- package/src/crawlers/crawlSitemap.ts +1 -1
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +546 -472
- package/src/crawlers/customAxeFunctions.ts +1 -1
- package/src/index.ts +2 -2
- package/src/mergeAxeResults.ts +590 -214
- package/src/screenshotFunc/pdfScreenshotFunc.ts +3 -3
- package/src/static/ejs/partials/components/scanAbout.ejs +65 -0
- package/src/static/ejs/partials/components/wcagCompliance.ejs +10 -29
- package/src/static/ejs/partials/footer.ejs +10 -13
- package/src/static/ejs/partials/scripts/categorySummary.ejs +2 -2
- package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
- package/src/static/ejs/partials/scripts/reportSearch.ejs +1 -0
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +54 -52
- package/src/static/ejs/partials/scripts/scanAboutScript.ejs +38 -0
- package/src/static/ejs/partials/styles/styles.ejs +26 -1
- package/src/static/ejs/partials/summaryMain.ejs +15 -42
- package/src/static/ejs/report.ejs +22 -12
- package/src/utils.ts +10 -2
- package/src/xPathToCss.ts +186 -0
- package/a11y-scan-results.zip +0 -0
- 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/
|
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/
|
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/
|
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}, ${
|
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">${
|
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">${
|
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">${
|
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
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
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
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
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"
|
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
|
-
|
24
|
-
|
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
|
-
|
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
|
-
|
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 (
|
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 (
|
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 (
|
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 (
|
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 (
|
209
|
+
if (categoryData.totalItems !== 0) {
|
209
210
|
spanInfo.textContent = `(${categoryData.rules.length} ${categoryData.rules.length === 1 ? 'issue' : 'issues'})`;
|
210
|
-
} else if (
|
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
|
-
|
411
|
-
|
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
|
-
|
195
|
-
|
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
|
+
};
|
package/a11y-scan-results.zip
DELETED
Binary file
|