From 0cadd08c02c8c3b2396cadb003a7c32467133894 Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 12:22:46 +0530 Subject: [PATCH 01/16] unit test for timeline --- .../seleniumboot/unit/TimelineReportTest.java | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/test/java/com/seleniumboot/unit/TimelineReportTest.java diff --git a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java new file mode 100644 index 0000000..d7e7aed --- /dev/null +++ b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java @@ -0,0 +1,129 @@ +package com.seleniumboot.unit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.lang.reflect.Method; + +public class TimelineReportTest { + + private static String timeline(String json) throws Exception { + JsonNode tests = new ObjectMapper().readTree(json); + return timelineNode(tests); + } + + private static String timelineNode(JsonNode tests) throws Exception { + Method m = com.seleniumboot.reporting.HtmlReportGenerator.class + .getDeclaredMethod("buildTimelineSection", JsonNode.class); + m.setAccessible(true); + return (String) m.invoke(null, tests); + } + + private static int count(String haystack, String needle) { + int count = 0; + int from = 0; + while ((from = haystack.indexOf(needle, from)) >= 0) { + count++; + from += needle.length(); + } + return count; + } + + @Test + public void rendersOneLanePerThreadUsingRealTimestamps() throws Exception { + String html = timeline(""" + [ + {"testId":"com.x.A.fast","thread":"pool-1-thread-1","status":"PASSED", + "totalMs":100,"startTimeMs":1000,"endTimeMs":1100}, + {"testId":"com.x.A.slow","thread":"pool-1-thread-1","status":"FAILED", + "totalMs":400,"startTimeMs":1100,"endTimeMs":1500}, + {"testId":"com.x.B.other","thread":"pool-1-thread-2","status":"PASSED", + "totalMs":250,"startTimeMs":1050,"endTimeMs":1300} + ] + """); + + Assert.assertTrue(html.contains("timeline-card"), html); + Assert.assertTrue(html.contains("pool-1-thread-1"), html); + Assert.assertTrue(html.contains("pool-1-thread-2"), html); + Assert.assertEquals(count(html, "gantt-row gantt-lane\""), 2, html); + Assert.assertTrue(html.contains("gantt-bar-passed"), html); + Assert.assertTrue(html.contains("gantt-bar-failed"), html); + Assert.assertTrue(html.contains("left:"), html); + Assert.assertTrue(html.contains("width:"), html); + Assert.assertFalse(html.contains("timeline-note"), + "real timestamps must not trigger the estimate note: " + html); + } + + @Test + public void overlappingThreadsArePositionedOnTheSameAxis() throws Exception { + String html = timeline(""" + [ + {"testId":"a","thread":"t1","status":"PASSED","totalMs":100,"startTimeMs":1000,"endTimeMs":1100}, + {"testId":"b","thread":"t2","status":"PASSED","totalMs":100,"startTimeMs":1000,"endTimeMs":1100} + ] + """); + // Both start at the same instant, so both bars sit at the left edge. + Assert.assertEquals(count(html, "gantt-bar-passed\" style=\"left:0.000%"), 2, html); + } + + @Test + public void fallsBackToSequentialEstimateWithoutStartTimes() throws Exception { + String html = timeline(""" + [ + {"testId":"a","thread":"main","status":"PASSED","totalMs":100}, + {"testId":"b","thread":"main","status":"PASSED","totalMs":300} + ] + """); + + Assert.assertTrue(html.contains("timeline-note"), html); + Assert.assertTrue(html.contains("gantt-bar-passed"), html); + // a: 0-100 of a 400ms lane -> second bar starts at 25% + Assert.assertTrue(html.contains("left:25.000%"), html); + Assert.assertTrue(html.contains("width:75.000%"), html); + } + + @Test + public void handlesSingleThreadRun() throws Exception { + String html = timeline(""" + [ + {"testId":"a","thread":"main","status":"PASSED","totalMs":100,"startTimeMs":1000,"endTimeMs":1100}, + {"testId":"b","thread":"main","status":"PASSED","totalMs":200,"startTimeMs":1100,"endTimeMs":1300} + ] + """); + + Assert.assertEquals(count(html, "gantt-row gantt-lane\""), 1, html); + Assert.assertEquals(count(html, "gantt-bar "), 2, html); + Assert.assertFalse(html.contains("timeline-note"), html); + } + + @Test + public void emptyStateWhenNoTimingData() throws Exception { + String html = timeline(""" + [ + {"testId":"a","status":"PASSED","totalMs":0}, + {"testId":"b","thread":"main","status":"SKIPPED"} + ] + """); + + Assert.assertTrue(html.contains("timeline-empty"), html); + Assert.assertTrue(html.contains("No timing data available"), html); + Assert.assertFalse(html.contains("gantt-bar "), html); + } + + @Test + public void missingThreadUsesUnknownLane() throws Exception { + String html = timeline(""" + [{"testId":"a","status":"PASSED","totalMs":100,"startTimeMs":1000,"endTimeMs":1100}] + """); + + Assert.assertTrue(html.contains(">unknown<"), html); + } + + @Test + public void nullOrEmptyTestsRenderNothing() throws Exception { + Assert.assertEquals(timelineNode(null), ""); + Assert.assertEquals(timeline("[]"), ""); + } +} From 0415251f1bf880f9c6a4fedd3f1074423655240e Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 12:23:33 +0530 Subject: [PATCH 02/16] verify statTimeMs and endtimeMs --- .../unit/ExecutionMetricsTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java b/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java index 09c294b..54d2143 100644 --- a/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java +++ b/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java @@ -209,6 +209,39 @@ public void exportToJson_includesFlakyCount() throws IOException { assertTrue(json.contains("\"recoveredTests\""), "JSON must include top-level recoveredTests"); } + @Test + public void exportToJson_includesStartAndEndTimestamps() throws Exception { + ExecutionMetrics.markStart("timing-test"); + Thread.sleep(20); + ExecutionMetrics.markEnd("timing-test"); + ExecutionMetrics.recordStatus("timing-test", "PASSED"); + ExecutionMetrics.exportToJson(); + + com.fasterxml.jackson.databind.JsonNode test = new com.fasterxml.jackson.databind.ObjectMapper() + .readTree(new File("target/selenium-boot-metrics.json")) + .path("tests").get(0); + assertTrue(test.has("startTimeMs"), "per-test export must include startTimeMs"); + assertTrue(test.has("endTimeMs"), "per-test export must include endTimeMs"); + long start = test.get("startTimeMs").asLong(); + long end = test.get("endTimeMs").asLong(); + assertTrue(start > 0, "start timestamp must be an absolute epoch value"); + assertTrue(end >= start, "end must not precede start"); + assertTrue(end - start >= test.get("totalMs").asLong(), + "timestamp span must cover the reported duration"); + } + + @Test + public void exportToJson_omitsTimestampsWhenTestNeverStarted() throws Exception { + ExecutionMetrics.recordStatus("no-start", "SKIPPED"); + ExecutionMetrics.exportToJson(); + + com.fasterxml.jackson.databind.JsonNode test = new com.fasterxml.jackson.databind.ObjectMapper() + .readTree(new File("target/selenium-boot-metrics.json")) + .path("tests").get(0); + assertFalse(test.has("startTimeMs"), "unstarted test must omit startTimeMs"); + assertFalse(test.has("endTimeMs"), "unstarted test must omit endTimeMs"); + } + @Test public void exportToJson_suiteDurationIsWallClockNotSumOfTests() throws Exception { // two tests running in parallel for ~300ms: summed test time ~600ms, wall-clock ~300ms From 7970830c8a4b7ec4215e060891095a0405576d66 Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 12:24:15 +0530 Subject: [PATCH 03/16] Added BuildTimelinesection --- .../reporting/HtmlReportGenerator.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java index e9afb06..eacdc7d 100644 --- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java +++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java @@ -268,6 +268,7 @@ static String buildHtml(JsonNode root) { : ""; String retrySection = buildRetrySection(flakyTests, recoveredTests); String slowestSection = tests != null ? buildSlowestTests(tests) : ""; + String timelineSection = buildTimelineSection(tests); String tagResults = tests != null ? buildTagResultsSection(tests) : ""; String flakinessSection = buildFlakinessSection() + buildTestStatusSection(tests); String failureRows = buildFailureRows(tests, isMatrixRun); @@ -304,6 +305,7 @@ static String buildHtml(JsonNode root) { .replace("{{AI_RUN_SUMMARY}}", aiRunSummary) .replace("{{RETRY_SECTION}}", retrySection) .replace("{{SLOWEST_TESTS}}", slowestSection) + .replace("{{TIMELINE_SECTION}}", timelineSection) .replace("{{TAG_RESULTS}}", tagResults) .replace("{{FLAKINESS_SECTION}}", flakinessSection) .replace("{{DONUT_DATA}}", donutData) @@ -761,6 +763,189 @@ private static String buildSlowestTests(JsonNode tests) { return sb.toString(); } + /** Minimum bar width in percent, so very short tests stay visible. */ + private static final double MIN_TIMELINE_BAR_PCT = 0.4; + + private static final class TimelineEntry { + String testId; + String shortName; + String thread; + String status; + long start; + long end; + long duration; + } + + /** + * Gantt-style timeline: one lane per thread, one bar per test laid out by its real + * start/end times. When the metrics JSON only has durations (runs recorded before + * timestamps were exported), bars are packed sequentially per lane and the card says so. + * When no test has usable timing, an empty state is rendered instead. + */ + static String buildTimelineSection(JsonNode tests) { + if (tests == null || !tests.isArray() || tests.size() == 0) return ""; + + java.util.List entries = new java.util.ArrayList<>(); + for (JsonNode test : tests) { + long totalMs = test.has("totalMs") ? test.get("totalMs").asLong() : 0L; + long start = test.has("startTimeMs") ? test.get("startTimeMs").asLong() : 0L; + long end = test.has("endTimeMs") ? test.get("endTimeMs").asLong() : 0L; + long duration = (start > 0 && end >= start) ? end - start : Math.max(totalMs, 0L); + // A zero-length bar conveys nothing (and older/odd metrics can report start == end), + // so treat it as missing timing instead of drawing an invisible sliver. + if (duration <= 0) continue; + + TimelineEntry e = new TimelineEntry(); + String rawId = test.has("testId") ? test.get("testId").asText() : "unknown"; + int dot = rawId.lastIndexOf('.'); + e.testId = rawId; + e.shortName = dot >= 0 ? rawId.substring(dot + 1) : rawId; + String thread = test.hasNonNull("thread") ? test.get("thread").asText("") : ""; + e.thread = thread.isBlank() ? "unknown" : thread; + e.status = test.has("status") ? test.get("status").asText() : "UNKNOWN"; + e.duration = duration; + e.start = start; + e.end = start > 0 ? (end >= start ? end : start + duration) : 0L; + entries.add(e); + } + + if (entries.isEmpty()) return buildTimelineEmptyCard(); + + // Only use absolute positioning when every bar has a start time; otherwise the + // axis would mix real and synthetic offsets. + boolean absolute = entries.stream().allMatch(e -> e.start > 0); + + java.util.Map> lanes = + new java.util.TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (TimelineEntry e : entries) { + lanes.computeIfAbsent(e.thread, k -> new java.util.ArrayList<>()).add(e); + } + + if (absolute) { + for (java.util.List lane : lanes.values()) { + lane.sort(java.util.Comparator.comparingLong(e -> e.start)); + } + } else { + // No timestamps: lay tests out back-to-back inside each lane. + for (java.util.List lane : lanes.values()) { + lane.sort(java.util.Comparator.comparing(e -> e.testId)); + long cursor = 0; + for (TimelineEntry e : lane) { + e.start = cursor; + e.end = cursor + e.duration; + cursor = e.end; + } + } + } + + long t0 = Long.MAX_VALUE; + long t1 = Long.MIN_VALUE; + for (TimelineEntry e : entries) { + t0 = Math.min(t0, e.start); + t1 = Math.max(t1, e.end); + } + long span = Math.max(1L, t1 - t0); + + StringBuilder sb = new StringBuilder(); + sb.append("
\n"); + sb.append("
Timeline") + .append("") + .append("Passed") + .append("Failed") + .append("Skipped") + .append("
\n"); + sb.append("
\n"); + sb.append("
\n"); + + // Time axis (0..100% of the run span) + sb.append("
") + .append("
") + .append("
"); + for (int i = 0; i <= 4; i++) { + long offset = Math.round(span * i / 4.0); + sb.append("") + .append(escapeHtml(formatTimelineTick(offset))) + .append(""); + } + sb.append("
\n"); + + // One lane per thread + for (java.util.Map.Entry> lane : lanes.entrySet()) { + sb.append("
") + .append("
") + .append(escapeHtml(lane.getKey())).append("
") + .append("
"); + + for (TimelineEntry e : lane.getValue()) { + double leftPct = (e.start - t0) * 100.0 / span; + double widthPct = e.duration * 100.0 / span; + if (widthPct < MIN_TIMELINE_BAR_PCT) widthPct = MIN_TIMELINE_BAR_PCT; + if (leftPct < 0) leftPct = 0; + if (leftPct + widthPct > 100) leftPct = Math.max(0, 100 - widthPct); + + String title = escapeHtml(e.testId) + + " Thread: " + escapeHtml(e.thread) + + " Status: " + escapeHtml(e.status) + + " Duration: " + formatTimelineDuration(e.duration) + + (absolute ? " Started: +" + formatTimelineDuration(Math.max(0, e.start - t0)) : ""); + + sb.append("
") + .append("").append(escapeHtml(e.shortName)).append("") + .append("
"); + } + sb.append("
\n"); + } + + sb.append("
\n"); + if (!absolute) { + sb.append("
Start times were not recorded for this run, ") + .append("so bars show a sequential estimate per thread.
\n"); + } + sb.append("
\n
\n"); + return sb.toString(); + } + + private static String buildTimelineEmptyCard() { + return "
" + + "
Timeline
" + + "
" + + "No timing data available for this run.
\n"; + } + + private static String timelineStatusSlug(String status) { + if (status == null) return "unknown"; + switch (status.toUpperCase(java.util.Locale.ROOT)) { + case "PASSED": return "passed"; + case "FAILED": return "failed"; + case "SKIPPED": return "skipped"; + default: return "unknown"; + } + } + + private static String timelinePct(double value) { + return String.format(java.util.Locale.ROOT, "%.3f", Math.max(0, value)); + } + + private static String formatTimelineTick(long ms) { + if (ms <= 0) return "0s"; + if (ms < 1000) return ms + "ms"; + if (ms < 60_000) return String.format(java.util.Locale.ROOT, "%.1fs", ms / 1000.0); + long totalSeconds = ms / 1000; + return (totalSeconds / 60) + "m " + + String.format(java.util.Locale.ROOT, "%02d", totalSeconds % 60) + "s"; + } + + private static String formatTimelineDuration(long ms) { + if (ms < 1000) return ms + " ms"; + if (ms < 60_000) return String.format(java.util.Locale.ROOT, "%.1f s", ms / 1000.0); + long totalSeconds = ms / 1000; + return (totalSeconds / 60) + "m " + + String.format(java.util.Locale.ROOT, "%02d", totalSeconds % 60) + "s"; + } + private static String buildRecordingCell(String recordingPath) { File gif = new File(recordingPath); String label = gif.getName(); From 5fb2af7121c90c2c5a0276f69e7d0f91e733e78e Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 12:24:51 +0530 Subject: [PATCH 04/16] updated skeleton for timeline issue --- src/main/resources/report-template.html | 212 +++++++++++++++++++++++- src/main/resources/report/report.css | 50 ++++++ 2 files changed, 257 insertions(+), 5 deletions(-) diff --git a/src/main/resources/report-template.html b/src/main/resources/report-template.html index 463b9cd..83e2caa 100644 --- a/src/main/resources/report-template.html +++ b/src/main/resources/report-template.html @@ -4,16 +4,218 @@ Selenium Boot Report - - + + -{{REPORT_MARKUP}} + +
+
+

Selenium Boot Report

+

Execution Metrics & Performance Analysis

+
+ +
+ + + + +
+ + + + + +
+ + +
+
Dashboard
+ + {{RUN_SUMMARY}} + + +
+
+
{{PASSED}}
+
Passed
+
+
+
{{FAILED}}
+
Failed
+
+
+
{{SKIPPED}}
+
Skipped
+
+
+
{{TOTAL_TESTS}}
+
Total Tests
+
+
+
{{PASS_RATE}}%
+
Pass Rate
+
+
+
{{SUITE_DURATION}}
+
Suite Time (hh:mm:ss)
+
+
+ + {{FAILURES_SHORTCUT}} + + {{AI_RUN_SUMMARY}} + + +
+
+
Pass / Fail / Skip
+
+ +
+
+
+
Execution Time Percentiles (ms)
+
+
+
+ + + + + + {{TIMELINE_SECTION}} + + + {{RETRY_SECTION}} + + +
+
Slowest 5 Tests
+
{{SLOWEST_TESTS}}
+
+ + {{TAG_RESULTS}} + + + {{FLAKINESS_SECTION}} + + + {{METADATA}} + + +
+ + +
+
Test Cases
+
+
+ + + + + + +
+ 🔍 + +
+
+
+ + + + + + + + + + + + + + {{ROWS}} + +
TestDescriptionStatusLogic (ms)Total (ms)ScreenshotDetail
+
+
+
+ + +
+
Failures ({{FAILED}} failed)
+ {{FAILURE_GROUPS}} +
+
+ + + + + + + + + + + + + + {{FAILURE_ROWS}} + +
TestDescriptionStatusLogic (ms)Total (ms)ScreenshotDetail
+
+
+
+ +
+
+ diff --git a/src/main/resources/report/report.css b/src/main/resources/report/report.css index 4b36b0e..b93a136 100644 --- a/src/main/resources/report/report.css +++ b/src/main/resources/report/report.css @@ -164,6 +164,56 @@ .slow-bar { height: 8px; background: var(--primary); border-radius: 4px; } .slow-ms { font-size: 12px; color: var(--text-secondary); width: 70px; text-align: right; flex-shrink: 0; } + /* ── Timeline (Gantt) ── */ + .timeline-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; } + .timeline-legend { display: inline-flex; align-items: center; gap: 12px; font-size: 11px; font-weight: 500; color: var(--text-secondary); } + .tl-key { display: inline-flex; align-items: center; gap: 5px; } + .tl-key::before { content: ""; width: 10px; height: 10px; border-radius: 2px; display: inline-block; } + .tl-key-passed::before { background: var(--good); } + .tl-key-failed::before { background: var(--bad); } + .tl-key-skipped::before { background: var(--warn); } + .timeline-body { padding-top: 8px; } + .timeline-note { margin-top: 10px; font-size: 12px; font-style: italic; color: var(--text-secondary); } + .timeline-empty { font-size: 13px; color: var(--text-secondary); padding: 8px 0; } + .gantt-wrap { overflow-x: auto; } + .gantt { min-width: 640px; padding: 4px 0; } + .gantt-row { display: grid; grid-template-columns: 130px 1fr; align-items: center; } + .gantt-axis-row { margin-bottom: 6px; } + .gantt-lane-name { + font-size: 12px; color: var(--text-secondary); padding-right: 12px; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-family: 'Roboto Mono', monospace; + } + .gantt-track { + position: relative; height: 30px; + background-image: linear-gradient(to right, var(--border) 1px, transparent 1px); + background-size: 25% 100%; + } + .gantt-axis-track { height: 18px; background-image: none; } + .gantt-tick { + position: absolute; top: 0; transform: translateX(-50%); + font-size: 10px; color: var(--text-secondary); white-space: nowrap; + font-variant-numeric: tabular-nums; + } + .gantt-tick:first-child { transform: none; } + .gantt-tick:last-child { transform: translateX(-100%); } + .gantt-bar { + position: absolute; top: 50%; transform: translateY(-50%); + height: 18px; border-radius: 3px; overflow: hidden; min-width: 2px; + display: flex; align-items: center; cursor: default; + box-shadow: 0 1px 2px rgba(0,0,0,.15); + } + .gantt-bar:hover { filter: brightness(1.12); } + .gantt-bar-passed { background: var(--good); } + .gantt-bar-failed { background: var(--bad); } + .gantt-bar-skipped { background: var(--warn); } + .gantt-bar-unknown { background: var(--text-secondary); } + .gantt-bar-label { + font-size: 10px; color: #fff; padding: 0 6px; line-height: 1; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + pointer-events: none; + } + /* ── Layout helpers ── */ .section-mb { margin-bottom: 20px; } .retry-summary-row { margin-bottom: 0; } From 4c096724b2e2d9f78013b244be77c2a04fe4b8af Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 12:25:26 +0530 Subject: [PATCH 05/16] Added startime and endtime fields --- .../com/seleniumboot/metrics/TestTiming.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/seleniumboot/metrics/TestTiming.java b/src/main/java/com/seleniumboot/metrics/TestTiming.java index bcdd80d..2a5e0bf 100644 --- a/src/main/java/com/seleniumboot/metrics/TestTiming.java +++ b/src/main/java/com/seleniumboot/metrics/TestTiming.java @@ -5,6 +5,8 @@ public class TestTiming { private final String threadName; private long testExecutionTime; private long totalTime; + private long startTimeMs; + private long endTimeMs; private String status; private String screenshotPath; @@ -51,6 +53,24 @@ public void setTotalTime(long totalTime) { this.totalTime = totalTime; } + /** Wall-clock start of the test (epoch ms); 0 when not recorded. */ + public long getStartTimeMs() { + return startTimeMs; + } + + public void setStartTimeMs(long startTimeMs) { + this.startTimeMs = startTimeMs; + } + + /** Wall-clock end of the test (epoch ms); 0 when not recorded. */ + public long getEndTimeMs() { + return endTimeMs; + } + + public void setEndTimeMs(long endTimeMs) { + this.endTimeMs = endTimeMs; + } + public String getStatus() { return status; } From c47667bfa859a45b0f4022d5c19e2afa63c75b75 Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 12:27:07 +0530 Subject: [PATCH 06/16] Test --- .../com/seleniumboot/metrics/ExecutionMetrics.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/seleniumboot/metrics/ExecutionMetrics.java b/src/main/java/com/seleniumboot/metrics/ExecutionMetrics.java index c756668..ca19a7a 100644 --- a/src/main/java/com/seleniumboot/metrics/ExecutionMetrics.java +++ b/src/main/java/com/seleniumboot/metrics/ExecutionMetrics.java @@ -266,7 +266,8 @@ public static void markEnd(String testId) { Long start = START_TIMES.remove(testId); if (start == null) return; - long total = System.currentTimeMillis() - start; + long end = System.currentTimeMillis(); + long total = end - start; TestTiming timing = TIMINGS.computeIfAbsent( testId, @@ -274,6 +275,8 @@ public static void markEnd(String testId) { ); timing.setTotalTime(total); + timing.setStartTimeMs(start); + timing.setEndTimeMs(end); long logicTime = total - timing.getDriverStartupTime(); timing.setTestExecutionTime(Math.max(logicTime, 0)); @@ -455,6 +458,12 @@ public static void exportToJson() { testEntry.put("driverStartupMs", timing.getDriverStartupTime()); testEntry.put("testLogicMs", timing.getTestExecutionTime()); testEntry.put("totalMs", timing.getTotalTime()); + if (timing.getStartTimeMs() > 0) { + testEntry.put("startTimeMs", timing.getStartTimeMs()); + } + if (timing.getEndTimeMs() > 0) { + testEntry.put("endTimeMs", timing.getEndTimeMs()); + } if (timing.getScreenshotPath() != null) { testEntry.put("screenshotPath", timing.getScreenshotPath()); } From d5f3d003923c926d2382a30dd61b68bad793223e Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 13:10:36 +0530 Subject: [PATCH 07/16] Exported startedAtMs per test from start times --- .../metrics/ExecutionMetrics.java | 13 ++++-------- .../com/seleniumboot/metrics/TestTiming.java | 20 +++++-------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/seleniumboot/metrics/ExecutionMetrics.java b/src/main/java/com/seleniumboot/metrics/ExecutionMetrics.java index ca19a7a..f9fe6f1 100644 --- a/src/main/java/com/seleniumboot/metrics/ExecutionMetrics.java +++ b/src/main/java/com/seleniumboot/metrics/ExecutionMetrics.java @@ -266,8 +266,7 @@ public static void markEnd(String testId) { Long start = START_TIMES.remove(testId); if (start == null) return; - long end = System.currentTimeMillis(); - long total = end - start; + long total = System.currentTimeMillis() - start; TestTiming timing = TIMINGS.computeIfAbsent( testId, @@ -275,8 +274,7 @@ public static void markEnd(String testId) { ); timing.setTotalTime(total); - timing.setStartTimeMs(start); - timing.setEndTimeMs(end); + timing.setStartedAtMs(start); long logicTime = total - timing.getDriverStartupTime(); timing.setTestExecutionTime(Math.max(logicTime, 0)); @@ -458,11 +456,8 @@ public static void exportToJson() { testEntry.put("driverStartupMs", timing.getDriverStartupTime()); testEntry.put("testLogicMs", timing.getTestExecutionTime()); testEntry.put("totalMs", timing.getTotalTime()); - if (timing.getStartTimeMs() > 0) { - testEntry.put("startTimeMs", timing.getStartTimeMs()); - } - if (timing.getEndTimeMs() > 0) { - testEntry.put("endTimeMs", timing.getEndTimeMs()); + if (timing.getStartedAtMs() > 0) { + testEntry.put("startedAtMs", timing.getStartedAtMs()); } if (timing.getScreenshotPath() != null) { testEntry.put("screenshotPath", timing.getScreenshotPath()); diff --git a/src/main/java/com/seleniumboot/metrics/TestTiming.java b/src/main/java/com/seleniumboot/metrics/TestTiming.java index 2a5e0bf..94ef92f 100644 --- a/src/main/java/com/seleniumboot/metrics/TestTiming.java +++ b/src/main/java/com/seleniumboot/metrics/TestTiming.java @@ -5,8 +5,7 @@ public class TestTiming { private final String threadName; private long testExecutionTime; private long totalTime; - private long startTimeMs; - private long endTimeMs; + private long startedAtMs; private String status; private String screenshotPath; @@ -54,21 +53,12 @@ public void setTotalTime(long totalTime) { } /** Wall-clock start of the test (epoch ms); 0 when not recorded. */ - public long getStartTimeMs() { - return startTimeMs; + public long getStartedAtMs() { + return startedAtMs; } - public void setStartTimeMs(long startTimeMs) { - this.startTimeMs = startTimeMs; - } - - /** Wall-clock end of the test (epoch ms); 0 when not recorded. */ - public long getEndTimeMs() { - return endTimeMs; - } - - public void setEndTimeMs(long endTimeMs) { - this.endTimeMs = endTimeMs; + public void setStartedAtMs(long startedAtMs) { + this.startedAtMs = startedAtMs; } public String getStatus() { From 67e16d7fcb2be964779f838a692d35bd809aad90 Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 13:10:57 +0530 Subject: [PATCH 08/16] Gant chart Rendering --- .../reporting/HtmlReportGenerator.java | 59 ++++++------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java index eacdc7d..cc3f8f2 100644 --- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java +++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java @@ -777,23 +777,23 @@ private static final class TimelineEntry { } /** - * Gantt-style timeline: one lane per thread, one bar per test laid out by its real - * start/end times. When the metrics JSON only has durations (runs recorded before - * timestamps were exported), bars are packed sequentially per lane and the card says so. - * When no test has usable timing, an empty state is rendered instead. + * Gantt-style timeline: one lane per thread, one bar per test positioned by + * {@code startedAtMs} offset from the earliest start. {@code end = startedAtMs + totalMs}. + * The card is hidden entirely when {@code startedAtMs} is missing (e.g. old history files). + * Handles single-thread runs and degrades cleanly. */ static String buildTimelineSection(JsonNode tests) { if (tests == null || !tests.isArray() || tests.size() == 0) return ""; java.util.List entries = new java.util.ArrayList<>(); for (JsonNode test : tests) { + if (!test.has("startedAtMs") || test.get("startedAtMs").isNull()) { + return ""; + } + long startedAt = test.get("startedAtMs").asLong(); + if (startedAt <= 0) return ""; long totalMs = test.has("totalMs") ? test.get("totalMs").asLong() : 0L; - long start = test.has("startTimeMs") ? test.get("startTimeMs").asLong() : 0L; - long end = test.has("endTimeMs") ? test.get("endTimeMs").asLong() : 0L; - long duration = (start > 0 && end >= start) ? end - start : Math.max(totalMs, 0L); - // A zero-length bar conveys nothing (and older/odd metrics can report start == end), - // so treat it as missing timing instead of drawing an invisible sliver. - if (duration <= 0) continue; + if (totalMs <= 0) continue; TimelineEntry e = new TimelineEntry(); String rawId = test.has("testId") ? test.get("testId").asText() : "unknown"; @@ -803,39 +803,22 @@ static String buildTimelineSection(JsonNode tests) { String thread = test.hasNonNull("thread") ? test.get("thread").asText("") : ""; e.thread = thread.isBlank() ? "unknown" : thread; e.status = test.has("status") ? test.get("status").asText() : "UNKNOWN"; - e.duration = duration; - e.start = start; - e.end = start > 0 ? (end >= start ? end : start + duration) : 0L; + e.duration = totalMs; + e.start = startedAt; + e.end = startedAt + totalMs; entries.add(e); } - if (entries.isEmpty()) return buildTimelineEmptyCard(); - - // Only use absolute positioning when every bar has a start time; otherwise the - // axis would mix real and synthetic offsets. - boolean absolute = entries.stream().allMatch(e -> e.start > 0); + if (entries.isEmpty()) return ""; + // Group by thread name; one lane per thread java.util.Map> lanes = - new java.util.TreeMap<>(String.CASE_INSENSITIVE_ORDER); + new java.util.LinkedHashMap<>(); for (TimelineEntry e : entries) { lanes.computeIfAbsent(e.thread, k -> new java.util.ArrayList<>()).add(e); } - - if (absolute) { - for (java.util.List lane : lanes.values()) { - lane.sort(java.util.Comparator.comparingLong(e -> e.start)); - } - } else { - // No timestamps: lay tests out back-to-back inside each lane. - for (java.util.List lane : lanes.values()) { - lane.sort(java.util.Comparator.comparing(e -> e.testId)); - long cursor = 0; - for (TimelineEntry e : lane) { - e.start = cursor; - e.end = cursor + e.duration; - cursor = e.end; - } - } + for (java.util.List lane : lanes.values()) { + lane.sort(java.util.Comparator.comparingLong(e -> e.start)); } long t0 = Long.MAX_VALUE; @@ -887,7 +870,7 @@ static String buildTimelineSection(JsonNode tests) { + " Thread: " + escapeHtml(e.thread) + " Status: " + escapeHtml(e.status) + " Duration: " + formatTimelineDuration(e.duration) - + (absolute ? " Started: +" + formatTimelineDuration(Math.max(0, e.start - t0)) : ""); + + " Started: +" + formatTimelineDuration(Math.max(0, e.start - t0)); sb.append("
\n"); - if (!absolute) { - sb.append("
Start times were not recorded for this run, ") - .append("so bars show a sequential estimate per thread.
\n"); - } sb.append(" \n\n"); return sb.toString(); } From 11cb84fc03f30c214582f60315827b0b0ec82891 Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 13:11:26 +0530 Subject: [PATCH 09/16] Test (all test have been passed ) --- .../unit/ExecutionMetricsTest.java | 13 ++----- .../seleniumboot/unit/TimelineReportTest.java | 37 ++++++++----------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java b/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java index 54d2143..1d024c0 100644 --- a/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java +++ b/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java @@ -220,14 +220,10 @@ public void exportToJson_includesStartAndEndTimestamps() throws Exception { com.fasterxml.jackson.databind.JsonNode test = new com.fasterxml.jackson.databind.ObjectMapper() .readTree(new File("target/selenium-boot-metrics.json")) .path("tests").get(0); - assertTrue(test.has("startTimeMs"), "per-test export must include startTimeMs"); - assertTrue(test.has("endTimeMs"), "per-test export must include endTimeMs"); - long start = test.get("startTimeMs").asLong(); - long end = test.get("endTimeMs").asLong(); + assertTrue(test.has("startedAtMs"), "per-test export must include startedAtMs"); + long start = test.get("startedAtMs").asLong(); assertTrue(start > 0, "start timestamp must be an absolute epoch value"); - assertTrue(end >= start, "end must not precede start"); - assertTrue(end - start >= test.get("totalMs").asLong(), - "timestamp span must cover the reported duration"); + assertEquals(start + test.get("totalMs").asLong(), test.path("startedAtMs").asLong() + test.get("totalMs").asLong()); } @Test @@ -238,8 +234,7 @@ public void exportToJson_omitsTimestampsWhenTestNeverStarted() throws Exception com.fasterxml.jackson.databind.JsonNode test = new com.fasterxml.jackson.databind.ObjectMapper() .readTree(new File("target/selenium-boot-metrics.json")) .path("tests").get(0); - assertFalse(test.has("startTimeMs"), "unstarted test must omit startTimeMs"); - assertFalse(test.has("endTimeMs"), "unstarted test must omit endTimeMs"); + assertFalse(test.has("startedAtMs"), "unstarted test must omit startedAtMs"); } @Test diff --git a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java index d7e7aed..dc284e4 100644 --- a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java +++ b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java @@ -32,15 +32,15 @@ private static int count(String haystack, String needle) { } @Test - public void rendersOneLanePerThreadUsingRealTimestamps() throws Exception { + public void rendersOneLanePerThreadUsingStartedAtMs() throws Exception { String html = timeline(""" [ {"testId":"com.x.A.fast","thread":"pool-1-thread-1","status":"PASSED", - "totalMs":100,"startTimeMs":1000,"endTimeMs":1100}, + "totalMs":100,"startedAtMs":1000}, {"testId":"com.x.A.slow","thread":"pool-1-thread-1","status":"FAILED", - "totalMs":400,"startTimeMs":1100,"endTimeMs":1500}, + "totalMs":400,"startedAtMs":1100}, {"testId":"com.x.B.other","thread":"pool-1-thread-2","status":"PASSED", - "totalMs":250,"startTimeMs":1050,"endTimeMs":1300} + "totalMs":250,"startedAtMs":1050} ] """); @@ -52,16 +52,15 @@ public void rendersOneLanePerThreadUsingRealTimestamps() throws Exception { Assert.assertTrue(html.contains("gantt-bar-failed"), html); Assert.assertTrue(html.contains("left:"), html); Assert.assertTrue(html.contains("width:"), html); - Assert.assertFalse(html.contains("timeline-note"), - "real timestamps must not trigger the estimate note: " + html); + Assert.assertFalse(html.contains("timeline-note"), html); } @Test public void overlappingThreadsArePositionedOnTheSameAxis() throws Exception { String html = timeline(""" [ - {"testId":"a","thread":"t1","status":"PASSED","totalMs":100,"startTimeMs":1000,"endTimeMs":1100}, - {"testId":"b","thread":"t2","status":"PASSED","totalMs":100,"startTimeMs":1000,"endTimeMs":1100} + {"testId":"a","thread":"t1","status":"PASSED","totalMs":100,"startedAtMs":1000}, + {"testId":"b","thread":"t2","status":"PASSED","totalMs":100,"startedAtMs":1000} ] """); // Both start at the same instant, so both bars sit at the left edge. @@ -69,7 +68,7 @@ public void overlappingThreadsArePositionedOnTheSameAxis() throws Exception { } @Test - public void fallsBackToSequentialEstimateWithoutStartTimes() throws Exception { + public void oldHistoryWithoutStartedAtMs_hidesCard() throws Exception { String html = timeline(""" [ {"testId":"a","thread":"main","status":"PASSED","totalMs":100}, @@ -77,19 +76,15 @@ public void fallsBackToSequentialEstimateWithoutStartTimes() throws Exception { ] """); - Assert.assertTrue(html.contains("timeline-note"), html); - Assert.assertTrue(html.contains("gantt-bar-passed"), html); - // a: 0-100 of a 400ms lane -> second bar starts at 25% - Assert.assertTrue(html.contains("left:25.000%"), html); - Assert.assertTrue(html.contains("width:75.000%"), html); + Assert.assertEquals(html, ""); } @Test public void handlesSingleThreadRun() throws Exception { String html = timeline(""" [ - {"testId":"a","thread":"main","status":"PASSED","totalMs":100,"startTimeMs":1000,"endTimeMs":1100}, - {"testId":"b","thread":"main","status":"PASSED","totalMs":200,"startTimeMs":1100,"endTimeMs":1300} + {"testId":"a","thread":"main","status":"PASSED","totalMs":100,"startedAtMs":1000}, + {"testId":"b","thread":"main","status":"PASSED","totalMs":200,"startedAtMs":1100} ] """); @@ -102,20 +97,18 @@ public void handlesSingleThreadRun() throws Exception { public void emptyStateWhenNoTimingData() throws Exception { String html = timeline(""" [ - {"testId":"a","status":"PASSED","totalMs":0}, - {"testId":"b","thread":"main","status":"SKIPPED"} + {"testId":"a","status":"PASSED","totalMs":0,"startedAtMs":1000}, + {"testId":"b","thread":"main","status":"SKIPPED","startedAtMs":1000} ] """); - Assert.assertTrue(html.contains("timeline-empty"), html); - Assert.assertTrue(html.contains("No timing data available"), html); - Assert.assertFalse(html.contains("gantt-bar "), html); + Assert.assertEquals(html, ""); } @Test public void missingThreadUsesUnknownLane() throws Exception { String html = timeline(""" - [{"testId":"a","status":"PASSED","totalMs":100,"startTimeMs":1000,"endTimeMs":1100}] + [{"testId":"a","status":"PASSED","totalMs":100,"startedAtMs":1000}] """); Assert.assertTrue(html.contains(">unknown<"), html); From a92740a5c001dade87d6ede17835ad7e0813543b Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sat, 24 Oct 2026 19:43:52 +0530 Subject: [PATCH 10/16] Address maintainer feedback: timeline in dashboard partial, remove unused code, fix test assert --- .../com/seleniumboot/reporting/HtmlReportGenerator.java | 7 ------- src/main/resources/report/partials/dashboard.html | 2 ++ src/main/resources/report/report.css | 8 -------- .../java/com/seleniumboot/unit/ExecutionMetricsTest.java | 7 +++++-- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java index cc3f8f2..f9e509b 100644 --- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java +++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java @@ -887,13 +887,6 @@ static String buildTimelineSection(JsonNode tests) { return sb.toString(); } - private static String buildTimelineEmptyCard() { - return "
" - + "
Timeline
" - + "
" - + "No timing data available for this run.
\n"; - } - private static String timelineStatusSlug(String status) { if (status == null) return "unknown"; switch (status.toUpperCase(java.util.Locale.ROOT)) { diff --git a/src/main/resources/report/partials/dashboard.html b/src/main/resources/report/partials/dashboard.html index 356559c..6481345 100644 --- a/src/main/resources/report/partials/dashboard.html +++ b/src/main/resources/report/partials/dashboard.html @@ -65,6 +65,8 @@ + {{TIMELINE_SECTION}} + {{RETRY_SECTION}} diff --git a/src/main/resources/report/report.css b/src/main/resources/report/report.css index b93a136..d4d96bc 100644 --- a/src/main/resources/report/report.css +++ b/src/main/resources/report/report.css @@ -167,14 +167,6 @@ /* ── Timeline (Gantt) ── */ .timeline-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; } .timeline-legend { display: inline-flex; align-items: center; gap: 12px; font-size: 11px; font-weight: 500; color: var(--text-secondary); } - .tl-key { display: inline-flex; align-items: center; gap: 5px; } - .tl-key::before { content: ""; width: 10px; height: 10px; border-radius: 2px; display: inline-block; } - .tl-key-passed::before { background: var(--good); } - .tl-key-failed::before { background: var(--bad); } - .tl-key-skipped::before { background: var(--warn); } - .timeline-body { padding-top: 8px; } - .timeline-note { margin-top: 10px; font-size: 12px; font-style: italic; color: var(--text-secondary); } - .timeline-empty { font-size: 13px; color: var(--text-secondary); padding: 8px 0; } .gantt-wrap { overflow-x: auto; } .gantt { min-width: 640px; padding: 4px 0; } .gantt-row { display: grid; grid-template-columns: 130px 1fr; align-items: center; } diff --git a/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java b/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java index 1d024c0..f113e51 100644 --- a/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java +++ b/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java @@ -212,7 +212,7 @@ public void exportToJson_includesFlakyCount() throws IOException { @Test public void exportToJson_includesStartAndEndTimestamps() throws Exception { ExecutionMetrics.markStart("timing-test"); - Thread.sleep(20); + Thread.sleep(30); ExecutionMetrics.markEnd("timing-test"); ExecutionMetrics.recordStatus("timing-test", "PASSED"); ExecutionMetrics.exportToJson(); @@ -223,7 +223,10 @@ public void exportToJson_includesStartAndEndTimestamps() throws Exception { assertTrue(test.has("startedAtMs"), "per-test export must include startedAtMs"); long start = test.get("startedAtMs").asLong(); assertTrue(start > 0, "start timestamp must be an absolute epoch value"); - assertEquals(start + test.get("totalMs").asLong(), test.path("startedAtMs").asLong() + test.get("totalMs").asLong()); + long total = test.get("totalMs").asLong(); + assertTrue(total >= 0, "totalMs must be recorded"); + assertTrue(System.currentTimeMillis() >= start, "startedAtMs must be <= current time"); + assertTrue(total <= 10_000, "test duration should be reasonable for this test"); } @Test From d53c41d5029ac8519f1bda59750bd3a970691dcb Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sun, 25 Oct 2026 01:23:23 +0530 Subject: [PATCH 11/16] Testing if the cards are getting properly displayed --- .../seleniumboot/demo/TimelineDemoTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/test/java/com/seleniumboot/demo/TimelineDemoTest.java diff --git a/src/test/java/com/seleniumboot/demo/TimelineDemoTest.java b/src/test/java/com/seleniumboot/demo/TimelineDemoTest.java new file mode 100644 index 0000000..d8de9f5 --- /dev/null +++ b/src/test/java/com/seleniumboot/demo/TimelineDemoTest.java @@ -0,0 +1,24 @@ +package com.seleniumboot.demo; + +import com.seleniumboot.test.BaseTest; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertTrue; + +public class TimelineDemoTest extends BaseTest { + + private void run(long pauseMs) throws InterruptedException { + open(); + Thread.sleep(pauseMs); // only to give the demo varied durations + assertTrue(getDriver().getTitle().contains("Example Domain")); + } + + @Test public void fast1() throws Exception { run(500); } + @Test public void fast2() throws Exception { run(500); } + @Test public void medium1() throws Exception { run(2000); } + @Test public void medium2() throws Exception { run(2500); } + @Test public void slow1() throws Exception { run(5000); } + @Test public void slow2() throws Exception { run(6000); } + @Test public void queued1() throws Exception { run(1500); } + @Test public void queued2() throws Exception { run(1500); } +} \ No newline at end of file From e25a369ec5bef8ae808ad495e3037ce06ba052b4 Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Sun, 25 Oct 2026 19:33:33 +0530 Subject: [PATCH 12/16] Use master's report-template.html (partials structure) --- src/main/resources/report-template.html | 212 +----------------------- 1 file changed, 5 insertions(+), 207 deletions(-) diff --git a/src/main/resources/report-template.html b/src/main/resources/report-template.html index 83e2caa..463b9cd 100644 --- a/src/main/resources/report-template.html +++ b/src/main/resources/report-template.html @@ -4,218 +4,16 @@ Selenium Boot Report - - + + - -
-
-

Selenium Boot Report

-

Execution Metrics & Performance Analysis

-
- -
- - - - -
- - - - - -
- - -
-
Dashboard
- - {{RUN_SUMMARY}} - - -
-
-
{{PASSED}}
-
Passed
-
-
-
{{FAILED}}
-
Failed
-
-
-
{{SKIPPED}}
-
Skipped
-
-
-
{{TOTAL_TESTS}}
-
Total Tests
-
-
-
{{PASS_RATE}}%
-
Pass Rate
-
-
-
{{SUITE_DURATION}}
-
Suite Time (hh:mm:ss)
-
-
- - {{FAILURES_SHORTCUT}} - - {{AI_RUN_SUMMARY}} - - -
-
-
Pass / Fail / Skip
-
- -
-
-
-
Execution Time Percentiles (ms)
-
-
-
- - - - - - {{TIMELINE_SECTION}} - - - {{RETRY_SECTION}} - - -
-
Slowest 5 Tests
-
{{SLOWEST_TESTS}}
-
- - {{TAG_RESULTS}} - - - {{FLAKINESS_SECTION}} - - - {{METADATA}} - - -
- - -
-
Test Cases
-
-
- - - - - - -
- 🔍 - -
-
-
- - - - - - - - - - - - - - {{ROWS}} - -
TestDescriptionStatusLogic (ms)Total (ms)ScreenshotDetail
-
-
-
- - -
-
Failures ({{FAILED}} failed)
- {{FAILURE_GROUPS}} -
-
- - - - - - - - - - - - - - {{FAILURE_ROWS}} - -
TestDescriptionStatusLogic (ms)Total (ms)ScreenshotDetail
-
-
-
- -
-
- +{{REPORT_MARKUP}} From edb2f4b67fb079a8f6cd0e36219a3f8550226899 Mon Sep 17 00:00:00 2001 From: Mohammad Saddam Hossain Date: Mon, 26 Oct 2026 10:05:49 +0530 Subject: [PATCH 13/16] fix(report): skip tests without startedAtMs in timeline instead of hiding it A single never-started test (e.g. skipped by dependency) made buildTimelineSection return "", so any run with a skipped test lost the whole timeline. Skip such tests; hide the card only when none remain. Also tighten the duration assert and rename a misnamed test. Co-Authored-By: Claude Sonnet 5.5 --- .../reporting/HtmlReportGenerator.java | 10 ++++------ .../seleniumboot/unit/ExecutionMetricsTest.java | 2 +- .../com/seleniumboot/unit/TimelineReportTest.java | 14 +++++++++++++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java index f9e509b..241d2c2 100644 --- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java +++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java @@ -779,7 +779,7 @@ private static final class TimelineEntry { /** * Gantt-style timeline: one lane per thread, one bar per test positioned by * {@code startedAtMs} offset from the earliest start. {@code end = startedAtMs + totalMs}. - * The card is hidden entirely when {@code startedAtMs} is missing (e.g. old history files). + * Tests without {@code startedAtMs} are left out; the card is hidden when none have it (e.g. old history files). * Handles single-thread runs and degrades cleanly. */ static String buildTimelineSection(JsonNode tests) { @@ -787,11 +787,9 @@ static String buildTimelineSection(JsonNode tests) { java.util.List entries = new java.util.ArrayList<>(); for (JsonNode test : tests) { - if (!test.has("startedAtMs") || test.get("startedAtMs").isNull()) { - return ""; - } - long startedAt = test.get("startedAtMs").asLong(); - if (startedAt <= 0) return ""; + // Tests that never started (e.g. skipped by dependency) have no start time; leave them out + long startedAt = test.hasNonNull("startedAtMs") ? test.get("startedAtMs").asLong() : 0L; + if (startedAt <= 0) continue; long totalMs = test.has("totalMs") ? test.get("totalMs").asLong() : 0L; if (totalMs <= 0) continue; diff --git a/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java b/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java index f113e51..af0cf94 100644 --- a/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java +++ b/src/test/java/com/seleniumboot/unit/ExecutionMetricsTest.java @@ -224,7 +224,7 @@ public void exportToJson_includesStartAndEndTimestamps() throws Exception { long start = test.get("startedAtMs").asLong(); assertTrue(start > 0, "start timestamp must be an absolute epoch value"); long total = test.get("totalMs").asLong(); - assertTrue(total >= 0, "totalMs must be recorded"); + assertTrue(total >= 30, "totalMs must cover the 30 ms sleep"); assertTrue(System.currentTimeMillis() >= start, "startedAtMs must be <= current time"); assertTrue(total <= 10_000, "test duration should be reasonable for this test"); } diff --git a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java index dc284e4..5d33561 100644 --- a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java +++ b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java @@ -79,6 +79,18 @@ public void oldHistoryWithoutStartedAtMs_hidesCard() throws Exception { Assert.assertEquals(html, ""); } + @Test + public void testWithoutStartedAtMs_isSkippedButOthersStillRender() throws Exception { + String html = timeline(""" + [ + {"testId":"a","thread":"main","status":"PASSED","totalMs":100,"startedAtMs":1000}, + {"testId":"b","thread":"main","status":"SKIPPED"} + ] + """); + Assert.assertTrue(html.contains("timeline-card"), html); + Assert.assertEquals(count(html, "gantt-bar "), 1, html); + } + @Test public void handlesSingleThreadRun() throws Exception { String html = timeline(""" @@ -94,7 +106,7 @@ public void handlesSingleThreadRun() throws Exception { } @Test - public void emptyStateWhenNoTimingData() throws Exception { + public void hidesCardWhenNoTestHasDuration() throws Exception { String html = timeline(""" [ {"testId":"a","status":"PASSED","totalMs":0,"startedAtMs":1000}, From 1eb24eaaed8eb0f946773e4b440d1cec5a0d188b Mon Sep 17 00:00:00 2001 From: Danish-git123 Date: Mon, 26 Oct 2026 10:17:35 +0530 Subject: [PATCH 14/16] fix(report): skip tests without startedAtMs in timeline instead of hiding it --- .../reporting/HtmlReportGenerator.java | 12 ++++++++++++ .../com/seleniumboot/unit/TimelineReportTest.java | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java index 241d2c2..a6d39e2 100644 --- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java +++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java @@ -779,7 +779,12 @@ private static final class TimelineEntry { /** * Gantt-style timeline: one lane per thread, one bar per test positioned by * {@code startedAtMs} offset from the earliest start. {@code end = startedAtMs + totalMs}. +<<<<<<< HEAD * Tests without {@code startedAtMs} are left out; the card is hidden when none have it (e.g. old history files). +======= + * Tests without {@code startedAtMs} are skipped (e.g. skipped by dependency); the card is + * hidden only when no tests remain with usable timing data. +>>>>>>> 025c20a (fix(report): skip tests without startedAtMs in timeline instead of hiding it) * Handles single-thread runs and degrades cleanly. */ static String buildTimelineSection(JsonNode tests) { @@ -787,8 +792,15 @@ static String buildTimelineSection(JsonNode tests) { java.util.List entries = new java.util.ArrayList<>(); for (JsonNode test : tests) { +<<<<<<< HEAD // Tests that never started (e.g. skipped by dependency) have no start time; leave them out long startedAt = test.hasNonNull("startedAtMs") ? test.get("startedAtMs").asLong() : 0L; +======= + if (!test.has("startedAtMs") || test.get("startedAtMs").isNull()) { + continue; + } + long startedAt = test.get("startedAtMs").asLong(); +>>>>>>> 025c20a (fix(report): skip tests without startedAtMs in timeline instead of hiding it) if (startedAt <= 0) continue; long totalMs = test.has("totalMs") ? test.get("totalMs").asLong() : 0L; if (totalMs <= 0) continue; diff --git a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java index 5d33561..a0ff568 100644 --- a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java +++ b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java @@ -80,6 +80,7 @@ public void oldHistoryWithoutStartedAtMs_hidesCard() throws Exception { } @Test +<<<<<<< HEAD public void testWithoutStartedAtMs_isSkippedButOthersStillRender() throws Exception { String html = timeline(""" [ @@ -89,6 +90,20 @@ public void testWithoutStartedAtMs_isSkippedButOthersStillRender() throws Except """); Assert.assertTrue(html.contains("timeline-card"), html); Assert.assertEquals(count(html, "gantt-bar "), 1, html); +======= + public void skippedTestWithoutStartedAtMs_doesNotHideTimeline() throws Exception { + String html = timeline(""" + [ + {"testId":"com.x.A.ran","thread":"pool-1-thread-1","status":"PASSED","totalMs":100,"startedAtMs":1000}, + {"testId":"com.x.B.skipped","thread":"pool-1-thread-1","status":"SKIPPED","totalMs":0} + ] + """); + + Assert.assertTrue(html.contains("timeline-card"), html); + Assert.assertTrue(html.contains("ran"), html); + Assert.assertFalse(html.contains("skipped"), html); + Assert.assertEquals(count(html, "gantt-row gantt-lane\""), 1, html); +>>>>>>> 025c20a (fix(report): skip tests without startedAtMs in timeline instead of hiding it) } @Test From 44de3d2d088189bcdb794eee437b7daa47f642e7 Mon Sep 17 00:00:00 2001 From: Mohammad Saddam Hossain Date: Mon, 26 Oct 2026 10:41:53 +0530 Subject: [PATCH 15/16] fix(report): resolve leftover merge-conflict markers in timeline change The previous push committed unresolved conflict markers, so the branch did not compile. Keep one doc/comment, keep both skipped-test tests, and make the exclusion assert check the test id (the legend itself contains the word "skipped"). Co-Authored-By: Claude Sonnet 5.5 --- .../seleniumboot/reporting/HtmlReportGenerator.java | 12 ------------ .../com/seleniumboot/unit/TimelineReportTest.java | 8 ++++---- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java index a6d39e2..241d2c2 100644 --- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java +++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java @@ -779,12 +779,7 @@ private static final class TimelineEntry { /** * Gantt-style timeline: one lane per thread, one bar per test positioned by * {@code startedAtMs} offset from the earliest start. {@code end = startedAtMs + totalMs}. -<<<<<<< HEAD * Tests without {@code startedAtMs} are left out; the card is hidden when none have it (e.g. old history files). -======= - * Tests without {@code startedAtMs} are skipped (e.g. skipped by dependency); the card is - * hidden only when no tests remain with usable timing data. ->>>>>>> 025c20a (fix(report): skip tests without startedAtMs in timeline instead of hiding it) * Handles single-thread runs and degrades cleanly. */ static String buildTimelineSection(JsonNode tests) { @@ -792,15 +787,8 @@ static String buildTimelineSection(JsonNode tests) { java.util.List entries = new java.util.ArrayList<>(); for (JsonNode test : tests) { -<<<<<<< HEAD // Tests that never started (e.g. skipped by dependency) have no start time; leave them out long startedAt = test.hasNonNull("startedAtMs") ? test.get("startedAtMs").asLong() : 0L; -======= - if (!test.has("startedAtMs") || test.get("startedAtMs").isNull()) { - continue; - } - long startedAt = test.get("startedAtMs").asLong(); ->>>>>>> 025c20a (fix(report): skip tests without startedAtMs in timeline instead of hiding it) if (startedAt <= 0) continue; long totalMs = test.has("totalMs") ? test.get("totalMs").asLong() : 0L; if (totalMs <= 0) continue; diff --git a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java index a0ff568..c1a6d14 100644 --- a/src/test/java/com/seleniumboot/unit/TimelineReportTest.java +++ b/src/test/java/com/seleniumboot/unit/TimelineReportTest.java @@ -80,7 +80,6 @@ public void oldHistoryWithoutStartedAtMs_hidesCard() throws Exception { } @Test -<<<<<<< HEAD public void testWithoutStartedAtMs_isSkippedButOthersStillRender() throws Exception { String html = timeline(""" [ @@ -90,7 +89,9 @@ public void testWithoutStartedAtMs_isSkippedButOthersStillRender() throws Except """); Assert.assertTrue(html.contains("timeline-card"), html); Assert.assertEquals(count(html, "gantt-bar "), 1, html); -======= + } + + @Test public void skippedTestWithoutStartedAtMs_doesNotHideTimeline() throws Exception { String html = timeline(""" [ @@ -101,9 +102,8 @@ public void skippedTestWithoutStartedAtMs_doesNotHideTimeline() throws Exception Assert.assertTrue(html.contains("timeline-card"), html); Assert.assertTrue(html.contains("ran"), html); - Assert.assertFalse(html.contains("skipped"), html); + Assert.assertFalse(html.contains("com.x.B.skipped"), html); Assert.assertEquals(count(html, "gantt-row gantt-lane\""), 1, html); ->>>>>>> 025c20a (fix(report): skip tests without startedAtMs in timeline instead of hiding it) } @Test From 74f73310c9885ff26d7ef22bc0497ca96df6283a Mon Sep 17 00:00:00 2001 From: Mohammad Saddam Hossain Date: Mon, 26 Oct 2026 11:25:16 +0530 Subject: [PATCH 16/16] test: drop TimelineDemoTest from the default suite It drives a real browser against example.com (8 tests, up to 6s each) and made the CI "Run unit tests" step run for 45+ minutes. The timeline logic stays covered by TimelineReportTest and ExecutionMetricsTest. Co-Authored-By: Claude Sonnet 5.5 --- .../seleniumboot/demo/TimelineDemoTest.java | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/test/java/com/seleniumboot/demo/TimelineDemoTest.java diff --git a/src/test/java/com/seleniumboot/demo/TimelineDemoTest.java b/src/test/java/com/seleniumboot/demo/TimelineDemoTest.java deleted file mode 100644 index d8de9f5..0000000 --- a/src/test/java/com/seleniumboot/demo/TimelineDemoTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.seleniumboot.demo; - -import com.seleniumboot.test.BaseTest; -import org.testng.annotations.Test; - -import static org.testng.Assert.assertTrue; - -public class TimelineDemoTest extends BaseTest { - - private void run(long pauseMs) throws InterruptedException { - open(); - Thread.sleep(pauseMs); // only to give the demo varied durations - assertTrue(getDriver().getTitle().contains("Example Domain")); - } - - @Test public void fast1() throws Exception { run(500); } - @Test public void fast2() throws Exception { run(500); } - @Test public void medium1() throws Exception { run(2000); } - @Test public void medium2() throws Exception { run(2500); } - @Test public void slow1() throws Exception { run(5000); } - @Test public void slow2() throws Exception { run(6000); } - @Test public void queued1() throws Exception { run(1500); } - @Test public void queued2() throws Exception { run(1500); } -} \ No newline at end of file