Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions TUnit.Engine/Reporters/Html/HtmlReportDataModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ internal sealed class ReportSummary

[JsonPropertyName("timedOut")]
public int TimedOut { get; set; }

[JsonPropertyName("flaky")]
public int Flaky { get; set; }
}

internal sealed class ReportTestGroup
Expand Down
68 changes: 65 additions & 3 deletions TUnit.Engine/Reporters/Html/HtmlReportGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ private static void AppendBody(StringBuilder sb, ReportData data)

// Quick-access sections populated by JS
sb.AppendLine("<div id=\"failedSection\" role=\"region\" aria-label=\"Failed tests\"></div>");
sb.AppendLine("<div id=\"flakySection\" role=\"region\" aria-label=\"Flaky tests\"></div>");
sb.AppendLine("<div id=\"failureClusters\" role=\"region\" aria-label=\"Failure clusters\"></div>");
sb.AppendLine("<div id=\"slowestSection\" role=\"region\" aria-label=\"Slowest tests\"></div>");

Expand Down Expand Up @@ -256,6 +257,8 @@ private static void AppendSummaryDashboard(StringBuilder sb, ReportSummary summa
sb.AppendLine("<div id=\"durationHist\" class=\"dur-hist\"></div>");
sb.AppendLine("</div>");

sb.AppendLine("<div class=\"flaky-indicator\" id=\"flakyIndicator\"></div>");

sb.AppendLine("</section>");
}

Expand Down Expand Up @@ -306,7 +309,10 @@ private static void AppendSearchAndFilters(StringBuilder sb, ReportSummary summa
sb.Append(summary.Total);
sb.AppendLine("</span></button>");
sb.Append("<button class=\"pill\" data-filter=\"passed\" aria-pressed=\"false\"><span class=\"dot emerald\"></span>Passed <span class=\"pill-count\">");
sb.Append(summary.Passed);
sb.Append(summary.Passed - summary.Flaky);
sb.AppendLine("</span></button>");
sb.Append("<button class=\"pill hidden\" data-filter=\"flaky\" aria-pressed=\"false\" id=\"flakyPill\"><span class=\"dot orange\"></span>Flaky <span class=\"pill-count\" id=\"flakyPillCount\">");
sb.Append(summary.Flaky);
sb.AppendLine("</span></button>");
sb.Append("<button class=\"pill\" data-filter=\"failed\" aria-pressed=\"false\"><span class=\"dot rose\"></span>Failed <span class=\"pill-count\">");
sb.Append(summary.Failed + summary.TimedOut);
Expand Down Expand Up @@ -473,6 +479,8 @@ private static string GetCss()
--indigo: #818cf8;
--indigo-d: rgba(129,140,248,.10);
--violet: #a78bfa;
--orange: #fb923c;
--orange-d: rgba(251,146,60,.12);

--font: 'Segoe UI Variable','Segoe UI',-apple-system,BlinkMacSystemFont,system-ui,sans-serif;
--mono: 'Cascadia Code','JetBrains Mono','Fira Code','SF Mono',ui-monospace,monospace;
Expand All @@ -490,6 +498,7 @@ private static string GetCss()
--emerald-d:rgba(52,211,153,.15);--rose-d:rgba(251,113,133,.15);
--amber-d:rgba(251,191,36,.12);--slate-d:rgba(148,163,184,.12);
--indigo-d:rgba(129,140,248,.12);--violet:#7c3aed;
--orange-d:rgba(251,146,60,.15);
}
:root[data-theme="light"] .grain{opacity:.008}

Expand All @@ -512,6 +521,7 @@ private static string GetCss()
@property --amber-d { syntax:'<color>'; inherits:true; initial-value:rgba(251,191,36,.10) }
@property --slate-d { syntax:'<color>'; inherits:true; initial-value:rgba(148,163,184,.10) }
@property --indigo-d { syntax:'<color>'; inherits:true; initial-value:rgba(129,140,248,.10) }
@property --orange-d { syntax:'<color>'; inherits:true; initial-value:rgba(251,146,60,.12) }

:root {
transition:
Expand All @@ -520,7 +530,8 @@ private static string GetCss()
--border .3s var(--ease), --border-h .3s var(--ease),
--text .3s var(--ease), --text-2 .3s var(--ease), --text-3 .3s var(--ease),
--emerald-d .3s var(--ease), --rose-d .3s var(--ease), --amber-d .3s var(--ease),
--slate-d .3s var(--ease), --indigo-d .3s var(--ease);
--slate-d .3s var(--ease), --indigo-d .3s var(--ease),
--orange-d .3s var(--ease);
}
/* Suppress per-element transitions during theme switch so only the
@property variable interpolations drive the animation — no stagger. */
Expand Down Expand Up @@ -669,6 +680,8 @@ @keyframes ring-draw{
.dash-dur{text-align:center;padding:4px 20px;flex-shrink:0}
.dash-dur-val{display:block;font-size:1.5rem;font-weight:800;font-family:var(--mono);letter-spacing:-.02em}
.dash-dur-lbl{display:block;font-size:.68rem;color:var(--text-3);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}
.flaky-indicator{display:none;text-align:center;margin-top:6px;font-size:.78rem;font-weight:700;color:var(--orange)}
.flaky-indicator.visible{display:block}

/* ── Toolbar (search + pills) ──────────────────────── */
.bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:16px;justify-content:flex-end}
Expand Down Expand Up @@ -707,11 +720,13 @@ .search input{
}
.pill:hover{border-color:var(--border-h);color:var(--text)}
.pill.active{background:var(--indigo);border-color:var(--indigo);color:#fff}
.pill.hidden{display:none}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block}
.dot.emerald{background:var(--emerald)}
.dot.rose{background:var(--rose)}
.dot.amber{background:var(--amber)}
.dot.slate{background:var(--slate)}
.dot.orange{background:var(--orange)}
.bar-info{font-size:.8rem;color:var(--text-3);margin-left:auto}

/* Category filter pills — extends .pill with smaller sizing and violet accent */
Expand Down Expand Up @@ -787,6 +802,7 @@ .search input{
.t-badge.failed,.t-badge.error,.t-badge.timedOut{background:var(--rose-d);color:var(--rose);box-shadow:0 0 6px rgba(251,113,133,.15)}
.t-badge.skipped{background:var(--amber-d);color:var(--amber);box-shadow:0 0 6px rgba(251,191,36,.12)}
.t-badge.cancelled{background:var(--slate-d);color:var(--slate)}
.t-badge.flaky{background:var(--orange-d);color:var(--orange);box-shadow:0 0 6px rgba(251,146,60,.15)}
.t-badge.inProgress,.t-badge.unknown{background:var(--surface-2);color:var(--text-3)}
.t-name{flex:1;font-size:.88rem;word-break:break-word;color:var(--text)}
.t-dur{font-size:.78rem;color:var(--text-3);font-family:var(--mono);white-space:nowrap;font-variant-numeric:tabular-nums}
Expand Down Expand Up @@ -1379,9 +1395,14 @@ function buildCatPills(){
if (tag) suiteSpanByClass[tag.value] = s;
});

function isFlaky(t) { return t.status === 'passed' && t.retryAttempt > 0; }
function matchesFilter(t) {
if (activeFilter !== 'all') {
if (activeFilter === 'failed') {
if (activeFilter === 'flaky') {
if (!isFlaky(t)) return false;
} else if (activeFilter === 'passed') {
if (t.status !== 'passed' || isFlaky(t)) return false;
} else if (activeFilter === 'failed') {
if (t.status !== 'failed' && t.status !== 'error' && t.status !== 'timedOut') return false;
} else if (t.status !== activeFilter) return false;
}
Expand Down Expand Up @@ -1782,6 +1803,46 @@ function renderSlowestSection() {
sec.innerHTML = h;
}

function renderFlakySection() {
const sec = document.getElementById('flakySection');
if (!sec) return;
const flaky = [];
groups.forEach(function(g){
g.tests.forEach(function(t){
if (isFlaky(t)) flaky.push({t:t,cls:g.className});
});
});
// Update pill visibility and count
const pill = document.getElementById('flakyPill');
const pillCount = document.getElementById('flakyPillCount');
if (pill) { if (flaky.length) pill.classList.remove('hidden'); else pill.classList.add('hidden'); }
if (pillCount) pillCount.textContent = flaky.length;
// Update dashboard indicator
const indicator = document.getElementById('flakyIndicator');
if (indicator) {
if (flaky.length) {
indicator.textContent = flaky.length + ' flaky';
indicator.classList.add('visible');
} else {
indicator.classList.remove('visible');
}
}
if (!flaky.length) { sec.innerHTML=''; return; }
flaky.sort(function(a,b){ return b.t.retryAttempt - a.t.retryAttempt; });
let h = '<div class="qa-section"><div class="tl-toggle">'+tlArrow+' Flaky Tests ('+flaky.length+')</div><div class="tl-content"><div class="tl-content-inner"><div class="tl-content-pad">';
flaky.forEach(function(f){
h += '<div class="qa-item" data-scroll-tid="'+f.t.id+'">';
h += '<span class="t-badge flaky">flaky</span>';
h += '<div class="qa-info"><div class="qa-info-name">'+esc(f.t.displayName)+'</div>';
h += '<div class="qa-info-class">'+esc(f.cls)+'</div></div>';
h += '<span class="retry-tag">'+f.t.retryAttempt+' '+(f.t.retryAttempt===1?'retry':'retries')+'</span>';
h += '<span class="qa-dur">'+fmt(f.t.durationMs)+'</span>';
h += '</div>';
});
h += '</div></div></div></div>';
sec.innerHTML = h;
}

function sortGroups(grps) {
if (sortMode === 'duration') {
const maxDur = new Map(grps.map(g => [g, g.tests.length ? Math.max(...g.tests.map(t => t.durationMs)) : 0]));
Expand Down Expand Up @@ -2081,6 +2142,7 @@ function toggleCategory(cat){
if(catNames.length > 0) buildCatPills();
render();
renderFailedSection();
renderFlakySection();
renderFailureClusters();
renderSlowestSection();
checkHash();
Expand Down
10 changes: 7 additions & 3 deletions TUnit.Engine/Reporters/Html/HtmlReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ private ReportData BuildReportData()

var testResult = ExtractTestResult(kvp.Key, testNode, traceId, spanId, retryAttempt, additionalTraceIdsForResult);

AccumulateStatus(summary, testResult.Status);
AccumulateStatus(summary, testResult.Status, testResult.RetryAttempt);

// Group by class name
var className = testResult.ClassName;
Expand Down Expand Up @@ -327,7 +327,7 @@ private ReportData BuildReportData()
var groupSummary = new ReportSummary();
foreach (var test in kvp.Value)
{
AccumulateStatus(groupSummary, test.Status);
AccumulateStatus(groupSummary, test.Status, test.RetryAttempt);
}

groups[i++] = new ReportTestGroup
Expand Down Expand Up @@ -413,11 +413,15 @@ private static (string? CommitSha, string? Branch, string? PullRequestNumber, st
return (commitSha, branch, prNumber, repoSlug);
}

private static void AccumulateStatus(ReportSummary summary, string status)
private static void AccumulateStatus(ReportSummary summary, string status, int retryAttempt)
{
summary.Total++;
switch (status)
{
case "passed" when retryAttempt > 0:
summary.Passed++;
summary.Flaky++;
break;
case "passed":
summary.Passed++;
break;
Expand Down
Loading