Skip to content

Commit 33a7fd6

Browse files
authored
54 show only flaky tests and show tests that do not have links (#66)
* Show only skipped tests filter * Updated html to list untracked cases * Implement filter all flaky tests
1 parent 367a6e5 commit 33a7fd6

3 files changed

Lines changed: 188 additions & 40 deletions

File tree

.github/workflows/unit-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ jobs:
3333
3434
- name: 🧪 Run tests with coverage
3535
run: |
36-
poetry run pytest --cov=pytest_reporter_plus tests/ --cov-fail-under=82 --cov-report=term
36+
poetry run pytest --cov=pytest_reporter_plus tests/ --cov-fail-under=82 --cov-report=term --reruns 1

pytest_reporter_plus/generate_html_report.py

Lines changed: 177 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -167,30 +167,99 @@ def generate_html_report(self):
167167
168168
169169
function toggleFilter(longestCheckbox) {{
170-
const failedCheckbox = document.getElementById('failedOnlyCheckbox');
171-
const testsContainer = document.getElementById('tests-container');
172-
const testElements = Array.from(testsContainer.querySelectorAll('.test'));
170+
const failedCheckbox = document.getElementById('failedOnlyCheckbox');
171+
const skippedCheckbox = document.getElementById('skippedOnlyCheckbox');
172+
const untrackedCheckbox = document.getElementById('untrackedOnlyCheckbox');
173+
const flakyCheckbox = document.getElementById('flakyOnlyCheckbox');
174+
const testsContainer = document.getElementById('tests-container');
175+
const testElements = Array.from(testsContainer.querySelectorAll('.test'));
176+
177+
if (longestCheckbox.checked) {{
178+
failedCheckbox.checked = false;
179+
skippedCheckbox.checked = false;
180+
untrackedCheckbox.checked = false;
181+
flakyCheckbox.checked = false;
182+
// Re-enable all tests before sorting
183+
testElements.forEach(el => el.style.display = 'block');
184+
185+
// Sort and reorder
186+
testElements.sort((a, b) => {{
187+
const aDuration = parseFloat(
188+
a.querySelector('.timestamp').textContent.replace(/[^\d.]/g, '')
189+
) || 0;
190+
const bDuration = parseFloat(b.querySelector('.timestamp').textContent.replace(/[^\d.]/g, '')) || 0;
191+
return bDuration - aDuration;
192+
}});
193+
194+
testElements.forEach(el => testsContainer.appendChild(el));
195+
}}
196+
197+
// Reapply marker filter (should now handle failed/skipped too)
198+
filterByMarkers();
199+
}}
200+
201+
function toggleUntrackedOnly(checkbox) {{
202+
const testCards = document.querySelectorAll('.test-card');
203+
const longestCheckbox = document.getElementById('longestOnlyCheckbox');
204+
const skippedCheckbox = document.getElementById('skippedOnlyCheckbox');
205+
const failedCheckbox = document.getElementById('failedOnlyCheckbox');
206+
const flakyCheckbox = document.getElementById('flakyOnlyCheckbox');
207+
208+
if (checkbox.checked) {{
209+
longestCheckbox.checked = false;
210+
skippedCheckbox.checked = false;
211+
failedCheckbox.checked = false;
212+
flakyCheckbox.checked = false
213+
testCards.forEach(card => {{
214+
const hasLink = card.querySelector('a[href]');
215+
card.style.display = hasLink ? 'none' : 'block';
216+
}});
217+
}} else {{
218+
testCards.forEach(card => {{
219+
card.style.display = 'block';
220+
}});
221+
}}
222+
}}
223+
224+
function toggleUntrackedInfo() {{
225+
const card = document.getElementById('untrackedInfoCard');
226+
card.style.display = card.style.display === 'none' ? 'block' : 'none';
227+
}}
228+
229+
function toggleFlakyOnly(checkbox) {{
230+
const longestCheckbox = document.getElementById('longestOnlyCheckbox');
231+
const untrackedCheckbox = document.getElementById('untrackedOnlyCheckbox');
232+
const skippedCheckbox = document.getElementById('skippedOnlyCheckbox');
233+
const failedCheckbox = document.getElementById('failedOnlyCheckbox');
234+
const testElements = document.querySelectorAll('.test');
235+
if (checkbox.checked) {{
236+
longestCheckbox.checked = false;
237+
skippedCheckbox.checked = false;
238+
failedCheckbox.checked = false;
239+
untrackedCheckbox.checked = false;
240+
testElements.forEach(el => {{
241+
const isFlaky = el.querySelector('.is-flaky') !== null;
242+
el.style.display = isFlaky ? 'block' : 'none';
243+
}});
244+
}} else {{
245+
testElements.forEach(el => {{
246+
el.style.display = 'block';
247+
}});
248+
}}
249+
}}
173250
174-
if (longestCheckbox.checked) {{
175-
failedCheckbox.checked = false;
176-
testElements.forEach(el => el.style.display = 'block');
177-
testElements.sort((a, b) => {{
178-
const aDuration = parseFloat(a.querySelector('.timestamp').textContent.replace(/[^\d.]/g, '')) || 0;
179-
const bDuration = parseFloat(b.querySelector('.timestamp').textContent.replace(/[^\d.]/g, '')) || 0;
180-
return bDuration - aDuration;
181-
}});
182-
testElements.forEach(el => testsContainer.appendChild(el));
183-
}} else {{
184-
testElements.forEach(el => el.style.display = 'block');
185-
}}
186-
filterByMarkers(); // Reapply marker filter
187-
}}
188251
189252
function toggleFailedOnly(failedCheckbox) {{
190253
const longestCheckbox = document.getElementById('longestOnlyCheckbox');
254+
const untrackedCheckbox = document.getElementById('untrackedOnlyCheckbox');
255+
const skippedCheckbox = document.getElementById('skippedOnlyCheckbox');
256+
const flakyCheckbox = document.getElementById('flakyOnlyCheckbox');
191257
const testElements = document.querySelectorAll('.test');
192258
if (failedCheckbox.checked) {{
193259
longestCheckbox.checked = false;
260+
untrackedCheckbox.checked = false;
261+
skippedCheckbox.checked = false;
262+
flakyCheckbox.checked = false;
194263
testElements.forEach(el => {{
195264
const header = el.querySelector('.header');
196265
const isFailed = header.classList.contains('failed');
@@ -201,6 +270,30 @@ def generate_html_report(self):
201270
}}
202271
filterByMarkers(); // Reapply marker filter
203272
}}
273+
function toggleSkippedOnly(skippedCheckbox) {{
274+
const longestCheckbox = document.getElementById('longestOnlyCheckbox');
275+
const failedCheckbox = document.getElementById('failedOnlyCheckbox');
276+
const untrackedCheckbox = document.getElementById('untrackedOnlyCheckbox');
277+
const flakyCheckbox = document.getElementById('flakyOnlyCheckbox');
278+
const testElements = document.querySelectorAll('.test');
279+
280+
if (skippedCheckbox.checked) {{
281+
longestCheckbox.checked = false;
282+
failedCheckbox.checked = false;
283+
untrackedCheckbox.checked = false;
284+
flakyCheckbox.checked = false;
285+
testElements.forEach(el => {{
286+
const header = el.querySelector('.header');
287+
const isSkipped = header.classList.contains('skipped');
288+
el.style.display = isSkipped ? 'block' : 'none';
289+
}});
290+
}} else {{
291+
testElements.forEach(el => el.style.display = 'block');
292+
}}
293+
294+
filterByMarkers(); // Reapply marker filter
295+
}}
296+
204297
function initializeUniversalSearch() {{
205298
const searchInput = document.getElementById('universal-search');
206299
if (!searchInput) return;
@@ -218,28 +311,40 @@ def generate_html_report(self):
218311
219312
document.addEventListener('DOMContentLoaded', initializeUniversalSearch);
220313
221-
function filterByMarkers() {{
314+
function filterByMarkers() {{
222315
const selected = Array.from(document.querySelectorAll('.marker-filter input[type="checkbox"]:checked')).map(cb => cb.value);
223316
const failedOnly = document.getElementById('failedOnlyCheckbox').checked;
317+
const skippedOnly = document.getElementById('skippedOnlyCheckbox').checked;
318+
224319
document.querySelectorAll('.test').forEach(el => {{
320+
const header = el.querySelector('.header');
225321
const markers = el.getAttribute('data-markers').split(',');
226-
const isFailed = el.querySelector('.header').classList.contains('failed');
322+
const isFailed = header.classList.contains('failed');
323+
const isSkipped = header.classList.contains('skipped');
324+
227325
const showAllMarkers = selected.length === 0;
228326
const matchesMarker = showAllMarkers || selected.some(m => markers.includes(m));
229327
const matchesFailed = !failedOnly || isFailed;
230-
el.style.display = (matchesMarker && matchesFailed) ? 'block' : 'none';
328+
const matchesSkipped = !skippedOnly || isSkipped;
329+
330+
el.style.display = (matchesMarker && matchesFailed && matchesSkipped) ? 'block' : 'none';
231331
}});
232332
}}
233333
234334
235335
window.onload = function() {{
236336
const failedCheckbox = document.getElementById('failedOnlyCheckbox');
237337
const longestCheckbox = document.getElementById('longestOnlyCheckbox');
338+
const skippedCheckbox = document.getElementById('skippedOnlyCheckbox');
339+
const untrackedCheckbox = document.getElementById('untrackedOnlyCheckbox');
340+
const flakyCheckbox = document.getElementById('flakyOnlyCheckbox');
238341
failedCheckbox.checked = true;
239342
toggleFailedOnly(failedCheckbox);
240343
failedCheckbox.addEventListener('change', () => toggleFailedOnly(failedCheckbox));
241344
longestCheckbox.addEventListener('change', () => toggleFilter(longestCheckbox));
242-
345+
skippedCheckbox.addEventListener('change', () => toggleSkippedOnly(skippedCheckbox));
346+
untrackedCheckbox.addEventListener('change', () => toggleUntrackedOnly(untrackedCheckbox));
347+
flakyCheckbox.addEventListener('change', () => toggleFlakyOnly(flakyCheckbox));
243348
const markerCheckboxes = document.querySelectorAll('.marker-filter input[type="checkbox"]');
244349
markerCheckboxes.forEach(cb => cb.addEventListener('change', filterByMarkers));
245350
}};
@@ -251,10 +356,60 @@ def generate_html_report(self):
251356
<input type="checkbox" id="failedOnlyCheckbox" />
252357
Show only failed tests
253358
</label>
359+
<label>
360+
<input type="checkbox" id="skippedOnlyCheckbox" />
361+
Show only skipped tests
362+
</label>
254363
<label style="margin-left: 1rem;">
255364
<input type="checkbox" id="longestOnlyCheckbox" />
256365
Sort by longest running tests
257366
</label>
367+
<label style="margin-left: 1rem;">
368+
<input type="checkbox" id="untrackedOnlyCheckbox" />
369+
Show untracked
370+
</label>
371+
<span onclick="toggleUntrackedInfo()" style="
372+
display: inline-flex;
373+
justify-content: center;
374+
align-items: center;
375+
width: 18px;
376+
height: 18px;
377+
margin-left: 6px;
378+
background-color: #3498db;
379+
color: white;
380+
border-radius: 50%;
381+
font-size: 12px;
382+
font-weight: bold;
383+
cursor: pointer;
384+
user-select: none;
385+
" title="What is untracked?">i</span>
386+
387+
<div id="untrackedInfoCard" style="
388+
display: none;
389+
background: #f9f9f9;
390+
color: #333;
391+
border: 1px solid #ccc;
392+
border-radius: 6px;
393+
padding: 10px 14px;
394+
margin-top: 8px;
395+
max-width: 400px;
396+
font-size: 0.85em;
397+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
398+
">
399+
<strong>Untracked Tests:</strong><br>
400+
These tests do not have any associated tracking markers like:
401+
<ul style="margin: 6px 0 0 16px; padding: 0;">
402+
<li><code>pytest.mark.link("https://...")</code></li>
403+
<li><code>pytest.mark.jira("PROJ-123")</code></li>
404+
<li><code>pytest.mark.issue("https://...")</code></li>
405+
<li><code>pytest.mark.testcase("https://...")</code></li>
406+
</ul>
407+
Add these markers to your test to track them better
408+
</div>
409+
<label style="margin-left: 1rem;">
410+
<input type="checkbox" id="flakyOnlyCheckbox">
411+
Show flaky tests only
412+
</label>
258413
</div>
259414
<div class="search-container">
260415
<input
@@ -307,33 +462,16 @@ def generate_html_report(self):
307462
'border-radius:3px;font-weight:bold;font-size:0.85em;">FLAKY</span>'
308463
)
309464

465+
link_html = ""
310466
links = test.get("links", [])
311-
312467
if links:
313-
link_html = ""
314468
for url in links:
315469
link_html += (
316470
f'<a href="{url}" target="_blank" '
317471
f'style="background:#3498db;color:white;padding:2px 6px;'
318472
f'border-radius:3px;font-weight:bold;font-size:0.85em;'
319473
f'text-decoration:none;margin-right:6px;"> Link </a>'
320474
)
321-
else:
322-
link_html = """
323-
<button class="info-btn"
324-
title="You can tag your tests with markers such as pytest.mark.link. Other supported markers include testcase, jira, issue, and ticket."
325-
onclick="event.stopPropagation();"
326-
style="
327-
cursor: pointer;
328-
background: none;
329-
border: none;
330-
font-size: 1.2em;
331-
padding: 0;
332-
line-height: 1;
333-
">
334-
ℹ️
335-
</button>
336-
"""
337475

338476
html += f'''
339477

tests/unit/test_flaky_badge.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import random
2+
import pytest
3+
4+
attempt_counter = {"count": 0}
5+
6+
def test_flaky_network_call():
7+
attempt_counter["count"] += 1
8+
if attempt_counter["count"] == 1:
9+
assert False, "Simulated network failure"
10+
assert True

0 commit comments

Comments
 (0)