@govtechsg/oobee 0.10.75 → 0.10.77

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 (134) hide show
  1. package/.github/workflows/publish.yml +8 -1
  2. package/INTEGRATION.md +7 -3
  3. package/dist/cli.js +252 -0
  4. package/dist/combine.js +221 -0
  5. package/dist/constants/cliFunctions.js +306 -0
  6. package/dist/constants/common.js +1669 -0
  7. package/dist/constants/constants.js +913 -0
  8. package/dist/constants/errorMeta.json +319 -0
  9. package/dist/constants/itemTypeDescription.js +7 -0
  10. package/dist/constants/oobeeAi.js +121 -0
  11. package/dist/constants/questions.js +151 -0
  12. package/dist/constants/sampleData.js +176 -0
  13. package/dist/crawlers/commonCrawlerFunc.js +428 -0
  14. package/dist/crawlers/crawlDomain.js +613 -0
  15. package/dist/crawlers/crawlIntelligentSitemap.js +135 -0
  16. package/dist/crawlers/crawlLocalFile.js +151 -0
  17. package/dist/crawlers/crawlSitemap.js +303 -0
  18. package/dist/crawlers/custom/escapeCssSelector.js +10 -0
  19. package/dist/crawlers/custom/evaluateAltText.js +11 -0
  20. package/dist/crawlers/custom/extractAndGradeText.js +44 -0
  21. package/dist/crawlers/custom/extractText.js +27 -0
  22. package/dist/crawlers/custom/findElementByCssSelector.js +36 -0
  23. package/dist/crawlers/custom/flagUnlabelledClickableElements.js +963 -0
  24. package/dist/crawlers/custom/framesCheck.js +37 -0
  25. package/dist/crawlers/custom/getAxeConfiguration.js +111 -0
  26. package/dist/crawlers/custom/gradeReadability.js +23 -0
  27. package/dist/crawlers/custom/utils.js +1024 -0
  28. package/dist/crawlers/custom/xPathToCss.js +147 -0
  29. package/dist/crawlers/guards/urlGuard.js +71 -0
  30. package/dist/crawlers/pdfScanFunc.js +276 -0
  31. package/dist/crawlers/runCustom.js +89 -0
  32. package/dist/exclusions.txt +7 -0
  33. package/dist/generateHtmlReport.js +144 -0
  34. package/dist/index.js +62 -0
  35. package/dist/logs.js +84 -0
  36. package/dist/mergeAxeResults.js +1571 -0
  37. package/dist/npmIndex.js +429 -0
  38. package/dist/proxyService.js +360 -0
  39. package/dist/runGenerateJustHtmlReport.js +16 -0
  40. package/dist/screenshotFunc/htmlScreenshotFunc.js +355 -0
  41. package/dist/screenshotFunc/pdfScreenshotFunc.js +645 -0
  42. package/dist/services/s3Uploader.js +127 -0
  43. package/dist/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
  44. package/dist/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
  45. package/dist/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
  46. package/dist/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
  47. package/dist/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
  48. package/dist/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
  49. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
  50. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
  51. package/dist/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
  52. package/dist/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
  53. package/dist/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
  54. package/dist/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
  55. package/dist/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
  56. package/dist/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
  57. package/dist/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
  58. package/dist/static/ejs/partials/components/summaryScanResults.ejs +16 -0
  59. package/dist/static/ejs/partials/components/summaryTable.ejs +20 -0
  60. package/dist/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
  61. package/dist/static/ejs/partials/components/topTen.ejs +6 -0
  62. package/dist/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
  63. package/dist/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
  64. package/dist/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
  65. package/dist/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
  66. package/dist/static/ejs/partials/footer.ejs +24 -0
  67. package/dist/static/ejs/partials/header.ejs +14 -0
  68. package/dist/static/ejs/partials/main.ejs +29 -0
  69. package/dist/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
  70. package/dist/static/ejs/partials/scripts/bootstrap.ejs +8 -0
  71. package/dist/static/ejs/partials/scripts/categorySummary.ejs +141 -0
  72. package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  73. package/dist/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
  74. package/dist/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
  75. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
  76. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
  77. package/dist/static/ejs/partials/scripts/highlightjs.ejs +335 -0
  78. package/dist/static/ejs/partials/scripts/popper.ejs +7 -0
  79. package/dist/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
  80. package/dist/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
  81. package/dist/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
  82. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +957 -0
  83. package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +353 -0
  84. package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
  85. package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
  86. package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
  87. package/dist/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
  88. package/dist/static/ejs/partials/scripts/screenshotLightbox.ejs +75 -0
  89. package/dist/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
  90. package/dist/static/ejs/partials/scripts/summaryTable.ejs +78 -0
  91. package/dist/static/ejs/partials/scripts/topTen.ejs +61 -0
  92. package/dist/static/ejs/partials/scripts/utils.ejs +453 -0
  93. package/dist/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
  94. package/dist/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
  95. package/dist/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
  96. package/dist/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
  97. package/dist/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
  98. package/dist/static/ejs/partials/styles/bootstrap.ejs +12391 -0
  99. package/dist/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
  100. package/dist/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
  101. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
  102. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
  103. package/dist/static/ejs/partials/styles/header.ejs +7 -0
  104. package/dist/static/ejs/partials/styles/highlightjs.ejs +54 -0
  105. package/dist/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
  106. package/dist/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
  107. package/dist/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
  108. package/dist/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
  109. package/dist/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
  110. package/dist/static/ejs/partials/styles/styles.ejs +1607 -0
  111. package/dist/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
  112. package/dist/static/ejs/partials/styles/topTenCard.ejs +44 -0
  113. package/dist/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
  114. package/dist/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
  115. package/dist/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
  116. package/dist/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
  117. package/dist/static/ejs/partials/summaryHeader.ejs +70 -0
  118. package/dist/static/ejs/partials/summaryMain.ejs +49 -0
  119. package/dist/static/ejs/report.ejs +226 -0
  120. package/dist/static/ejs/summary.ejs +47 -0
  121. package/dist/types/types.js +1 -0
  122. package/dist/utils.js +1070 -0
  123. package/examples/oobee-cypress-integration-js/cypress/support/e2e.js +36 -6
  124. package/examples/oobee-cypress-integration-js/cypress.config.js +45 -1
  125. package/examples/oobee-cypress-integration-ts/cypress.config.ts +47 -1
  126. package/examples/oobee-cypress-integration-ts/src/cypress/support/e2e.ts +36 -6
  127. package/examples/oobee-playwright-integration-js/oobee-playwright-demo.js +2 -1
  128. package/examples/oobee-playwright-integration-ts/src/oobee-playwright-demo.ts +2 -1
  129. package/package.json +9 -3
  130. package/src/constants/common.ts +2 -2
  131. package/src/constants/constants.ts +3 -1
  132. package/src/crawlers/crawlDomain.ts +1 -0
  133. package/src/crawlers/runCustom.ts +0 -1
  134. package/src/npmIndex.ts +42 -24
@@ -0,0 +1,16 @@
1
+ <div id="wcag-compliance-card" class="wcag-card">
2
+ <div class="wcag-row">
3
+ <div>
4
+ <h2 class="wcag-title">WCAG Score</h2>
5
+
6
+ <p class="wcag-sub">Based on Automated Checks</p>
7
+
8
+ <p id="wcagStatus" class="wcag-status" hidden>
9
+ This website does not meet the minimum accessibility standards.
10
+ </p>
11
+ </div>
12
+
13
+ <div><%- include('WcagGaugeBar') %></div>
14
+ </div>
15
+ <div><%- include('FailedCriteria') %></div>
16
+ </div>
@@ -0,0 +1,16 @@
1
+ <figure id="wcagGauge"
2
+ class="wcag-gauge"
3
+ role="img"
4
+ aria-label="WCAG A & AA automated checks passed">
5
+ <svg viewBox="0 0 100 60" aria-hidden="true" focusable="false">
6
+ <path class="gauge-track" d="M10,50 A40,40 0 0 1 90,50" />
7
+ <path class="gauge-fill" id="gaugeFill" d="M10,50 A40,40 0 0 1 90,50" />
8
+ </svg>
9
+
10
+ <figcaption class="gauge-center">
11
+ <span class="gauge-number">
12
+ <span id="gaugeValueNumber" class="gauge-value-number">0</span>/<span id="gaugeValueTotal">0</span>
13
+ </span>
14
+ </figcaption>
15
+ <div class="gauge-caption">Target to meet: <span id="gaugeTarget">20</span></div>
16
+ </figure>
@@ -0,0 +1,18 @@
1
+ <div id="wcagCoverage" class="my-3">
2
+ <h5 class="fw-semibold mb-2">
3
+ <span id="wcagAALabelCount">20</span> (A &amp; AA) WCAG Success Criteria
4
+ </h5>
5
+ <div class="wcag-box">
6
+ <ul id="wcagLinksListAA" class="wcag-grid list-unstyled m-0">
7
+ <!-- dynamically populated -->
8
+ </ul>
9
+ </div>
10
+ <h5 class="fw-semibold mt-4 mb-2">
11
+ <span id="wcagAAALabelCount">6</span> (AAA) WCAG Success Criteria
12
+ </h5>
13
+ <div class="wcag-box">
14
+ <ul id="wcagLinksListAAA" class="wcag-grid list-unstyled m-0">
15
+ <!-- dynamically populated -->
16
+ </ul>
17
+ </div>
18
+ </div>
@@ -0,0 +1,24 @@
1
+ <footer aria-label="Report footer" id="footer" class="card w-100">
2
+ <div class="row mx-0">
3
+ <div class="col-sm-6 text-sm-start">
4
+ <%
5
+ const encodedUrlScanned = encodeURIComponent(urlScanned);
6
+ const encodedVersionNumber = encodeURIComponent(oobeeAppVersion);
7
+
8
+ // Use %0A for line breaks in the body
9
+ const mailtoAddress = `oobee@wogaa.gov.sg`;
10
+ const mailtoSubject = encodeURIComponent(`Support Request - Oobee Version ${oobeeAppVersion}`);
11
+ const mailtoBody = encodeURIComponent(`URL Scanned: ${urlScanned}\nVersion: ${oobeeAppVersion}`);
12
+ const feedbackEmail = `mailto:${mailtoAddress}?subject=${mailtoSubject}&body=${mailtoBody}`;
13
+ %>
14
+
15
+ <a href="<%=feedbackEmail%>" aria-label="Send feedback to <%=mailtoAddress%>" target="_blank">Help us improve</a>
16
+ <hr class="d-sm-none" />
17
+ </div>
18
+ <div class="col-sm-6 text-sm-end">
19
+ Created by
20
+ <a href="https://go.gov.sg/a11y" target="_blank">GovTech Accessibility Enabling Team</a> |
21
+ <a href="https://go.gov.sg/oobee-report-third-party-licenses" target="_blank">Third-Party Licenses</a>
22
+ </div>
23
+ </div>
24
+ </footer>
@@ -0,0 +1,14 @@
1
+ <header class="w-100">
2
+ <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path
4
+ d="M48 24C48 37.2548 37.2548 48 24 48C10.7452 48 0 37.2548 0 24C0 10.7452 10.7452 0 24 0C37.2548 0 48 10.7452 48 24Z"
5
+ fill="white"
6
+ />
7
+ <path
8
+ d="M24 9C13.5686 9 12 13.2831 12 20.0321V27.9679C12 34.7169 13.5686 39 24 39C34.4314 39 36 34.7169 36 27.9679V20.0136C36 13.2645 34.4314 9 24 9ZM25.7647 16.157V33.2151H22.2353V14.7849H27.902L25.7647 16.157Z"
9
+ fill="#5735DF"
10
+ />
11
+ </svg>
12
+
13
+ <h1 class="header-title">Accessibility Report</h1>
14
+ </header>
@@ -0,0 +1,29 @@
1
+ <div class="d-flex flex-grow-1 align-items-stretch main-container">
2
+ <%- include("components/screenshotLightbox") %>
3
+ <main class="d-flex flex-grow-1" style="min-width: 0;">
4
+ <section class="w-100 m-4" style="max-width: calc(100% - 3rem); box-sizing: border-box;">
5
+ <%- include("components/header/SiteInfo") %>
6
+ <div class="compliance-summary gap-4 mt-4">
7
+ <div class="col card rounded"><%- include("components/wcagCompliance/WcagCompliance") %></div>
8
+ <div class="col card rounded"><%- include("components/topTen") %></div>
9
+ </div>
10
+ <%- include("components/prioritiseIssues/PrioritiseIssues") %>
11
+ <!-- jsOffMessage -->
12
+ <p id="jsOffMessage" class="mx-3">
13
+ Please open this file in a browser to experience the report's full functionality.
14
+ </p>
15
+ <%- include("components/allIssues/AllIssues") %>
16
+ <div id="jsOn" class="d-none">
17
+ <%- include("components/ruleModal/ruleOffcanvas") %>
18
+ <p id="containsAISuggestions" class="d-none">Tooltip: Contains AI suggestions.</p>
19
+ <p id="mustFixAriaDescription" class="d-none">Tooltip: <%= items.mustFix.description %></p>
20
+ <p id="goodToFixAriaDescription" class="d-none">
21
+ Tooltip: <%= items.goodToFix.description %>
22
+ </p>
23
+ <p id="passedAriaDescription" class="d-none">Tooltip: <%= items.passed.description %></p>
24
+ </div>
25
+ </section>
26
+ </main>
27
+ </div>
28
+ <%- include("footer") %>
29
+ <%- include("components/header/aboutScanModal/AboutScanModal") %>
@@ -0,0 +1,376 @@
1
+ <script>
2
+ (function initAllIssues() {
3
+ let allIssues = [];
4
+ let filteredIssues = [];
5
+ let currentSort = { column: 'occurrence', direction: 'desc' };
6
+ let currentCategory = 'mustFix';
7
+ const a11yRuleShortDescriptionMap = scanData?.a11yRuleShortDescriptionMap;
8
+ const wcagCriteriaLabels = scanData?.wcagCriteriaLabels;
9
+ const disabilityBadgesMap = scanData?.disabilityBadgesMap;
10
+
11
+ window.addEventListener('DOMContentLoaded', () => {
12
+ initializeIssues();
13
+ populateCriteriaFilter();
14
+ renderTable();
15
+ updateTotalCount();
16
+ initializeSortableHeaders();
17
+
18
+ document.getElementById('severityFilter').addEventListener('change', handleFilterChange);
19
+ document.getElementById('criteriaFilter').addEventListener('change', handleFilterChange);
20
+ document.getElementById('disabilityFilter').addEventListener('change', handleFilterChange);
21
+ document.getElementById('issuesSearchInput').addEventListener('input', handleSearch);
22
+ });
23
+
24
+ function initializeIssues() {
25
+ const categories = ['mustFix', 'goodToFix', 'needsReview'];
26
+ allIssues = [];
27
+
28
+ categories.forEach(category => {
29
+ if (scanItems[category] && scanItems[category].rules) {
30
+ scanItems[category].rules.forEach(rule => {
31
+ allIssues.push({
32
+ category,
33
+ ruleId: rule.rule,
34
+ description: a11yRuleShortDescriptionMap[rule.rule] || rule.description,
35
+ totalItems: rule.totalItems,
36
+ conformance: rule.conformance || [],
37
+ pagesAffected: rule.pagesAffected || [],
38
+ axeImpact: rule.axeImpact || '',
39
+ helpUrl: rule.helpUrl || '',
40
+ disabilities: disabilityBadgesMap[rule.rule] || [],
41
+ });
42
+ });
43
+ }
44
+ });
45
+
46
+ filteredIssues = [...allIssues];
47
+ }
48
+
49
+ function initializeSortableHeaders() {
50
+ const headers = [
51
+ { element: document.querySelector('.issues-table th:nth-child(1)'), column: 'severity' },
52
+ { element: document.querySelector('.issues-table th:nth-child(2)'), column: 'issueName' },
53
+ { element: document.querySelector('.issues-table th:nth-child(3)'), column: 'occurrence' }
54
+ ];
55
+
56
+ headers.forEach(({ element, column }) => {
57
+ if (!element) return;
58
+
59
+ const sortHeader = function() {
60
+ window.sortTable(column);
61
+ };
62
+
63
+ // Click handler
64
+ element.addEventListener('click', sortHeader);
65
+
66
+ // Keyboard handler
67
+ element.addEventListener('keydown', function(event) {
68
+ if (event.key === 'Enter' || event.key === ' ') {
69
+ event.preventDefault();
70
+ sortHeader();
71
+ }
72
+ });
73
+ });
74
+ }
75
+
76
+ function populateCriteriaFilter() {
77
+ const criteriaFilter = document.getElementById('criteriaFilter');
78
+ const criteriaSet = new Set();
79
+
80
+ allIssues.forEach(issue => {
81
+ if (issue.conformance) {
82
+ issue.conformance.forEach(conf => {
83
+ if (conf.startsWith('wcag')) {
84
+ const formatted = formatWcagId(conf);
85
+ if (wcagCriteriaLabels[formatted]) criteriaSet.add(formatted);
86
+ }
87
+ });
88
+ }
89
+ });
90
+
91
+ const sortedCriteria = Array.from(criteriaSet).sort((a, b) => {
92
+ const aNum = a.replace(/[^\d]/g, '');
93
+ const bNum = b.replace(/[^\d]/g, '');
94
+ return aNum.localeCompare(bNum, undefined, { numeric: true });
95
+ });
96
+
97
+ sortedCriteria.forEach(criteria => {
98
+ const option = document.createElement('option');
99
+ option.value = criteria;
100
+ option.textContent = criteria;
101
+ criteriaFilter.appendChild(option);
102
+ });
103
+ }
104
+
105
+ window.filterByCategory = function (category) {
106
+ currentCategory = category;
107
+
108
+ // Update active badge
109
+ document.querySelectorAll('.category-badge').forEach(badge => {
110
+ badge.classList.remove('active');
111
+ });
112
+ document.getElementById(category + 'Badge').classList.add('active');
113
+
114
+ // Update severity filter
115
+ const severityMap = {
116
+ mustFix: 'mustFix',
117
+ goodToFix: 'goodToFix',
118
+ needsReview: 'needsReview',
119
+ };
120
+ document.getElementById('severityFilter').value = severityMap[category];
121
+
122
+ handleFilterChange();
123
+ };
124
+
125
+ function handleFilterChange() {
126
+ const severityFilter = document.getElementById('severityFilter').value;
127
+ const criteriaFilter = document.getElementById('criteriaFilter').value;
128
+ const disabilityFilter = document.getElementById('disabilityFilter').value;
129
+ const searchValue = document.getElementById('issuesSearchInput').value.toLowerCase();
130
+
131
+ filteredIssues = allIssues.filter(issue => {
132
+ // Severity filter
133
+ if (severityFilter !== 'all' && issue.category !== severityFilter) {
134
+ return false;
135
+ }
136
+
137
+ // Criteria filter
138
+ if (criteriaFilter !== 'all') {
139
+ const hasCriteria =
140
+ issue.conformance &&
141
+ issue.conformance.some(conf => {
142
+ return formatWcagId(conf) === criteriaFilter;
143
+ });
144
+ if (!hasCriteria) return false;
145
+ }
146
+
147
+ // Disability filter
148
+ if (disabilityFilter !== 'all') {
149
+ const disabilities = disabilityBadgesMap[issue.ruleId] || [];
150
+ if (!disabilities.includes(disabilityFilter)) return false;
151
+ }
152
+
153
+ // Search filter
154
+ if (searchValue && !issue.description.toLowerCase().includes(searchValue)) {
155
+ return false;
156
+ }
157
+
158
+ return true;
159
+ });
160
+
161
+ renderTable();
162
+ updateTotalCount();
163
+ }
164
+
165
+ function handleSearch(event) {
166
+ handleFilterChange();
167
+ }
168
+
169
+ window.sortTable = function (column) {
170
+ if (currentSort.column === column) {
171
+ currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
172
+ } else {
173
+ currentSort.column = column;
174
+ currentSort.direction = 'desc';
175
+ }
176
+
177
+ // Update sort indicators
178
+ document.querySelectorAll('.issues-table th[aria-sort]').forEach(th => {
179
+ th.setAttribute('aria-sort', 'none');
180
+ const sortIcon = th.querySelector('.sort-icon');
181
+ if (sortIcon) {
182
+ sortIcon.classList.remove('active');
183
+ const paths = th.querySelectorAll('.sort-icon path');
184
+ if (paths.length === 2) {
185
+ paths[0].setAttribute('opacity', '0.3');
186
+ paths[1].setAttribute('opacity', '1');
187
+ }
188
+ }
189
+ });
190
+
191
+ const columnIndex = {
192
+ severity: 1,
193
+ issueName: 2,
194
+ occurrence: 3
195
+ };
196
+
197
+ const sortedTh = document.querySelector(`.issues-table th:nth-child(${columnIndex[column]})`);
198
+ if (sortedTh) {
199
+ sortedTh.setAttribute(
200
+ 'aria-sort',
201
+ currentSort.direction === 'asc' ? 'ascending' : 'descending',
202
+ );
203
+ const sortIcon = sortedTh.querySelector('.sort-icon');
204
+ if (sortIcon) {
205
+ sortIcon.classList.add('active');
206
+
207
+ const paths = sortedTh.querySelectorAll('.sort-icon path');
208
+ if (paths.length === 2) {
209
+ if (currentSort.direction === 'asc') {
210
+ paths[0].setAttribute('opacity', '1');
211
+ paths[1].setAttribute('opacity', '0.3');
212
+ } else {
213
+ paths[0].setAttribute('opacity', '0.3');
214
+ paths[1].setAttribute('opacity', '1');
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ sortIssues();
221
+ renderTable();
222
+ };
223
+
224
+ function sortIssues() {
225
+ filteredIssues.sort((a, b) => {
226
+ let aValue, bValue;
227
+
228
+ switch (currentSort.column) {
229
+ case 'severity':
230
+ const severityOrder = { mustFix: 1, goodToFix: 2, needsReview: 3 };
231
+ aValue = severityOrder[a.category];
232
+ bValue = severityOrder[b.category];
233
+ break;
234
+ case 'issueName':
235
+ aValue = a.description.toLowerCase();
236
+ bValue = b.description.toLowerCase();
237
+ break;
238
+ case 'occurrence':
239
+ aValue = a.totalItems;
240
+ bValue = b.totalItems;
241
+ break;
242
+ }
243
+
244
+ if (aValue < bValue) return currentSort.direction === 'asc' ? -1 : 1;
245
+ if (aValue > bValue) return currentSort.direction === 'asc' ? 1 : -1;
246
+ return 0;
247
+ });
248
+ }
249
+
250
+ function renderTable() {
251
+ const tbody = document.getElementById('issuesTableBody');
252
+ const emptyState = document.getElementById('emptyState');
253
+ const table = document.querySelector('.issues-table');
254
+
255
+ if (filteredIssues.length === 0) {
256
+ table.style.display = 'none';
257
+ emptyState.style.display = 'block';
258
+ return;
259
+ }
260
+
261
+ table.style.display = 'table';
262
+ emptyState.style.display = 'none';
263
+
264
+ tbody.innerHTML = filteredIssues
265
+ .map(issue => {
266
+ const severityLabel = {
267
+ mustFix: 'Must Fix',
268
+ goodToFix: 'Good to Fix',
269
+ needsReview: 'Manual Test',
270
+ }[issue.category];
271
+
272
+ const severityClass = issue.category.replace(/([A-Z])/g, '-$1').toLowerCase();
273
+
274
+ const conformanceBadges = issue.conformance
275
+ .filter(c => c.startsWith('wcag'))
276
+ .slice(0, 3)
277
+ .map(conf => {
278
+ const formatted = formatWcagId(conf);
279
+ if (wcagCriteriaLabels[formatted]) {
280
+ return `<span class="conformance-badge" title="${formatted}">${formatted}</span>`;
281
+ }
282
+ })
283
+ .join('');
284
+
285
+ const disabilitiesBadges = issue.disabilities
286
+ .map(disability => {
287
+ return `<span class="conformance-badge" title="${disability}">${disability} Disability</span>`;
288
+ })
289
+ .join('');
290
+
291
+ return `
292
+ <tr class="issue-row" data-category="${issue.category}" data-rule-id="${issue.ruleId}" role="button" tabindex="0" aria-label="${issue.description}, ${severityLabel}, ${issue.totalItems} occurrences">
293
+ <td>
294
+ <span class="category-badge ${severityClass}">${severityLabel}</span>
295
+ </td>
296
+ <td>
297
+ <div class="issue-name-cell">
298
+ <div class="issue-description">${htmlEscapeString(issue.description)}</div>
299
+ <div class="issue-conformance-badges">
300
+ ${conformanceBadges}${disabilitiesBadges}
301
+ </div>
302
+ </div>
303
+ </td>
304
+ <td>
305
+ <span class="occurrence-count">${issue.totalItems} occ.</span>
306
+ </td>
307
+ <td>
308
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
309
+ <g clip-path="url(#clip0_263_236)">
310
+ <path d="M8.58984 16.59L13.1698 12L8.58984 7.41L9.99984 6L15.9998 12L9.99984 18L8.58984 16.59Z" fill="#5735DF"/>
311
+ </g>
312
+ <defs>
313
+ <clipPath id="clip0_263_236">
314
+ <rect width="24" height="24" fill="white"/>
315
+ </clipPath>
316
+ </defs>
317
+ </svg>
318
+ </td>
319
+ </tr>
320
+ `;
321
+ })
322
+ .join('');
323
+
324
+ // Add event listeners after rendering
325
+ document.querySelectorAll('.issue-row').forEach(row => {
326
+ const openIssue = function() {
327
+ const category = row.getAttribute('data-category');
328
+ const ruleId = row.getAttribute('data-rule-id');
329
+ const issue = filteredIssues.find(i => i.ruleId === ruleId && i.category === category);
330
+
331
+ if (issue) {
332
+ window.openIssueDetails(category, issue);
333
+ }
334
+ };
335
+
336
+ // Click handler
337
+ row.addEventListener('click', openIssue);
338
+
339
+ // Keyboard handler
340
+ row.addEventListener('keydown', function(event) {
341
+ if (event.key === 'Enter' || event.key === ' ') {
342
+ event.preventDefault();
343
+ openIssue();
344
+ }
345
+ });
346
+ });
347
+ }
348
+
349
+ function updateTotalCount() {
350
+ const countElement = document.getElementById('totalIssuesCount');
351
+ const totalCount = filteredIssues.length;
352
+ const allCount = allIssues.length;
353
+
354
+ if (totalCount === allCount) {
355
+ countElement.textContent = `Total ${totalCount} issues`;
356
+ } else {
357
+ countElement.textContent = `Showing ${totalCount} issues`;
358
+ }
359
+ }
360
+
361
+ window.openIssueDetails = function (category, issueData) {
362
+ const issue = typeof issueData === 'string' ? JSON.parse(issueData) : issueData;
363
+ const ruleData = scanItems[category]?.rules?.find(r => r.rule === issue.ruleId);
364
+
365
+ if (ruleData && typeof expandRule === 'function') {
366
+ expandRule(category, ruleData);
367
+
368
+ const modalElement = document.getElementById('expandedRule');
369
+ if (modalElement) {
370
+ const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
371
+ modal.show();
372
+ }
373
+ }
374
+ };
375
+ })();
376
+ </script>