@govtechsg/oobee 0.10.69 → 0.10.72

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 (94) hide show
  1. package/DETAILS.md +0 -1
  2. package/README.md +13 -1
  3. package/S3_UPLOAD_README.md +172 -0
  4. package/dev/runGenerateJustHtmlReport.ts +25 -0
  5. package/package.json +4 -2
  6. package/src/combine.ts +71 -14
  7. package/src/constants/common.ts +89 -91
  8. package/src/constants/constants.ts +535 -60
  9. package/src/crawlers/crawlDomain.ts +313 -305
  10. package/src/crawlers/crawlIntelligentSitemap.ts +24 -18
  11. package/src/crawlers/crawlLocalFile.ts +31 -25
  12. package/src/crawlers/crawlSitemap.ts +264 -253
  13. package/src/crawlers/custom/utils.ts +809 -119
  14. package/src/crawlers/guards/urlGuard.ts +77 -0
  15. package/src/crawlers/runCustom.ts +32 -4
  16. package/src/generateHtmlReport.ts +224 -0
  17. package/src/mergeAxeResults.ts +94 -44
  18. package/src/runGenerateJustHtmlReport.ts +20 -0
  19. package/src/services/s3Uploader.ts +184 -0
  20. package/src/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
  21. package/src/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
  22. package/src/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
  23. package/src/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
  24. package/src/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
  25. package/src/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
  26. package/src/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
  27. package/src/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
  28. package/src/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
  29. package/src/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
  30. package/src/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
  31. package/src/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
  32. package/src/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
  33. package/src/static/ejs/partials/components/{topFive.ejs → topTen.ejs} +2 -2
  34. package/src/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
  35. package/src/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
  36. package/src/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
  37. package/src/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
  38. package/src/static/ejs/partials/footer.ejs +1 -1
  39. package/src/static/ejs/partials/header.ejs +7 -223
  40. package/src/static/ejs/partials/main.ejs +12 -23
  41. package/src/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
  42. package/src/static/ejs/partials/scripts/categorySummary.ejs +1 -1
  43. package/src/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
  44. package/src/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
  45. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
  46. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
  47. package/src/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
  48. package/src/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
  49. package/src/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
  50. package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +949 -0
  51. package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +352 -0
  52. package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
  53. package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
  54. package/src/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
  55. package/src/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
  56. package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +61 -57
  57. package/src/static/ejs/partials/scripts/topTen.ejs +61 -0
  58. package/src/static/ejs/partials/scripts/utils.ejs +15 -0
  59. package/src/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
  60. package/src/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
  61. package/src/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
  62. package/src/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
  63. package/src/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
  64. package/src/static/ejs/partials/styles/bootstrap.ejs +17 -1
  65. package/src/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
  66. package/src/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
  67. package/src/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
  68. package/src/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
  69. package/src/static/ejs/partials/styles/header.ejs +7 -0
  70. package/src/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
  71. package/src/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
  72. package/src/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
  73. package/src/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
  74. package/src/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
  75. package/src/static/ejs/partials/styles/styles.ejs +198 -470
  76. package/src/static/ejs/partials/styles/topTenCard.ejs +44 -0
  77. package/src/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
  78. package/src/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
  79. package/src/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
  80. package/src/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
  81. package/src/static/ejs/report.ejs +42 -259
  82. package/src/static/ejs/summary.ejs +1 -1
  83. package/src/utils.ts +30 -0
  84. package/src/static/ejs/partials/components/categorySelector.ejs +0 -4
  85. package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +0 -57
  86. package/src/static/ejs/partials/components/pagesScannedModal.ejs +0 -70
  87. package/src/static/ejs/partials/components/reportSearch.ejs +0 -47
  88. package/src/static/ejs/partials/components/ruleOffcanvas.ejs +0 -105
  89. package/src/static/ejs/partials/components/scanAbout.ejs +0 -328
  90. package/src/static/ejs/partials/components/wcagCompliance.ejs +0 -52
  91. package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +0 -190
  92. package/src/static/ejs/partials/scripts/reportSearch.ejs +0 -287
  93. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +0 -804
  94. package/src/static/ejs/partials/scripts/scanAboutScript.ejs +0 -38
@@ -0,0 +1,306 @@
1
+ <%- include('./utilities') %> <%- include('./itemCardRenderer') %> <%-
2
+ include('./pageAccordionBuilder') %> <%- include('./constants') %>
3
+
4
+ <script>
5
+ document.addEventListener('DOMContentLoaded', function () {
6
+ const modal = document.getElementById('expandedRule');
7
+ if (!modal) return;
8
+
9
+ modal.addEventListener('hidden.bs.modal', function () {
10
+ const openCollapses = modal.querySelectorAll('.accordion-collapse.show');
11
+ openCollapses.forEach(collapseEl => {
12
+ collapseEl.classList.remove('show');
13
+ collapseEl.style.height = '';
14
+ });
15
+
16
+ const accordionButtons = modal.querySelectorAll('.accordion-button');
17
+ accordionButtons.forEach(button => {
18
+ button.classList.add('collapsed');
19
+ button.setAttribute('aria-expanded', 'false');
20
+ });
21
+ });
22
+ });
23
+
24
+ function populateStepByStepGuide(ruleId) {
25
+ const container = document.getElementById('stepByStepContainer');
26
+ if (!container) return;
27
+
28
+ const steps = stepByStepGuide[ruleId];
29
+
30
+ if (!steps) {
31
+ // No step-by-step guide available for this rule
32
+ container.innerHTML = `
33
+ <div class="no-steps-message">
34
+ <p>Step-by-step guide not yet available for this issue.</p>
35
+ </div>
36
+ `;
37
+ return;
38
+ }
39
+
40
+ // Convert A11y Playground text to link
41
+ const linkifyA11yPlayground = (text) => {
42
+ const escaped = htmlEscapeString(text);
43
+ return escaped.replace(
44
+ /A11y Playground/g,
45
+ `<a href="${a11yPlaygroundLink}" target="_blank" rel="noopener noreferrer">A11y Playground</a>`
46
+ );
47
+ };
48
+
49
+ // Build the step-by-step HTML
50
+ const stepsHtml = `
51
+ <div class="rule-modal-step">
52
+ <div class="step-header">
53
+ <h4 class="step-title">1. Check</h4>
54
+ </div>
55
+ <div class="step-content">
56
+ <p class="step-text">${linkifyA11yPlayground(steps.check)}</p>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="rule-modal-step">
61
+ <div class="step-header">
62
+ <h4 class="step-title">2. Fix</h4>
63
+ </div>
64
+ <div class="step-content">
65
+ <p class="step-text">${linkifyA11yPlayground(steps.fix)}</p>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="rule-modal-step">
70
+ <div class="step-header">
71
+ <h4 class="step-title">3. Review</h4>
72
+ </div>
73
+ <div class="step-content">
74
+ <p class="step-text">${linkifyA11yPlayground(steps.review)}</p>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="rule-modal-step">
79
+ <div class="step-header">
80
+ <h4 class="step-title">4. Learn</h4>
81
+ </div>
82
+ <div class="step-content">
83
+ <p class="step-text">${linkifyA11yPlayground(steps.learn)}</p>
84
+ </div>
85
+ </div>
86
+ `;
87
+
88
+ container.innerHTML = stepsHtml;
89
+ }
90
+
91
+ function expandRule(selectedCategory, selectedRule) {
92
+ const conformanceLevels = {
93
+ wcag2a: 'A',
94
+ wcag2aa: 'AA',
95
+ wcag21aa: 'AA',
96
+ wcag22aa: 'AA',
97
+ wcag2aaa: 'AAA',
98
+ };
99
+
100
+ const a11yRuleShortDescriptionMap = scanData?.a11yRuleShortDescriptionMap || {};
101
+ const a11yRuleLongDescriptionMap = scanData?.a11yRuleLongDescriptionMap || {};
102
+ const disabilityBadgesMap = scanData?.disabilityBadgesMap || {};
103
+
104
+ // Set image based on WCAG criterion
105
+ const imageElement = document.getElementById('expandedRuleImage');
106
+ const imageContainer = document.getElementById('expandedRuleImageContainer');
107
+
108
+ if (imageElement && imageContainer) {
109
+ let imageSrc = null;
110
+
111
+ // Get the first WCAG criterion from the rule's conformance
112
+ if (selectedRule.conformance && selectedRule.conformance.length > 0) {
113
+ const wcagConformance = selectedRule.conformance.filter(c => c.startsWith('wcag'));
114
+ const wcagCriteriaLabels = scanData?.wcagCriteriaLabels || {};
115
+
116
+ // Find the first matching WCAG criterion
117
+ for (const wcag of wcagConformance) {
118
+ const formattedWcag = formatWcagId(wcag);
119
+
120
+ if (wcagCriteriaLabels[formattedWcag]) {
121
+ // Try to get SVG from the map
122
+ const svg = window.getWcagSvg ? window.getWcagSvg(formattedWcag) : null;
123
+ if (svg) {
124
+ imageSrc = window.svgToDataUrl ? window.svgToDataUrl(svg) : svg;
125
+ break;
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ // Hide image container if no SVG found, otherwise show it
132
+ if (!imageSrc) {
133
+ imageContainer.style.display = 'none';
134
+ } else {
135
+ imageContainer.style.display = 'flex';
136
+ imageElement.src = imageSrc;
137
+ imageElement.alt = selectedRule.description || 'Issue illustration';
138
+ }
139
+ }
140
+
141
+ // Set title from a11yRuleShortDescriptionMap
142
+ const titleElement = document.getElementById('expandedRuleName');
143
+ if (titleElement) {
144
+ const shortDescription = a11yRuleShortDescriptionMap[selectedRule.rule];
145
+ titleElement.textContent = shortDescription || selectedRule.description || '';
146
+ }
147
+
148
+ // Set category badge
149
+ const categoryBadge = document.getElementById('expandedRuleCategoryBadge');
150
+ if (categoryBadge) {
151
+ const categoryLabels = {
152
+ mustFix: 'Must Fix',
153
+ goodToFix: 'Good to Fix',
154
+ needsReview: 'Manual Test',
155
+ passed: 'Passed',
156
+ };
157
+ categoryBadge.textContent = categoryLabels[selectedCategory] || 'Must Fix';
158
+
159
+ // Update badge class
160
+ categoryBadge.className = 'mustfix-badge-label';
161
+ if (selectedCategory === 'goodToFix') {
162
+ categoryBadge.style.background = 'var(--strong-orange)';
163
+ categoryBadge.style.borderColor = 'var(--strong-orange)';
164
+ } else if (selectedCategory === 'needsReview') {
165
+ categoryBadge.style.background = 'var(--very-dark-gray)';
166
+ categoryBadge.style.borderColor = 'var(--very-dark-gray)';
167
+ } else if (selectedCategory === 'mustFix') {
168
+ categoryBadge.style.background = 'var(--light-carmine-pink)';
169
+ categoryBadge.style.borderColor = 'var(--light-carmine-pink)';
170
+ }
171
+
172
+ // Update Manual Test Section
173
+ const manualTestSection = document.getElementById('expandedRuleManualTestSection');
174
+ if (selectedCategory === 'needsReview' && manualTestSection) {
175
+ manualTestSection.style.display = 'block';
176
+ } else if (manualTestSection) {
177
+ manualTestSection.style.display = 'none';
178
+ }
179
+ }
180
+
181
+ // Populate conformance badges
182
+ const conformanceContainer = document.getElementById('expandedRuleConformance');
183
+ if (conformanceContainer && selectedRule.conformance && selectedRule.conformance.length > 0) {
184
+ const wcagConformance = selectedRule.conformance.filter(c => c.startsWith('wcag'));
185
+ const wcagCriteriaLabels = scanData?.wcagCriteriaLabels || {};
186
+
187
+ const criteriaNumbers = [];
188
+ let level = null;
189
+
190
+ wcagConformance.forEach(wcag => {
191
+ const formattedWcag = formatWcagId(wcag);
192
+
193
+ if (wcagCriteriaLabels[formattedWcag]) {
194
+ criteriaNumbers.push(formattedWcag);
195
+ if (!level) {
196
+ level = wcagCriteriaLabels[formattedWcag];
197
+ }
198
+ }
199
+ });
200
+
201
+ const badges = criteriaNumbers.map(
202
+ criteria => `<span class="conformance-badge">${criteria}</span>`,
203
+ );
204
+
205
+ if (level) {
206
+ badges.push(`<span class="conformance-badge">Level ${level}</span>`);
207
+ }
208
+
209
+ conformanceContainer.innerHTML = badges.join('');
210
+ }
211
+
212
+ // Set long description from a11yRuleLongDescriptionMap
213
+ const longDescriptionElement = document.getElementById('expandedRuleDescription');
214
+ if (longDescriptionElement) {
215
+ const longDescription = a11yRuleLongDescriptionMap[selectedRule.rule];
216
+ longDescriptionElement.textContent = longDescription || whyItMatters[selectedRule.rule] || '';
217
+ }
218
+
219
+ // Populate disability impact
220
+ const disabilitySection = document.getElementById('expandedRuleDisabilitySection');
221
+ const disabilityMessage = document.getElementById('expandedRuleDisabilityMessage');
222
+ const disabilities = disabilityBadgesMap[selectedRule.rule] || [];
223
+
224
+ if (disabilityMessage && disabilities.length > 0) {
225
+ let disabilityText = '';
226
+ if (disabilities.length === 1) {
227
+ disabilityText = `${disabilities[0]} Disability`;
228
+ } else if (disabilities.length === 2) {
229
+ disabilityText = `${disabilities[0]} and ${disabilities[1]} Disability`;
230
+ } else {
231
+ const disabilityList = [...disabilities];
232
+ const lastDisability = disabilityList.pop();
233
+ disabilityText = `${disabilityList.join(', ')} and ${lastDisability} Disability`;
234
+ }
235
+
236
+ disabilityMessage.innerHTML = `This issue prevents users with <span class="disability-text">${disabilityText}</span> from navigating your website.`;
237
+ disabilitySection.style.display = 'block';
238
+ } else {
239
+ disabilitySection.style.display = 'none';
240
+ }
241
+
242
+ if (oobeeAiRules.includes(selectedRule.rule)) {
243
+ document.querySelector('#expandedRuleAiFeedback').style.display = 'block';
244
+ } else {
245
+ document.querySelector('#expandedRuleAiFeedback').style.display = 'none';
246
+ }
247
+
248
+ // Populate step-by-step guide
249
+ populateStepByStepGuide(selectedRule.rule);
250
+
251
+ const availableFixCategories = [];
252
+
253
+ const itemsToUse = typeof filteredItems !== 'undefined' ? filteredItems : scanItems;
254
+
255
+ // Reset group by radio buttons to "Page" when modal opens
256
+ const groupByPageRadio = document.getElementById('groupByPage');
257
+ const groupByHtmlRadio = document.getElementById('groupByHtmlElement');
258
+ if (groupByPageRadio && groupByHtmlRadio) {
259
+ groupByPageRadio.checked = true;
260
+ groupByHtmlRadio.checked = false;
261
+ }
262
+
263
+ // Note: Need to check if we want to sort by default in scanItems or keep the function here
264
+ const sortCategoriesByConformance = (items) => {
265
+ ['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
266
+ const categoryData = itemsToUse[category];
267
+ if (!categoryData || !Array.isArray(categoryData.rules)) return;
268
+
269
+ categoryData.rules.forEach(rule => {
270
+ if (!Array.isArray(rule.pagesAffected)) return;
271
+
272
+ rule.pagesAffected.sort((a, b) => {
273
+ const lenA = Array.isArray(a.items) ? a.items.length : 0;
274
+ const lenB = Array.isArray(b.items) ? b.items.length : 0;
275
+ return lenB - lenA; // DESC
276
+ });
277
+ });
278
+ });
279
+ };
280
+
281
+ sortCategoriesByConformance(itemsToUse);
282
+
283
+ Object.keys(itemsToUse).forEach(category => {
284
+ const ruleInCategory = itemsToUse[category]?.rules?.find(r => r.rule === selectedRule.rule);
285
+
286
+ if (ruleInCategory !== undefined && category !== 'passed') {
287
+ if (category !== 'passed') {
288
+ availableFixCategories.push(category);
289
+ }
290
+
291
+ // Automatically display content for the selected category
292
+ if (category === selectedCategory) {
293
+ const occurrencesText = `${ruleInCategory.totalItems} Total occurrences`;
294
+ const dropdownToggle = document.getElementById('expandedRuleDropdownToggleCategoryInfo');
295
+ dropdownToggle.innerText = `${ruleInCategory.totalItems} Total occ.`;
296
+ dropdownToggle.setAttribute('aria-label', occurrencesText);
297
+ document.getElementById('expandedRuleDropdownTitle').innerText =
298
+ `Pages affected by this issue (${ruleInCategory.pagesAffected.length})`;
299
+ buildExpandedRuleCategoryContent(category, ruleInCategory);
300
+ document.getElementById('expandedRulePageContent').innerText =
301
+ `Total ${ruleInCategory.pagesAffected.length} affected pages`;
302
+ }
303
+ }
304
+ });
305
+ }
306
+ </script>