@govtechsg/oobee 0.10.70 → 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 (93) hide show
  1. package/DETAILS.md +0 -1
  2. package/README.md +12 -0
  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 +534 -59
  9. package/src/crawlers/crawlDomain.ts +313 -305
  10. package/src/crawlers/crawlIntelligentSitemap.ts +24 -18
  11. package/src/crawlers/crawlLocalFile.ts +29 -27
  12. package/src/crawlers/crawlSitemap.ts +264 -253
  13. package/src/crawlers/custom/utils.ts +809 -119
  14. package/src/crawlers/runCustom.ts +29 -4
  15. package/src/generateHtmlReport.ts +224 -0
  16. package/src/mergeAxeResults.ts +94 -44
  17. package/src/runGenerateJustHtmlReport.ts +20 -0
  18. package/src/services/s3Uploader.ts +184 -0
  19. package/src/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
  20. package/src/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
  21. package/src/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
  22. package/src/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
  23. package/src/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
  24. package/src/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
  25. package/src/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
  26. package/src/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
  27. package/src/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
  28. package/src/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
  29. package/src/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
  30. package/src/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
  31. package/src/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
  32. package/src/static/ejs/partials/components/{topFive.ejs → topTen.ejs} +2 -2
  33. package/src/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
  34. package/src/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
  35. package/src/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
  36. package/src/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
  37. package/src/static/ejs/partials/footer.ejs +1 -1
  38. package/src/static/ejs/partials/header.ejs +7 -223
  39. package/src/static/ejs/partials/main.ejs +12 -23
  40. package/src/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
  41. package/src/static/ejs/partials/scripts/categorySummary.ejs +1 -1
  42. package/src/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
  43. package/src/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
  44. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
  45. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
  46. package/src/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
  47. package/src/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
  48. package/src/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
  49. package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +949 -0
  50. package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +352 -0
  51. package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
  52. package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
  53. package/src/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
  54. package/src/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
  55. package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +61 -57
  56. package/src/static/ejs/partials/scripts/topTen.ejs +61 -0
  57. package/src/static/ejs/partials/scripts/utils.ejs +15 -0
  58. package/src/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
  59. package/src/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
  60. package/src/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
  61. package/src/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
  62. package/src/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
  63. package/src/static/ejs/partials/styles/bootstrap.ejs +17 -1
  64. package/src/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
  65. package/src/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
  66. package/src/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
  67. package/src/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
  68. package/src/static/ejs/partials/styles/header.ejs +7 -0
  69. package/src/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
  70. package/src/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
  71. package/src/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
  72. package/src/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
  73. package/src/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
  74. package/src/static/ejs/partials/styles/styles.ejs +198 -470
  75. package/src/static/ejs/partials/styles/topTenCard.ejs +44 -0
  76. package/src/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
  77. package/src/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
  78. package/src/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
  79. package/src/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
  80. package/src/static/ejs/report.ejs +42 -259
  81. package/src/static/ejs/summary.ejs +1 -1
  82. package/src/utils.ts +30 -0
  83. package/src/static/ejs/partials/components/categorySelector.ejs +0 -4
  84. package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +0 -57
  85. package/src/static/ejs/partials/components/pagesScannedModal.ejs +0 -70
  86. package/src/static/ejs/partials/components/reportSearch.ejs +0 -47
  87. package/src/static/ejs/partials/components/ruleOffcanvas.ejs +0 -105
  88. package/src/static/ejs/partials/components/scanAbout.ejs +0 -328
  89. package/src/static/ejs/partials/components/wcagCompliance.ejs +0 -52
  90. package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +0 -190
  91. package/src/static/ejs/partials/scripts/reportSearch.ejs +0 -287
  92. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +0 -804
  93. package/src/static/ejs/partials/scripts/scanAboutScript.ejs +0 -38
@@ -0,0 +1,137 @@
1
+ <script>
2
+ (function initIssueDetailCard() {
3
+ window.populateIssueDetailCard = function (issue) {
4
+ const detailCard = document.getElementById('a11yIssueDetailCard');
5
+ if (!detailCard) return;
6
+
7
+ detailCard.style.display = 'block';
8
+
9
+ const titleElement = document.getElementById('issueDetailTitle');
10
+ if (titleElement) {
11
+ titleElement.textContent = issue.description || 'Issue';
12
+ }
13
+
14
+ const imageElement = document.getElementById('issueDetailImage');
15
+ const imageContainer = imageElement?.closest('.issue-detail-image-container');
16
+
17
+ if (imageElement && imageContainer) {
18
+ let imageSrc = null;
19
+
20
+ if (issue.conformance && issue.conformance.length > 0) {
21
+ const wcagConformance = issue.conformance.filter(c => c.startsWith('wcag'));
22
+ const wcagCriteriaLabels = scanData?.wcagCriteriaLabels || {};
23
+
24
+ for (const wcag of wcagConformance) {
25
+ const formattedWcag = formatWcagId(wcag);
26
+
27
+ if (wcagCriteriaLabels[formattedWcag]) {
28
+ const svg = window.getWcagSvg ? window.getWcagSvg(formattedWcag) : null;
29
+ if (svg) {
30
+ imageSrc = window.svgToDataUrl ? window.svgToDataUrl(svg) : svg;
31
+ break;
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ if (!imageSrc) {
38
+ imageContainer.style.display = 'none';
39
+ } else {
40
+ imageContainer.style.display = 'flex';
41
+ imageElement.src = imageSrc;
42
+ imageElement.alt = issue.description || 'Issue illustration';
43
+ }
44
+ }
45
+
46
+ // Populate conformance badges
47
+ const conformanceContainer = document.getElementById('issueDetailConformance');
48
+ if (conformanceContainer && issue.conformance && issue.conformance.length > 0) {
49
+ const wcagConformance = issue.conformance.filter(c => c.startsWith('wcag'));
50
+ const wcagCriteriaLabels = scanData?.wcagCriteriaLabels || {};
51
+
52
+ const criteriaNumbers = [];
53
+ let level = null;
54
+
55
+ wcagConformance.forEach(wcag => {
56
+ const formattedWcag = formatWcagId(wcag);
57
+
58
+ if (wcagCriteriaLabels[formattedWcag]) {
59
+ criteriaNumbers.push(formattedWcag);
60
+ if (!level) {
61
+ level = wcagCriteriaLabels[formattedWcag];
62
+ }
63
+ }
64
+ });
65
+
66
+ const badges = criteriaNumbers.map(
67
+ criteria => `<span class="issue-detail-conformance-badge">${criteria}</span>`,
68
+ );
69
+
70
+ if (level) {
71
+ badges.push(`<span class="issue-detail-conformance-badge">Level ${level}</span>`);
72
+ }
73
+
74
+ conformanceContainer.innerHTML = badges.join('');
75
+ }
76
+
77
+ // Populate long description
78
+ const longDescriptionElement = document.getElementById('issueDetailLongDescription');
79
+ const a11yRuleLongDescriptionMap = scanData?.a11yRuleLongDescriptionMap || {};
80
+ if (longDescriptionElement) {
81
+ const longDescription = a11yRuleLongDescriptionMap[issue.ruleId] || '';
82
+ longDescriptionElement.textContent = longDescription;
83
+ }
84
+
85
+ // Populate disability impact
86
+ const disabilitySection = document.getElementById('issueDetailDisabilitySection');
87
+ const disabilityMessage = document.getElementById('issueDetailDisabilityMessage');
88
+ const disabilityBadgesMap = scanData?.disabilityBadgesMap || {};
89
+ const disabilities = disabilityBadgesMap[issue.ruleId] || [];
90
+
91
+ if (disabilityMessage && disabilities.length > 0) {
92
+ let disabilityText = '';
93
+ if (disabilities.length === 1) {
94
+ disabilityText = `${disabilities[0]} Disability `;
95
+ } else if (disabilities.length === 2) {
96
+ disabilityText = `${disabilities[0]} and ${disabilities[1]} Disability`;
97
+ } else {
98
+ const boldedDisabilities = disabilities.map(d => `${d}`);
99
+ const lastDisability = boldedDisabilities.pop();
100
+ disabilityText = `${boldedDisabilities.join(', ')} and ${lastDisability} Disability`;
101
+ }
102
+
103
+ disabilityMessage.innerHTML = `This issue prevents users with <span class="disability-text">${disabilityText}</span> from navigating your website.`;
104
+ disabilitySection.style.display = 'block';
105
+ } else {
106
+ disabilitySection.style.display = 'none';
107
+ }
108
+
109
+ // Handle "View issue details" button
110
+ const viewDetailsBtn = document.getElementById('viewIssueDetailsBtn');
111
+ if (viewDetailsBtn) {
112
+ const newBtn = viewDetailsBtn.cloneNode(true);
113
+ viewDetailsBtn.parentNode.replaceChild(newBtn, viewDetailsBtn);
114
+
115
+ newBtn.addEventListener('click', function () {
116
+ const category = issue.category || 'mustFix';
117
+ const ruleData = scanItems[category]?.rules?.find(r => r.rule === issue.ruleId);
118
+
119
+ if (ruleData && typeof expandRule === 'function') {
120
+ expandRule(category, ruleData);
121
+
122
+ const modalElement = document.getElementById('expandedRule');
123
+ if (modalElement) {
124
+ const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
125
+ modal.show();
126
+ }
127
+ }
128
+ });
129
+ }
130
+ };
131
+
132
+ const detailCard = document.getElementById('a11yIssueDetailCard');
133
+ if (detailCard) {
134
+ detailCard.style.display = 'none';
135
+ }
136
+ })();
137
+ </script>
@@ -0,0 +1,214 @@
1
+ <script>
2
+ (function populatePrioritiseIssues() {
3
+ const a11yRuleShortDescriptionMap = scanData?.a11yRuleShortDescriptionMap || {};
4
+ const disabilityBadgesMap = scanData?.disabilityBadgesMap || {};
5
+ const wcagLinks = scanData?.wcagLinks || {};
6
+ const wcagViolations = scanData?.wcagViolations || [];
7
+ const wcagClauses = scanData?.wcagClauses || {};
8
+ const mustFixRules = scanItems?.mustFix?.rules || [];
9
+
10
+ const wcagCriteriaMap = new Map();
11
+
12
+ mustFixRules.forEach(rule => {
13
+ const ruleId = rule.rule;
14
+ const wcagCriteria = rule.conformance?.filter(c => c.startsWith('wcag')) || [];
15
+
16
+ wcagCriteria.forEach(wcag => {
17
+ const formattedWcag = formatWcagId(wcag);
18
+
19
+ if (!wcagCriteriaMap.has(formattedWcag)) {
20
+ const clauseKey = formattedWcag.replace('WCAG ', '');
21
+ wcagCriteriaMap.set(formattedWcag, {
22
+ id: formattedWcag,
23
+ name: wcagClauses[clauseKey] || formattedWcag,
24
+ issues: [],
25
+ totalIssueCount: 0,
26
+ });
27
+ }
28
+
29
+ const criteria = wcagCriteriaMap.get(formattedWcag);
30
+ const displayName = a11yRuleShortDescriptionMap[ruleId] || rule.description;
31
+
32
+ criteria.issues.push({
33
+ ruleId: ruleId,
34
+ description: displayName,
35
+ originalDescription: rule.description,
36
+ totalItems: rule.totalItems || 0,
37
+ helpUrl: rule.helpUrl || '',
38
+ axeImpact: rule.axeImpact || '',
39
+ conformance: rule.conformance || [],
40
+ pagesAffected: rule.pagesAffected || [],
41
+ });
42
+
43
+ criteria.totalIssueCount = criteria.issues.length || 0;
44
+ });
45
+ });
46
+
47
+ const sortedCriteria = Array.from(wcagCriteriaMap.values()).sort((a, b) => {
48
+ if (a.totalIssueCount !== b.totalIssueCount) {
49
+ return a.totalIssueCount - b.totalIssueCount;
50
+ }
51
+ return a.id.localeCompare(b.id);
52
+ });
53
+
54
+ const failedCriteria = sortedCriteria.filter(criteria =>
55
+ wcagViolations.some(v => formatWcagId(v) === criteria.id),
56
+ );
57
+
58
+ const cardElement = document.getElementById('prioritiseIssuesCard');
59
+ if (failedCriteria.length === 0) {
60
+ if (cardElement) {
61
+ cardElement.style.display = 'none';
62
+ }
63
+ return;
64
+ }
65
+
66
+ const accordionContainer = document.getElementById('prioritiseIssuesAccordion');
67
+ if (!accordionContainer) return;
68
+
69
+ const accordionHTML = failedCriteria
70
+ .map((criteria, index) => {
71
+ const wcagScore = criteria.id.replace('WCAG ', '');
72
+ const issueWord = criteria.issues.length === 1 ? 'issue' : 'issues';
73
+
74
+ const issuesListHTML = criteria.issues
75
+ .map(issue => {
76
+ const disabilities = disabilityBadgesMap[issue.ruleId] || [];
77
+ const disabilityBadges =
78
+ disabilities.length > 0
79
+ ? `
80
+ <div class="priority-issue-badges">
81
+ ${disabilities.map(disability => `<span class="disability-badge">${disability} Disability</span>`).join('')}
82
+ </div>
83
+ `
84
+ : '';
85
+
86
+ return `
87
+ <li class="priority-issue-item d-flex g-one" data-rule-id="${issue.ruleId}" role="button" tabindex="0">
88
+ <div class="d-flex justify-content-between align-items-center w-90">
89
+ <div class="priority-issue-title">${issue.description}</div>
90
+ ${disabilityBadges}
91
+ </div>
92
+ </li>
93
+ `;
94
+ })
95
+ .join('');
96
+
97
+ return `
98
+ <div class="accordion-item">
99
+ <h3 class="accordion-header" id="heading${index}">
100
+ <button
101
+ class="accordion-button collapsed"
102
+ type="button"
103
+ data-bs-toggle="collapse"
104
+ data-bs-target="#collapse${index}"
105
+ aria-expanded="false"
106
+ aria-controls="collapse${index}"
107
+ >
108
+ <span class="h4 mb-0">WCAG ${wcagScore} -&nbsp;</span>
109
+ <span class="h4 fw-normal mb-0">${criteria.issues.length} ${issueWord}</span>
110
+ <span class="wcag-score-badge">-1 WCAG Score</span>
111
+ <svg class="accordion-icon-collapsed" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
112
+ <path d="M7.41 8.58984L12 13.1698L16.59 8.58984L18 9.99984L12 15.9998L6 9.99984L7.41 8.58984Z" fill="#5735DF"/>
113
+ </svg>
114
+ <svg class="accordion-icon-expanded" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
115
+ <path d="M7.41 15.4102L12 10.8302L16.59 15.4102L18 14.0002L12 8.00016L6 14.0002L7.41 15.4102Z" fill="#5735DF"/>
116
+ </svg>
117
+ </button>
118
+ </h3>
119
+ <div
120
+ id="collapse${index}"
121
+ class="accordion-collapse collapse"
122
+ data-bs-parent="#prioritiseIssuesAccordion"
123
+ >
124
+ <div class="accordion-body">
125
+ <ul class="priority-issues-list">
126
+ ${issuesListHTML}
127
+ </ul>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ `;
132
+ })
133
+ .join('');
134
+
135
+ accordionContainer.innerHTML = accordionHTML;
136
+
137
+ document.querySelectorAll('.priority-issue-item').forEach(item => {
138
+ const selectIssue = function () {
139
+ const ruleId = item.getAttribute('data-rule-id');
140
+
141
+ document.querySelectorAll('.priority-issue-item').forEach(el => {
142
+ el.classList.remove('active');
143
+ el.setAttribute('aria-selected', 'false');
144
+ });
145
+ document.querySelectorAll('.priority-issue-title').forEach(el => {
146
+ el.classList.remove('active');
147
+ });
148
+
149
+ item.classList.add('active');
150
+ item.setAttribute('aria-selected', 'true');
151
+ const title = item.querySelector('.priority-issue-title');
152
+ if (title) {
153
+ title.classList.add('active');
154
+ }
155
+
156
+ let selectedRule = null;
157
+ for (const criteria of failedCriteria) {
158
+ const issue = criteria.issues.find(i => i.ruleId === ruleId);
159
+ if (issue) {
160
+ selectedRule = issue;
161
+ break;
162
+ }
163
+ }
164
+
165
+ if (selectedRule && typeof window.populateIssueDetailCard === 'function') {
166
+ window.populateIssueDetailCard(selectedRule);
167
+ }
168
+ };
169
+
170
+ // Click handler
171
+ item.addEventListener('click', selectIssue);
172
+
173
+ // Keyboard handler
174
+ item.addEventListener('keydown', function (event) {
175
+ if (event.key === 'Enter' || event.key === ' ') {
176
+ event.preventDefault();
177
+ selectIssue();
178
+ }
179
+ });
180
+ });
181
+
182
+ // Automatically open first accordion and select first issue
183
+ if (failedCriteria.length > 0 && failedCriteria[0].issues.length > 0) {
184
+ const firstAccordionButton = document.querySelector(
185
+ '#prioritiseIssuesAccordion .accordion-button',
186
+ );
187
+ const firstAccordionCollapse = document.querySelector(
188
+ '#prioritiseIssuesAccordion .accordion-collapse',
189
+ );
190
+
191
+ if (firstAccordionButton && firstAccordionCollapse) {
192
+ firstAccordionButton.classList.remove('collapsed');
193
+ firstAccordionButton.setAttribute('aria-expanded', 'true');
194
+ firstAccordionCollapse.classList.add('show');
195
+ }
196
+
197
+ const firstIssueItem = document.querySelector('.priority-issue-item');
198
+ if (firstIssueItem) {
199
+ firstIssueItem.classList.add('active');
200
+ firstIssueItem.setAttribute('aria-selected', 'true');
201
+ const firstIssueTitle = firstIssueItem.querySelector('.priority-issue-title');
202
+ if (firstIssueTitle) {
203
+ firstIssueTitle.classList.add('active');
204
+ }
205
+
206
+ const firstIssue = failedCriteria[0].issues[0];
207
+
208
+ if (typeof window.populateIssueDetailCard === 'function') {
209
+ window.populateIssueDetailCard(firstIssue);
210
+ }
211
+ }
212
+ }
213
+ })();
214
+ </script>