snap-ally 0.0.2 → 0.1.0-beta
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/README.md +44 -43
- package/dist/A11yHtmlRenderer.d.ts +3 -3
- package/dist/A11yHtmlRenderer.js +31 -18
- package/dist/A11yReportAssets.d.ts +1 -1
- package/dist/A11yReportAssets.js +20 -12
- package/dist/A11yScanner.d.ts +2 -0
- package/dist/A11yScanner.js +63 -14
- package/dist/A11yTimeUtils.d.ts +4 -0
- package/dist/A11yTimeUtils.js +15 -0
- package/dist/SnapAllyReporter.d.ts +1 -0
- package/dist/SnapAllyReporter.js +47 -23
- package/dist/models/index.d.ts +11 -1
- package/dist/templates/accessibility-report.html +205 -1324
- package/dist/templates/execution-summary.html +155 -644
- package/dist/templates/global-report-styles.css +1536 -0
- package/dist/templates/report-app.js +857 -0
- package/dist/templates/test-execution-report.html +151 -536
- package/package.json +6 -7
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report App - Vanilla JS Rendering Engine for Snap Ally
|
|
3
|
+
* Replaces EJS for generating HTML reports securely using client-side DOM processing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
7
|
+
// Pull injected data
|
|
8
|
+
const data = window.snapAllyData;
|
|
9
|
+
if (!data) {
|
|
10
|
+
console.error('Snap Ally: No report data found in window.snapAllyData');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Set up custom severity colors if provided
|
|
15
|
+
applyCustomColors(data);
|
|
16
|
+
|
|
17
|
+
// Determine which template we are on based on root containers
|
|
18
|
+
if (document.getElementById('report-summary-root')) {
|
|
19
|
+
renderExecutionSummary(data);
|
|
20
|
+
} else if (document.getElementById('test-execution-root')) {
|
|
21
|
+
renderTestExecutionReport(data);
|
|
22
|
+
} else if (document.getElementById('accessibility-report-root')) {
|
|
23
|
+
renderAccessibilityReport(data);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Apply user-defined CSS overrides */
|
|
28
|
+
function applyCustomColors(data) {
|
|
29
|
+
if (!data) return;
|
|
30
|
+
const root = document.documentElement;
|
|
31
|
+
|
|
32
|
+
// Custom colors from Summary / Execution Report (data.colors)
|
|
33
|
+
if (data.colors) {
|
|
34
|
+
if (data.colors.critical) root.style.setProperty('--critical', data.colors.critical);
|
|
35
|
+
if (data.colors.serious) root.style.setProperty('--serious', data.colors.serious);
|
|
36
|
+
if (data.colors.moderate) root.style.setProperty('--moderate', data.colors.moderate);
|
|
37
|
+
if (data.colors.minor) root.style.setProperty('--minor', data.colors.minor);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Custom colors from specific Action Report (data.criticalColor)
|
|
41
|
+
if (data.criticalColor) root.style.setProperty('--critical', data.criticalColor);
|
|
42
|
+
if (data.seriousColor) root.style.setProperty('--serious', data.seriousColor);
|
|
43
|
+
if (data.moderateColor) root.style.setProperty('--moderate', data.moderateColor);
|
|
44
|
+
if (data.minorColor) root.style.setProperty('--minor', data.minorColor);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// TEMPLATE 1: Test Execution Report (Individual Test Result)
|
|
49
|
+
// ============================================================================
|
|
50
|
+
function renderTestExecutionReport(data) {
|
|
51
|
+
const root = document.getElementById('test-execution-root');
|
|
52
|
+
if (!root) return;
|
|
53
|
+
root.style.display = 'block';
|
|
54
|
+
|
|
55
|
+
// Set document title
|
|
56
|
+
document.title = `Snap Ally - Test Execution: ${data.title}`;
|
|
57
|
+
|
|
58
|
+
// Header
|
|
59
|
+
document.getElementById('report-title').textContent = data.title;
|
|
60
|
+
|
|
61
|
+
const statusBadge = document.getElementById('report-status-badge');
|
|
62
|
+
statusBadge.classList.add(`status-${data.status}`);
|
|
63
|
+
document.getElementById('report-status-icon').textContent = data.statusIcon;
|
|
64
|
+
document.getElementById('report-status-text').textContent = data.status;
|
|
65
|
+
|
|
66
|
+
if (data.a11yReportPath && data.a11yErrorCount === 0) {
|
|
67
|
+
document.getElementById('report-a11y-verified').style.display = 'flex';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
document.getElementById('report-duration').textContent = data.duration;
|
|
71
|
+
document.getElementById('report-browser').textContent = data.browser;
|
|
72
|
+
|
|
73
|
+
// Tags
|
|
74
|
+
const tagsContainer = document.getElementById('report-tags-container');
|
|
75
|
+
const tagTemplate = document.getElementById('tag-template');
|
|
76
|
+
(data.tags || []).forEach(tag => {
|
|
77
|
+
const clone = tagTemplate.content.cloneNode(true);
|
|
78
|
+
clone.querySelector('.tag-badge').textContent = tag;
|
|
79
|
+
tagsContainer.appendChild(clone);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (data.a11yReportPath) {
|
|
83
|
+
const a11yLink = document.getElementById('view-a11y-report-link');
|
|
84
|
+
a11yLink.href = `./${data.a11yReportPath}`;
|
|
85
|
+
a11yLink.style.display = 'flex';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Description
|
|
89
|
+
if (data.description) {
|
|
90
|
+
document.getElementById('card-description').style.display = 'block';
|
|
91
|
+
document.getElementById('report-description').textContent = data.description;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Helper method for array elements
|
|
95
|
+
const renderList = (array, listId, cardId) => {
|
|
96
|
+
if (array && array.length > 0) {
|
|
97
|
+
document.getElementById(cardId).style.display = 'block';
|
|
98
|
+
const list = document.getElementById(listId);
|
|
99
|
+
const tpl = document.getElementById('string-item-template');
|
|
100
|
+
array.forEach(item => {
|
|
101
|
+
const clone = tpl.content.cloneNode(true);
|
|
102
|
+
clone.querySelector('li').textContent = item;
|
|
103
|
+
list.appendChild(clone);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
renderList(data.preConditions, 'list-preconditions', 'card-preconditions');
|
|
109
|
+
renderList(data.steps, 'list-steps', 'card-steps');
|
|
110
|
+
renderList(data.postConditions, 'list-postconditions', 'card-postconditions');
|
|
111
|
+
|
|
112
|
+
// Exceptions (Filter out A11y generic error text)
|
|
113
|
+
const filteredErrs = (data.errors || []).filter(err => !err.includes('Accessibility audit failed'));
|
|
114
|
+
renderList(filteredErrs, 'list-exceptions', 'card-exceptions');
|
|
115
|
+
|
|
116
|
+
if (data.videoPath) {
|
|
117
|
+
document.getElementById('card-video').style.display = 'block';
|
|
118
|
+
const source = document.getElementById('report-video-source');
|
|
119
|
+
source.src = data.videoPath;
|
|
120
|
+
source.parentElement.load();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (data.screenshotPaths && data.screenshotPaths.length > 0) {
|
|
124
|
+
document.getElementById('card-screenshots').style.display = 'block';
|
|
125
|
+
const grid = document.getElementById('grid-screenshots');
|
|
126
|
+
const tpl = document.getElementById('screenshot-template');
|
|
127
|
+
data.screenshotPaths.forEach((p) => {
|
|
128
|
+
const clone = tpl.content.cloneNode(true);
|
|
129
|
+
clone.querySelector('img').src = p;
|
|
130
|
+
grid.appendChild(clone);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (data.attachments && data.attachments.length > 0) {
|
|
135
|
+
document.getElementById('card-attachments').style.display = 'block';
|
|
136
|
+
const list = document.getElementById('list-attachments');
|
|
137
|
+
const tpl = document.getElementById('attachment-template');
|
|
138
|
+
data.attachments.forEach(att => {
|
|
139
|
+
const clone = tpl.content.cloneNode(true);
|
|
140
|
+
const a = clone.querySelector('a');
|
|
141
|
+
a.href = att.path;
|
|
142
|
+
clone.querySelector('.attachment-name').textContent = att.name;
|
|
143
|
+
list.appendChild(clone);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Accessibility Success / Error Cards inside general test report
|
|
148
|
+
if (data.a11yReportPath && data.a11yErrorCount === 0) {
|
|
149
|
+
document.getElementById('card-a11y-success').style.display = 'flex';
|
|
150
|
+
} else if (data.a11yErrors && data.a11yErrors.length > 0) {
|
|
151
|
+
document.getElementById('card-a11y-errors').style.display = 'block';
|
|
152
|
+
const list = document.getElementById('list-a11y-errors');
|
|
153
|
+
const tplError = document.getElementById('a11y-error-template');
|
|
154
|
+
const tplInstance = document.getElementById('a11y-instance-template');
|
|
155
|
+
|
|
156
|
+
data.a11yErrors.forEach(error => {
|
|
157
|
+
const clone = tplError.content.cloneNode(true);
|
|
158
|
+
clone.querySelector('.violation-item').classList.add(error.severity);
|
|
159
|
+
clone.querySelector('.rule-id').textContent = error.id;
|
|
160
|
+
const sevBadge = clone.querySelector('.sev-badge');
|
|
161
|
+
sevBadge.classList.add(error.severity);
|
|
162
|
+
sevBadge.textContent = error.severity;
|
|
163
|
+
clone.querySelector('.help-text').textContent = error.help;
|
|
164
|
+
if (error.description) {
|
|
165
|
+
const desc = clone.querySelector('.desc-text');
|
|
166
|
+
desc.textContent = error.description;
|
|
167
|
+
desc.style.display = 'block';
|
|
168
|
+
}
|
|
169
|
+
clone.querySelector('.occ-count').textContent = error.total;
|
|
170
|
+
|
|
171
|
+
const grid = clone.querySelector('.instance-grid');
|
|
172
|
+
if (error.target && error.target.length > 0) {
|
|
173
|
+
error.target.forEach(t => {
|
|
174
|
+
if (t.screenshot) {
|
|
175
|
+
const iClone = tplInstance.content.cloneNode(true);
|
|
176
|
+
iClone.querySelector('img').src = t.screenshot;
|
|
177
|
+
iClone.querySelector('img').alt = `Violation on ${t.element}`;
|
|
178
|
+
const info = iClone.querySelector('.instance-info');
|
|
179
|
+
info.textContent = t.element;
|
|
180
|
+
info.title = t.element;
|
|
181
|
+
grid.appendChild(iClone);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
list.appendChild(clone);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// TEMPLATE 2: Global Execution Summary (Tabs, Charts, Tables)
|
|
192
|
+
// ============================================================================
|
|
193
|
+
function renderExecutionSummary(data) {
|
|
194
|
+
const root = document.getElementById('report-summary-root');
|
|
195
|
+
if (!root) return;
|
|
196
|
+
root.style.display = 'block';
|
|
197
|
+
|
|
198
|
+
// Hero Section
|
|
199
|
+
document.getElementById('summary-status-badge').classList.add(`status-${data.status}`);
|
|
200
|
+
document.getElementById('summary-status-icon').textContent = data.statusIcon;
|
|
201
|
+
document.getElementById('summary-status-text').textContent = data.status;
|
|
202
|
+
document.getElementById('summary-date').textContent = data.date;
|
|
203
|
+
document.getElementById('summary-duration').textContent = data.duration;
|
|
204
|
+
|
|
205
|
+
// Top Level Metrics
|
|
206
|
+
const setElText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
|
|
207
|
+
setElText('summary-total', data.total);
|
|
208
|
+
setElText('summary-passed', data.totalPassed);
|
|
209
|
+
setElText('summary-failed', data.totalFailed);
|
|
210
|
+
setElText('summary-flaky', data.totalFlaky);
|
|
211
|
+
setElText('summary-skipped', data.totalSkipped);
|
|
212
|
+
|
|
213
|
+
const browsers = Object.keys(data.browserSummaries);
|
|
214
|
+
|
|
215
|
+
const setElDisplay = (id, disp) => { const el = document.getElementById(id); if(el) el.style.display = disp; };
|
|
216
|
+
|
|
217
|
+
if (data.totalA11yErrorCount > 0) {
|
|
218
|
+
setElDisplay('summary-global-a11y-box', 'flex');
|
|
219
|
+
setElText('summary-a11y-total', data.totalA11yErrorCount);
|
|
220
|
+
setElDisplay('global-errors-container', 'block');
|
|
221
|
+
|
|
222
|
+
// Populate WCAG Error Cards
|
|
223
|
+
const wcagGrid = document.getElementById('global-metrics-grid');
|
|
224
|
+
const wcagTpl = document.getElementById('wcag-metric-template');
|
|
225
|
+
Object.entries(data.wcagErrors).sort((a, b) => b[1].count - a[1].count).forEach(([rule, info]) => {
|
|
226
|
+
const clone = wcagTpl.content.cloneNode(true);
|
|
227
|
+
const card = clone.querySelector('.metric-card');
|
|
228
|
+
card.classList.add(info.severity);
|
|
229
|
+
clone.querySelector('.metric-rule').textContent = rule;
|
|
230
|
+
const sev = clone.querySelector('.metric-sev');
|
|
231
|
+
sev.textContent = info.severity;
|
|
232
|
+
sev.classList.add(info.severity);
|
|
233
|
+
|
|
234
|
+
if (info.description) {
|
|
235
|
+
const desc = clone.querySelector('.metric-desc');
|
|
236
|
+
desc.textContent = info.description;
|
|
237
|
+
desc.style.display = 'block';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const count = clone.querySelector('.metric-count');
|
|
241
|
+
count.textContent = info.count;
|
|
242
|
+
count.classList.add(info.severity);
|
|
243
|
+
|
|
244
|
+
if (info.helpUrl) {
|
|
245
|
+
const btn = clone.querySelector('.btn-guide');
|
|
246
|
+
btn.href = info.helpUrl;
|
|
247
|
+
btn.style.display = 'inline-flex';
|
|
248
|
+
}
|
|
249
|
+
wcagGrid.appendChild(clone);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
} else {
|
|
253
|
+
const successCard = document.getElementById('global-success-card');
|
|
254
|
+
successCard.style.display = 'block'; // Container block, layout inside is flex
|
|
255
|
+
const icon = document.getElementById('global-success-icon');
|
|
256
|
+
const title = document.getElementById('global-success-title');
|
|
257
|
+
const desc = document.getElementById('global-success-desc');
|
|
258
|
+
|
|
259
|
+
if (data.status === 'failed') {
|
|
260
|
+
icon.textContent = 'report_off';
|
|
261
|
+
icon.style.color = 'rgba(255,255,255,0.8)';
|
|
262
|
+
title.textContent = 'Accessibility Checks Passed';
|
|
263
|
+
desc.textContent = 'No accessibility violations were detected. However, some functional tests failed. Please review the detailed logs below.';
|
|
264
|
+
successCard.style.background = 'linear-gradient(135deg, #475569 0%, #1e293b 100%)';
|
|
265
|
+
successCard.style.boxShadow = '0 20px 50px rgba(30, 41, 59, 0.2)';
|
|
266
|
+
} else {
|
|
267
|
+
icon.textContent = 'verified_user';
|
|
268
|
+
title.textContent = 'Compliance Verified';
|
|
269
|
+
desc.textContent = 'Congratulations! Your application passed all accessibility checks with flying colors. Your interface is inclusive and compliant.';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Browser Tabs rendering
|
|
274
|
+
const tabBtnContainer = document.getElementById('summary-tabs-container');
|
|
275
|
+
const tabBtnTpl = document.getElementById('browser-tab-btn-template');
|
|
276
|
+
|
|
277
|
+
const contentContainer = document.getElementById('browser-tabs-content');
|
|
278
|
+
const browserTpl = document.getElementById('browser-tab-content-template');
|
|
279
|
+
|
|
280
|
+
browsers.forEach(browser => {
|
|
281
|
+
// 1. Create Tab Button
|
|
282
|
+
const btnClone = tabBtnTpl.content.cloneNode(true);
|
|
283
|
+
const btn = btnClone.querySelector('.tab-btn');
|
|
284
|
+
const capBrowser = browser.charAt(0).toUpperCase() + browser.slice(1);
|
|
285
|
+
btn.textContent = capBrowser;
|
|
286
|
+
btn.setAttribute('data-target', browser);
|
|
287
|
+
tabBtnContainer.appendChild(btnClone);
|
|
288
|
+
|
|
289
|
+
// 2. Create Tab Content
|
|
290
|
+
const bStats = data.browserSummaries[browser];
|
|
291
|
+
const contentClone = browserTpl.content.cloneNode(true);
|
|
292
|
+
const tabDiv = contentClone.querySelector('.tab-content');
|
|
293
|
+
tabDiv.id = `tab-${browser}`;
|
|
294
|
+
|
|
295
|
+
if (bStats.totalA11yErrorCount > 0) {
|
|
296
|
+
contentClone.querySelector('.browser-errors-container').style.display = 'block';
|
|
297
|
+
contentClone.querySelector('.browser-chart-title').textContent = capBrowser;
|
|
298
|
+
contentClone.querySelector('.browser-chart-container').id = `chart-${browser}-violations`;
|
|
299
|
+
|
|
300
|
+
const bGrid = contentClone.querySelector('.browser-metrics-grid');
|
|
301
|
+
const wcagTpl = document.getElementById('wcag-metric-template');
|
|
302
|
+
|
|
303
|
+
Object.entries(bStats.wcagErrors).sort((a, b) => b[1].count - a[1].count).forEach(([rule, info]) => {
|
|
304
|
+
const mClone = wcagTpl.content.cloneNode(true);
|
|
305
|
+
const card = mClone.querySelector('.metric-card');
|
|
306
|
+
card.classList.add(info.severity);
|
|
307
|
+
mClone.querySelector('.metric-rule').textContent = rule;
|
|
308
|
+
const sev = mClone.querySelector('.metric-sev');
|
|
309
|
+
sev.textContent = info.severity;
|
|
310
|
+
sev.classList.add(info.severity);
|
|
311
|
+
|
|
312
|
+
if (info.description) {
|
|
313
|
+
const desc = mClone.querySelector('.metric-desc');
|
|
314
|
+
desc.textContent = info.description;
|
|
315
|
+
desc.style.display = 'block';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const count = mClone.querySelector('.metric-count');
|
|
319
|
+
count.textContent = info.count;
|
|
320
|
+
count.classList.add(info.severity);
|
|
321
|
+
bGrid.appendChild(mClone);
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
const sucContainer = contentClone.querySelector('.browser-success-container');
|
|
325
|
+
sucContainer.style.display = 'block'; // Container block, layout inside is flex
|
|
326
|
+
const bTitle = sucContainer.querySelector('.browser-success-title');
|
|
327
|
+
if (bTitle) bTitle.textContent = `${capBrowser} Compliant`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
contentContainer.appendChild(contentClone);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
// Test Suite Details Accordion
|
|
335
|
+
const accordionContainer = document.getElementById('accordion-container');
|
|
336
|
+
const groupTpl = document.getElementById('group-section-template');
|
|
337
|
+
const testTpl = document.getElementById('test-card-template');
|
|
338
|
+
|
|
339
|
+
Object.keys(data.groupedResults).forEach(groupKey => {
|
|
340
|
+
const groupClone = groupTpl.content.cloneNode(true);
|
|
341
|
+
const section = groupClone.querySelector('.group-section');
|
|
342
|
+
if (data.groupedResults[groupKey].length === 0) {
|
|
343
|
+
section.classList.add('hidden');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
groupClone.querySelector('.group-title').textContent = groupKey;
|
|
347
|
+
const gContent = groupClone.querySelector('.group-content');
|
|
348
|
+
|
|
349
|
+
data.groupedResults[groupKey].forEach(test => {
|
|
350
|
+
const tClone = testTpl.content.cloneNode(true);
|
|
351
|
+
const a = tClone.querySelector('a');
|
|
352
|
+
a.href = `./${test.executionReportPath}`;
|
|
353
|
+
a.setAttribute('data-browser', test.browser);
|
|
354
|
+
|
|
355
|
+
const icon = tClone.querySelector('.status-icon-small');
|
|
356
|
+
icon.classList.add(`status-icon-${test.status}`);
|
|
357
|
+
icon.textContent = test.statusIcon;
|
|
358
|
+
|
|
359
|
+
tClone.querySelector('.test-title').textContent = `${test.num}. ${test.title}`;
|
|
360
|
+
|
|
361
|
+
const bChip = tClone.querySelector('.browser-chip');
|
|
362
|
+
bChip.textContent = test.browser;
|
|
363
|
+
bChip.classList.add(`chip-${test.browser.toLowerCase()}`);
|
|
364
|
+
|
|
365
|
+
const badge = tClone.querySelector('.err-badge');
|
|
366
|
+
if (test.a11yErrorCount > 0) {
|
|
367
|
+
badge.style.display = 'inline-block';
|
|
368
|
+
badge.textContent = `${test.a11yErrorCount} errors`;
|
|
369
|
+
} else if (test.status === 'failed') {
|
|
370
|
+
badge.style.display = 'inline-block';
|
|
371
|
+
badge.textContent = 'Functional Error';
|
|
372
|
+
badge.style.border = '1px solid #fecdd3';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
tClone.querySelector('.test-dur').textContent = test.duration;
|
|
376
|
+
gContent.appendChild(tClone);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
accordionContainer.appendChild(groupClone);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Setup UI Interactions
|
|
383
|
+
setupTabs();
|
|
384
|
+
setupAccordions();
|
|
385
|
+
|
|
386
|
+
// Render Charts if ApexCharts is loaded
|
|
387
|
+
if (typeof ApexCharts !== 'undefined') {
|
|
388
|
+
if (data.totalA11yErrorCount > 0) {
|
|
389
|
+
renderBarChart('chart-global-violations', data.wcagErrors, data.colors);
|
|
390
|
+
}
|
|
391
|
+
browsers.forEach(browser => {
|
|
392
|
+
if (data.browserSummaries[browser].totalA11yErrorCount > 0) {
|
|
393
|
+
renderBarChart(`chart-${browser}-violations`, data.browserSummaries[browser].wcagErrors, data.colors);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================================================
|
|
400
|
+
// TEMPLATE 3: Single Page Accessibility Report
|
|
401
|
+
// ============================================================================
|
|
402
|
+
function renderAccessibilityReport(injectedData) {
|
|
403
|
+
const root = document.getElementById('accessibility-report-root');
|
|
404
|
+
if (!root) return;
|
|
405
|
+
root.style.display = 'block';
|
|
406
|
+
|
|
407
|
+
document.title = "Snap Ally - Accessibility Audit";
|
|
408
|
+
|
|
409
|
+
// Support nested data format from SnapAllyReporter
|
|
410
|
+
const data = injectedData.data || injectedData;
|
|
411
|
+
const pageUrl = data.pageUrl || data.pageKey || 'Resource';
|
|
412
|
+
const timestamp = data.timestamp || new Date().toLocaleString();
|
|
413
|
+
const violations = data.a11yErrors || data.errors || data.violations || [];
|
|
414
|
+
let failedCount = 0;
|
|
415
|
+
if (typeof data.a11yErrorCount !== 'undefined') {
|
|
416
|
+
failedCount = data.a11yErrorCount;
|
|
417
|
+
} else if (typeof data.failed !== 'undefined') {
|
|
418
|
+
failedCount = data.failed;
|
|
419
|
+
} else {
|
|
420
|
+
failedCount = violations.reduce((acc, v) => acc + (v.total || v.target?.length || v.nodes?.length || 0), 0);
|
|
421
|
+
}
|
|
422
|
+
const videoPath = data.video || data.videoPath || '';
|
|
423
|
+
|
|
424
|
+
// Hero Section
|
|
425
|
+
const pageUrlEl = document.getElementById('a11y-page-url');
|
|
426
|
+
if (pageUrlEl) pageUrlEl.textContent = pageUrl;
|
|
427
|
+
|
|
428
|
+
const timestampEl = document.getElementById('a11y-timestamp');
|
|
429
|
+
if (timestampEl) timestampEl.textContent = timestamp;
|
|
430
|
+
|
|
431
|
+
if (failedCount === 0) {
|
|
432
|
+
const passedPill = document.getElementById('a11y-pill-passed');
|
|
433
|
+
if (passedPill) passedPill.style.display = 'flex';
|
|
434
|
+
const successCard = document.getElementById('a11y-success-card');
|
|
435
|
+
if (successCard) successCard.style.display = 'flex';
|
|
436
|
+
} else {
|
|
437
|
+
const failedPill = document.getElementById('a11y-pill-failed');
|
|
438
|
+
if (failedPill) failedPill.style.display = 'flex';
|
|
439
|
+
const failedCountEl = document.getElementById('a11y-failed-count');
|
|
440
|
+
if (failedCountEl) failedCountEl.textContent = failedCount;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Render video section if exists
|
|
444
|
+
if (videoPath) {
|
|
445
|
+
document.getElementById('a11y-video-card').style.display = 'block';
|
|
446
|
+
const source = document.getElementById('a11y-video-source');
|
|
447
|
+
source.src = videoPath;
|
|
448
|
+
source.parentElement.load();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Iterate over violations
|
|
452
|
+
if (violations && violations.length > 0) {
|
|
453
|
+
const container = document.getElementById('a11y-violations-container');
|
|
454
|
+
const vTpl = document.getElementById('violation-card-template');
|
|
455
|
+
const bTpl = document.getElementById('bug-list-item-template');
|
|
456
|
+
|
|
457
|
+
violations.forEach(v => {
|
|
458
|
+
const impact = v.severity || v.impact;
|
|
459
|
+
const nodes = v.target || v.nodes || [];
|
|
460
|
+
|
|
461
|
+
const vClone = vTpl.content.cloneNode(true);
|
|
462
|
+
|
|
463
|
+
vClone.querySelector('.violation-title').textContent = v.id;
|
|
464
|
+
vClone.querySelector('.rule-name').textContent = v.wcagRule || (v.tags ? v.tags.join(', ') : '');
|
|
465
|
+
vClone.querySelector('.rule-wcag-tags').textContent = v.help;
|
|
466
|
+
|
|
467
|
+
const badge = vClone.querySelector('.severity-badge');
|
|
468
|
+
badge.classList.add(impact);
|
|
469
|
+
badge.textContent = impact;
|
|
470
|
+
|
|
471
|
+
vClone.querySelector('.fix-desc').textContent = v.description;
|
|
472
|
+
vClone.querySelector('.fix-link').href = v.helpUrl;
|
|
473
|
+
vClone.querySelector('.instances-count').textContent = nodes.length;
|
|
474
|
+
|
|
475
|
+
const instancesContainer = vClone.querySelector('.instances-container');
|
|
476
|
+
|
|
477
|
+
nodes.forEach((node, idx) => {
|
|
478
|
+
const bClone = bTpl.content.cloneNode(true);
|
|
479
|
+
const snippetText = node.snippet || node.friendlySnippet || node.element || node.target?.[0] || 'Unknown Node';
|
|
480
|
+
|
|
481
|
+
// Setup accordion IDs
|
|
482
|
+
const header = bClone.querySelector('.bug-item-header');
|
|
483
|
+
const collapseBody = bClone.querySelector('.collapse');
|
|
484
|
+
const vIdSafe = (v.id || 'err').replace(/[^a-zA-Z0-9]/g, '-');
|
|
485
|
+
const collapseId = `details-${vIdSafe}-${idx}`;
|
|
486
|
+
header.setAttribute('data-bs-target', `#${collapseId}`);
|
|
487
|
+
collapseBody.id = collapseId;
|
|
488
|
+
|
|
489
|
+
// Map data
|
|
490
|
+
bClone.querySelector('.bug-rule-name').textContent = v.help;
|
|
491
|
+
bClone.querySelector('.bug-snippet-text').textContent = snippetText;
|
|
492
|
+
|
|
493
|
+
const btn = bClone.querySelector('.btn-bug');
|
|
494
|
+
// generateAdoPayload binding
|
|
495
|
+
const safeSnippet = escapeHtml(snippetText);
|
|
496
|
+
const wcag = escapeHtml(v.wcagRule || (v.tags ? v.tags.join(', ') : ''));
|
|
497
|
+
btn.setAttribute('onclick', `event.preventDefault(); event.stopPropagation(); window.generateAdoPayload('${escapeHtml(v.id || 'Unknown ID')}', '${escapeHtml(v.help || 'No Help Provided')}', '${escapeHtml(node.failureSummary || '')}', '${escapeHtml(node.html || '')}', '${impact || 'unknown'}', '${escapeHtml(node.screenshotBase64 || node.screenshot || node.screenshotPath || '')}', '${escapeHtml(videoPath || '')}', '${safeSnippet}', '${wcag}')`);
|
|
498
|
+
|
|
499
|
+
const failSec = bClone.querySelector('.bug-failure-summary');
|
|
500
|
+
let stepsArray = node.steps || [];
|
|
501
|
+
if (typeof stepsArray === 'string') {
|
|
502
|
+
try { stepsArray = JSON.parse(stepsArray); } catch(e) { stepsArray = []; }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (stepsArray && stepsArray.length > 0) {
|
|
506
|
+
failSec.innerHTML = `<ol style="margin: 0; padding-left: 18px; line-height: 1.5;">${stepsArray.map(s => `<li>${escapeHtml(s)}</li>`).join('')}</ol>`;
|
|
507
|
+
failSec.style.fontFamily = 'Inter, sans-serif';
|
|
508
|
+
failSec.style.fontSize = '0.9rem';
|
|
509
|
+
failSec.style.color = 'var(--text-main)';
|
|
510
|
+
failSec.style.whiteSpace = 'normal';
|
|
511
|
+
} else if (node.failureSummary) {
|
|
512
|
+
failSec.textContent = node.failureSummary;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (node.screenshot || node.screenshotPath) {
|
|
516
|
+
const visSec = bClone.querySelector('.visual-evidence-section');
|
|
517
|
+
visSec.style.display = 'block';
|
|
518
|
+
bClone.querySelector('.bug-screenshot').src = node.screenshot || node.screenshotPath;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
instancesContainer.appendChild(bClone);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
container.appendChild(vClone);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ============================================================================
|
|
530
|
+
// Helper Utilities
|
|
531
|
+
// ============================================================================
|
|
532
|
+
|
|
533
|
+
function setupTabs() {
|
|
534
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
535
|
+
btn.addEventListener('click', (e) => {
|
|
536
|
+
const targetId = e.target.getAttribute('data-target');
|
|
537
|
+
|
|
538
|
+
// Update Tab Styles
|
|
539
|
+
document.querySelectorAll('.tab-btn').forEach(tb => tb.classList.remove('active'));
|
|
540
|
+
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
|
|
541
|
+
e.target.classList.add('active');
|
|
542
|
+
document.getElementById('tab-' + targetId).classList.add('active');
|
|
543
|
+
|
|
544
|
+
// Filter Accordion Cards
|
|
545
|
+
document.querySelectorAll('.test-card').forEach(card => {
|
|
546
|
+
if (targetId === 'global' || card.getAttribute('data-browser') === targetId) {
|
|
547
|
+
card.style.display = 'flex';
|
|
548
|
+
} else {
|
|
549
|
+
card.style.display = 'none';
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Filter empty accordions
|
|
554
|
+
document.querySelectorAll('.group-section').forEach(group => {
|
|
555
|
+
const hasVisible = Array.from(group.querySelectorAll('.test-card')).some(c => c.style.display !== 'none');
|
|
556
|
+
group.style.display = hasVisible ? 'block' : 'none';
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
window.dispatchEvent(new Event('resize'));
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function setupAccordions() {
|
|
565
|
+
document.querySelectorAll('.group-header').forEach(header => {
|
|
566
|
+
header.addEventListener('click', () => {
|
|
567
|
+
header.parentElement.classList.toggle('collapsed');
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function escapeHtml(unsafe) {
|
|
573
|
+
if (!unsafe) return '';
|
|
574
|
+
return String(unsafe)
|
|
575
|
+
.replace(/&/g, "&")
|
|
576
|
+
.replace(/</g, "<")
|
|
577
|
+
.replace(/>/g, ">")
|
|
578
|
+
.replace(/"/g, """)
|
|
579
|
+
.replace(/'/g, "'");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function formatDate(date) {
|
|
583
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
584
|
+
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
585
|
+
const month = monthNames[date.getMonth()];
|
|
586
|
+
const year = date.getFullYear();
|
|
587
|
+
let hours = date.getHours();
|
|
588
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
589
|
+
hours = hours % 12 || 12;
|
|
590
|
+
const formattedHours = String(hours).padStart(2, '0');
|
|
591
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
592
|
+
return `${day} ${month} ${year}, ${formattedHours}:${minutes} ${ampm}`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Chart Generation
|
|
596
|
+
function renderBarChart(elementId, wcagData, colors) {
|
|
597
|
+
const el = document.querySelector("#" + elementId);
|
|
598
|
+
if (!el) {
|
|
599
|
+
throw new Error('CRITICAL_NULL_ELEMENT_ID: ' + elementId);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const wcagEntries = Object.entries(wcagData).sort((a, b) => b[1].count - a[1].count);
|
|
603
|
+
if (wcagEntries.length === 0) return;
|
|
604
|
+
|
|
605
|
+
const chartColors = wcagEntries.map(e => colors[e[1].severity] || '#ef4444');
|
|
606
|
+
|
|
607
|
+
new ApexCharts(document.querySelector("#" + elementId), {
|
|
608
|
+
chart: { type: 'bar', height: 350, toolbar: { show: false } },
|
|
609
|
+
series: [{ name: 'Violations', data: wcagEntries.map(e => e[1].count) }],
|
|
610
|
+
xaxis: {
|
|
611
|
+
categories: wcagEntries.map(e => e[0]),
|
|
612
|
+
labels: { style: { fontFamily: 'Inter', fontSize: '14px', fontWeight: 700, colors: '#475569' } },
|
|
613
|
+
forceNiceScale: true,
|
|
614
|
+
decimalsInFloat: 0
|
|
615
|
+
},
|
|
616
|
+
yaxis: { labels: { style: { fontWeight: 600, fontSize: '15px' } } },
|
|
617
|
+
colors: chartColors,
|
|
618
|
+
plotOptions: {
|
|
619
|
+
bar: {
|
|
620
|
+
borderRadius: 6,
|
|
621
|
+
horizontal: true,
|
|
622
|
+
barHeight: '70%',
|
|
623
|
+
distributed: true,
|
|
624
|
+
dataLabels: { position: 'top' }
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
fill: { type: 'solid', opacity: 1 },
|
|
628
|
+
states: { hover: { filter: { type: 'darken', value: 0.9 } } },
|
|
629
|
+
grid: { borderColor: '#f1f5f9', padding: { right: 50, left: 10 } },
|
|
630
|
+
dataLabels: {
|
|
631
|
+
enabled: true,
|
|
632
|
+
textAnchor: 'start',
|
|
633
|
+
offsetX: 10,
|
|
634
|
+
style: { fontSize: '16px', fontWeight: 900, colors: ['#0f172a'] },
|
|
635
|
+
background: { enabled: false },
|
|
636
|
+
dropShadow: { enabled: false }
|
|
637
|
+
},
|
|
638
|
+
legend: { show: false },
|
|
639
|
+
tooltip: { theme: 'light', style: { fontFamily: 'Inter' } }
|
|
640
|
+
}).render();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ============================================================================
|
|
644
|
+
// ADO Integration
|
|
645
|
+
// ============================================================================
|
|
646
|
+
|
|
647
|
+
window.addEventListener('load', function () {
|
|
648
|
+
const tokenForm = document.getElementById('tokenForm');
|
|
649
|
+
if (tokenForm) {
|
|
650
|
+
tokenForm.addEventListener('submit', function (e) {
|
|
651
|
+
e.preventDefault();
|
|
652
|
+
const token = document.getElementById('tokenInput').value;
|
|
653
|
+
if (token) {
|
|
654
|
+
sessionStorage.setItem('userToken', token);
|
|
655
|
+
const modalEl = document.getElementById('tokenModal');
|
|
656
|
+
if (window.bootstrap && bootstrap.Modal) {
|
|
657
|
+
bootstrap.Modal.getInstance(modalEl).hide();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const confirmBugBtn = document.getElementById('confirmBugBtn');
|
|
664
|
+
if (confirmBugBtn) {
|
|
665
|
+
confirmBugBtn.addEventListener('click', async () => {
|
|
666
|
+
const btn = document.getElementById('confirmBugBtn');
|
|
667
|
+
const modalEl = document.getElementById('bugPreviewModal');
|
|
668
|
+
let modalInst = null;
|
|
669
|
+
if (window.bootstrap) {
|
|
670
|
+
modalInst = bootstrap.Modal.getInstance(modalEl);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
btn.disabled = true;
|
|
674
|
+
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Creating...';
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
await submitFinalBug();
|
|
678
|
+
if (modalInst) modalInst.hide();
|
|
679
|
+
} catch (err) {
|
|
680
|
+
console.error(err);
|
|
681
|
+
} finally {
|
|
682
|
+
btn.disabled = false;
|
|
683
|
+
btn.innerHTML = 'Create Bug';
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const bugModalEl = document.getElementById('bugPreviewModal');
|
|
689
|
+
if (bugModalEl && window.bootstrap) {
|
|
690
|
+
bugModalEl.addEventListener('shown.bs.modal', function () {
|
|
691
|
+
const input = document.getElementById('bugTitleInput');
|
|
692
|
+
if (input) {
|
|
693
|
+
input.focus();
|
|
694
|
+
input.select();
|
|
695
|
+
input.removeAttribute('readonly');
|
|
696
|
+
input.removeAttribute('disabled');
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
window.generateAdoPayload = function(axeId, help, failureSummary, htmlSnippet, severity, screenshotBase64, videoPath, snippet, wcag) {
|
|
703
|
+
const pat = sessionStorage.getItem('userToken');
|
|
704
|
+
if (!pat && window.bootstrap) {
|
|
705
|
+
const tokenModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('tokenModal'));
|
|
706
|
+
tokenModal.show();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
document.getElementById('bugTitleInput').value = `[A11y] ${help} (${snippet})`;
|
|
711
|
+
document.getElementById('bugSeverityInput').value = severity;
|
|
712
|
+
|
|
713
|
+
const data = window.snapAllyData || {};
|
|
714
|
+
const currentUrl = document.getElementById('bugUrlPreview');
|
|
715
|
+
if (currentUrl) currentUrl.textContent = data.pageUrl || data.pageKey || "Resource";
|
|
716
|
+
|
|
717
|
+
const stepsHtml = `
|
|
718
|
+
<div style="font-family: monospace; background: #fffcf0; padding: 12px; border: 1px solid #e2e8f0;">
|
|
719
|
+
${failureSummary ? failureSummary.replace(/\\n/g, '<br>') : 'Issue discovered via static analysis scans.'}
|
|
720
|
+
</div>
|
|
721
|
+
<br>
|
|
722
|
+
<div style="font-family: monospace; background: #fffcf0; padding: 12px; border: 1px solid #e2e8f0; overflow-x: auto;">
|
|
723
|
+
${htmlSnippet ? htmlSnippet.replace(/</g, '<').replace(/>/g, '>') : 'No DOM snippet available.'}
|
|
724
|
+
</div>
|
|
725
|
+
`;
|
|
726
|
+
|
|
727
|
+
document.getElementById('bugReproPreview').innerHTML = `
|
|
728
|
+
<div style="margin-bottom: 4px;"><b>Rule:</b> ${axeId} (${wcag})</div>
|
|
729
|
+
<div style="margin-bottom: 8px;"><b>Recommendation:</b> ${help}</div>
|
|
730
|
+
<div style="border-top: 1px solid #e2e8f0; margin: 8px 0; padding-top: 8px;"><b>Failure Summary:</b></div>
|
|
731
|
+
${stepsHtml}
|
|
732
|
+
`;
|
|
733
|
+
|
|
734
|
+
const screenshotPreview = document.getElementById('bugScreenshotPreview');
|
|
735
|
+
const screenshotThumbContainer = document.getElementById('screenshotThumbContainer');
|
|
736
|
+
if (screenshotBase64) {
|
|
737
|
+
if (screenshotBase64.startsWith('data:')) {
|
|
738
|
+
screenshotPreview.src = screenshotBase64;
|
|
739
|
+
} else if (screenshotBase64.startsWith('http') || screenshotBase64.startsWith('./') || screenshotBase64.includes('.')) {
|
|
740
|
+
screenshotPreview.src = screenshotBase64;
|
|
741
|
+
} else {
|
|
742
|
+
screenshotPreview.src = `data:image/png;base64,${screenshotBase64}`;
|
|
743
|
+
}
|
|
744
|
+
if (screenshotThumbContainer) screenshotThumbContainer.style.display = 'block';
|
|
745
|
+
} else {
|
|
746
|
+
if (screenshotThumbContainer) screenshotThumbContainer.style.display = 'none';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const videoThumbContainer = document.getElementById('videoThumbContainer');
|
|
750
|
+
const videoPreview = document.getElementById('bugVideoPreview');
|
|
751
|
+
if (videoPath) {
|
|
752
|
+
if (videoPreview) videoPreview.src = videoPath;
|
|
753
|
+
if (videoThumbContainer) videoThumbContainer.style.display = 'block';
|
|
754
|
+
} else {
|
|
755
|
+
if (videoThumbContainer) videoThumbContainer.style.display = 'none';
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
window.currentBugData = { axeId, wcag, help, stepsHtml, screenshotBase64, videoPath };
|
|
759
|
+
|
|
760
|
+
if (window.bootstrap) {
|
|
761
|
+
const modalElement = document.getElementById('bugPreviewModal');
|
|
762
|
+
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('bugPreviewModal'));
|
|
763
|
+
modal.show();
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
async function uploadAttachment(blob, name) {
|
|
768
|
+
const data = window.snapAllyData || {};
|
|
769
|
+
const org = data.adoOrganization;
|
|
770
|
+
const proj = data.adoProject;
|
|
771
|
+
const pat = sessionStorage.getItem('userToken');
|
|
772
|
+
if (!org || !proj || !pat) return null;
|
|
773
|
+
|
|
774
|
+
const url = `https://dev.azure.com/${org}/${proj}/_apis/wit/attachments?fileName=${name}&api-version=7.1`;
|
|
775
|
+
const res = await fetch(url, {
|
|
776
|
+
method: 'POST',
|
|
777
|
+
headers: { 'Content-Type': 'application/octet-stream', 'Authorization': `Basic ${btoa(':' + pat)}` },
|
|
778
|
+
body: blob
|
|
779
|
+
});
|
|
780
|
+
return res.ok ? (await res.json()).url : null;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function submitFinalBug() {
|
|
784
|
+
const data = window.snapAllyData || {};
|
|
785
|
+
const org = data.adoOrganization;
|
|
786
|
+
const proj = data.adoProject;
|
|
787
|
+
const pat = sessionStorage.getItem('userToken');
|
|
788
|
+
const { axeId, wcag, help, stepsHtml, screenshotBase64, videoPath } = window.currentBugData;
|
|
789
|
+
|
|
790
|
+
const title = document.getElementById('bugTitleInput').value;
|
|
791
|
+
const severity = document.getElementById('bugSeverityInput').value;
|
|
792
|
+
const area = document.getElementById('bugAreaInput').value || "Accessibility";
|
|
793
|
+
|
|
794
|
+
let screenshotUrl = null;
|
|
795
|
+
if (screenshotBase64) {
|
|
796
|
+
let screenshotBlob;
|
|
797
|
+
if (screenshotBase64.startsWith('data:')) {
|
|
798
|
+
screenshotBlob = await fetch(screenshotBase64).then(res => res.blob());
|
|
799
|
+
} else if (screenshotBase64.startsWith('http') || screenshotBase64.startsWith('./')) {
|
|
800
|
+
screenshotBlob = await fetch(screenshotBase64).then(res => res.blob());
|
|
801
|
+
} else {
|
|
802
|
+
screenshotBlob = await fetch(`data:image/png;base64,${screenshotBase64}`).then(res => res.blob());
|
|
803
|
+
}
|
|
804
|
+
screenshotUrl = await uploadAttachment(screenshotBlob, 'a11y-issue.png');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
let videoUrl = null;
|
|
808
|
+
if (videoPath) {
|
|
809
|
+
try {
|
|
810
|
+
const videoBlob = await fetch(videoPath).then(res => res.blob());
|
|
811
|
+
videoUrl = await uploadAttachment(videoBlob, 'session-recording.webm');
|
|
812
|
+
} catch (e) {
|
|
813
|
+
console.error("Failed to video upload", e);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const combinedReproHtml = `
|
|
818
|
+
<div style="margin-bottom: 12px;"><b>Rule:</b> ${axeId} (${wcag})</div>
|
|
819
|
+
<div style="margin-bottom: 12px;"><b>Recommendation:</b> ${help}</div>
|
|
820
|
+
<hr>
|
|
821
|
+
<div style="margin-bottom: 8px;"><b>Failure Trace & Details:</b></div>
|
|
822
|
+
${stepsHtml}
|
|
823
|
+
`;
|
|
824
|
+
|
|
825
|
+
const safePageKey = data.pageKey || "Unknown URL";
|
|
826
|
+
const priority = severity === 'critical' ? 1 : (severity === 'serious' ? 2 : 3);
|
|
827
|
+
|
|
828
|
+
const payload = [
|
|
829
|
+
{ op: "add", path: "/fields/System.Title", value: title },
|
|
830
|
+
{ op: "add", path: "/fields/Microsoft.VSTS.TCM.ReproSteps", value: combinedReproHtml },
|
|
831
|
+
{ op: "add", path: "/fields/System.Description", value: `Found at URL / Resource: <a href="${safePageKey}">${safePageKey}</a>` },
|
|
832
|
+
{ op: "add", path: "/fields/Microsoft.VSTS.Common.Priority", value: priority },
|
|
833
|
+
{ op: "add", path: "/fields/System.AreaPath", value: `${proj}\\\\${area}` },
|
|
834
|
+
{ op: "add", path: "/fields/System.Tags", value: "A11y;SnapAlly;UI-Test" }
|
|
835
|
+
];
|
|
836
|
+
|
|
837
|
+
if (screenshotUrl) {
|
|
838
|
+
payload.push({ op: "add", path: "/relations/-", value: { rel: "AttachedFile", url: screenshotUrl, attributes: { comment: "Accessibility Violation Screenshot" } } });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (videoUrl) {
|
|
842
|
+
payload.push({ op: "add", path: "/relations/-", value: { rel: "AttachedFile", url: videoUrl, attributes: { comment: "Audit Session Recording" } } });
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const res = await fetch(`https://dev.azure.com/${org}/${proj}/_apis/wit/workitems/$Bug?api-version=7.1`, {
|
|
846
|
+
method: 'POST',
|
|
847
|
+
headers: { 'Content-Type': 'application/json-patch+json', 'Authorization': `Basic ${btoa(':' + pat)}` },
|
|
848
|
+
body: JSON.stringify(payload)
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
if (res.ok) alert('Bug successfully filed in Azure DevOps!');
|
|
852
|
+
else {
|
|
853
|
+
const errorBody = await res.text();
|
|
854
|
+
console.error("ADO Bug Creation Error", errorBody);
|
|
855
|
+
alert('Failed to create bug. Check token and organization settings. Error: ' + errorBody.slice(0, 100));
|
|
856
|
+
}
|
|
857
|
+
}
|