@@ -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
219312document.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
0 commit comments