test-reporting 3.6.0__tar.gz → 3.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {test_reporting-3.6.0/test_reporting.egg-info → test_reporting-3.6.2}/PKG-INFO +1 -1
  2. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/config.py +1 -1
  3. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/publisher.py +15 -5
  4. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/templates/index.html +2 -2
  5. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/templates/project.html +117 -35
  6. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/templates/run.html +4 -2
  7. {test_reporting-3.6.0 → test_reporting-3.6.2}/setup.py +1 -1
  8. {test_reporting-3.6.0 → test_reporting-3.6.2/test_reporting.egg-info}/PKG-INFO +1 -1
  9. {test_reporting-3.6.0 → test_reporting-3.6.2}/LICENSE +0 -0
  10. {test_reporting-3.6.0 → test_reporting-3.6.2}/README.md +0 -0
  11. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/__init__.py +0 -0
  12. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/classifier.py +0 -0
  13. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/cli.py +0 -0
  14. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/plugin.py +0 -0
  15. {test_reporting-3.6.0 → test_reporting-3.6.2}/reporting/storage.py +0 -0
  16. {test_reporting-3.6.0 → test_reporting-3.6.2}/setup.cfg +0 -0
  17. {test_reporting-3.6.0 → test_reporting-3.6.2}/test_reporting.egg-info/SOURCES.txt +0 -0
  18. {test_reporting-3.6.0 → test_reporting-3.6.2}/test_reporting.egg-info/dependency_links.txt +0 -0
  19. {test_reporting-3.6.0 → test_reporting-3.6.2}/test_reporting.egg-info/entry_points.txt +0 -0
  20. {test_reporting-3.6.0 → test_reporting-3.6.2}/test_reporting.egg-info/requires.txt +0 -0
  21. {test_reporting-3.6.0 → test_reporting-3.6.2}/test_reporting.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: test-reporting
3
- Version: 3.6.0
3
+ Version: 3.6.2
4
4
  Summary: Multi-project test reporting dashboard — collect results locally or push to S3
5
5
  Home-page: https://github.com/amahdy77/test-reporting.git
6
6
  Author: Ashfaqur Mahdy
@@ -102,7 +102,7 @@ class ReportingConfig:
102
102
 
103
103
  @property
104
104
  def DASHBOARD_TITLE(self):
105
- return f"{self.PROJECT_NAME} - Test Dashboard"
105
+ return f"{self.PROJECT_NAME} - Test Result Dashboard"
106
106
 
107
107
  def ensure_directories(self):
108
108
  self.REPORTS_DIR.mkdir(exist_ok=True, parents=True)
@@ -493,8 +493,12 @@ class Publisher:
493
493
  # Regression grouping
494
494
  if tag not in regressions:
495
495
  regressions[tag] = {'tag': tag, 'date': tag[:10], 'suites': [],
496
- 'total_tests': 0, 'passed': 0, 'failed': 0, 'pass_rate': 0}
496
+ 'total_tests': 0, 'passed': 0, 'failed': 0, 'pass_rate': 0,
497
+ 'timestamp': ''}
497
498
  reg = regressions[tag]
499
+ # Track the latest timestamp for this regression
500
+ if ts > reg.get('timestamp', ''):
501
+ reg['timestamp'] = ts
498
502
  existing = next((s for s in reg['suites'] if s['suite'] == suite), None)
499
503
  if existing:
500
504
  if ts > existing.get('timestamp', ''):
@@ -525,8 +529,10 @@ class Publisher:
525
529
  s['runs'] = sorted(s['runs'], key=lambda r: r['timestamp'], reverse=True)
526
530
  s['runs'] = s['runs'][:self.config.DASHBOARD_MAX_RUNS_PER_SUITE]
527
531
 
528
- # Sort regressions newest first
529
- regressions = dict(sorted(regressions.items(), key=lambda x: x[0], reverse=True))
532
+ # Sort regressions by timestamp (newest first), fallback to tag name
533
+ regressions = dict(sorted(regressions.items(),
534
+ key=lambda x: (x[1].get('timestamp', ''), x[0]),
535
+ reverse=True))
530
536
 
531
537
  # Overall health from latest regression
532
538
  last_reg = next(iter(regressions.values()), None) if regressions else None
@@ -745,7 +751,9 @@ class Publisher:
745
751
  s3.put_object(Bucket=bucket, Key=dest_key, Body=f.read(),
746
752
  ContentType='image/png')
747
753
  print(f"[Reporting] ✓ Uploaded screenshot: {src.name}")
748
- test['screenshot_path'] = f"artifacts/{src.name}"
754
+ # Use full S3 URL so it works when dashboard is served from S3
755
+ s3_url = f"https://{bucket}.s3.{self.config.S3_REGION}.amazonaws.com/{dest_key}"
756
+ test['screenshot_path'] = s3_url
749
757
  artifact_count += 1
750
758
  else:
751
759
  print(f"[Reporting] ✗ Screenshot not found: {src}")
@@ -761,7 +769,9 @@ class Publisher:
761
769
  s3.put_object(Bucket=bucket, Key=dest_key, Body=f.read(),
762
770
  ContentType='application/zip')
763
771
  print(f"[Reporting] ✓ Uploaded trace: {src.name}")
764
- test['trace_path'] = f"artifacts/{src.name}"
772
+ # Use full S3 URL so it works when dashboard is served from S3
773
+ s3_url = f"https://{bucket}.s3.{self.config.S3_REGION}.amazonaws.com/{dest_key}"
774
+ test['trace_path'] = s3_url
765
775
  artifact_count += 1
766
776
  else:
767
777
  print(f"[Reporting] ✗ Trace not found: {src}")
@@ -6,7 +6,7 @@
6
6
  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
7
7
  <meta http-equiv="Pragma" content="no-cache" />
8
8
  <meta http-equiv="Expires" content="0" />
9
- <title>Test Dashboard</title>
9
+ <title>Test Result Dashboard</title>
10
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
11
11
  <style>
12
12
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -197,7 +197,7 @@
197
197
  <body>
198
198
 
199
199
  <div class="topbar" id="topbar">
200
- <div class="topbar-brand">Test <span>Dashboard</span></div>
200
+ <div class="topbar-brand">Test Result <span>Dashboard</span></div>
201
201
  <div class="topbar-right">
202
202
  <div class="topbar-meta" id="lastUpdated"></div>
203
203
  <button class="theme-toggle" id="themeToggle" title="Toggle dark mode">🌙</button>
@@ -6,7 +6,7 @@
6
6
  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
7
7
  <meta http-equiv="Pragma" content="no-cache" />
8
8
  <meta http-equiv="Expires" content="0" />
9
- <title>Project - Test Dashboard</title>
9
+ <title>Project - Test Result Dashboard</title>
10
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
11
11
  <style>
12
12
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -273,9 +273,9 @@
273
273
  .treemap { display: flex; flex-wrap: wrap; gap: 8px; }
274
274
  .treemap-cell { height: 80px; display: flex; flex-direction: column;
275
275
  justify-content: flex-end; padding: 8px;
276
- border-radius: 6px; cursor: default; position: relative;
277
- overflow: hidden; transition: transform .1s; }
278
- .treemap-cell:hover { transform: scale(1.02); z-index: 1; }
276
+ border-radius: 6px; cursor: pointer; position: relative;
277
+ overflow: hidden; transition: transform .1s, box-shadow .1s; }
278
+ .treemap-cell:hover { transform: scale(1.05); z-index: 1; box-shadow: 0 4px 12px rgba(0,0,0,.3); }
279
279
  .treemap-name { font-size: 11px; font-weight: 600; color: #fff; opacity: .9;
280
280
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
281
281
  .treemap-rate { font-size: 14px; font-weight: 800; color: #fff; }
@@ -286,6 +286,13 @@
286
286
  /* ── History table ── */
287
287
  .hist-table a { color: var(--blue); text-decoration: none; font-weight: 500; }
288
288
  .hist-table a:hover { text-decoration: underline; }
289
+ .hist-table th.sortable { cursor: pointer; user-select: none; position: relative; }
290
+ .hist-table th.sortable:hover { background: var(--bg); }
291
+ .hist-table th.sortable::after { content: '⇅'; margin-left: 6px; opacity: 0.3; font-size: 10px; }
292
+ .hist-table th.sortable.asc::after { content: '↑'; opacity: 1; }
293
+ .hist-table th.sortable.desc::after { content: '↓'; opacity: 1; }
294
+ .hist-table tbody tr { cursor: pointer; }
295
+ .hist-table tbody tr:hover { background: var(--blue-bg); }
289
296
 
290
297
  /* ── Sparkline cell ── */
291
298
  .sparkline-cell canvas { display: block; }
@@ -385,6 +392,14 @@
385
392
  return new Date(iso).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
386
393
  }
387
394
 
395
+ function fmtDateTime(iso) {
396
+ if (!iso) return '-';
397
+ const date = new Date(iso);
398
+ const dateStr = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
399
+ const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
400
+ return dateStr + ' ' + timeStr;
401
+ }
402
+
388
403
  function fmtDur(s) {
389
404
  if (!s || s === 0) return '-';
390
405
  if (s >= 60) return Math.floor(s / 60) + 'm ' + Math.floor(s % 60) + 's';
@@ -546,10 +561,12 @@
546
561
  }).join('');
547
562
 
548
563
  const isOpen = i === 0;
564
+ const regDateTime = reg.timestamp ? fmtDateTime(reg.timestamp) : '';
565
+ const regTitle = regDateTime ? reg.tag + ' · ' + regDateTime : reg.tag;
549
566
  return '<div class="regression">' +
550
567
  '<div class="reg-head" onclick="toggleReg(this)">' +
551
568
  '<div>' +
552
- '<div class="reg-title">' + reg.tag + '</div>' +
569
+ '<div class="reg-title">' + regTitle + '</div>' +
553
570
  '<div class="reg-sub">' + reg.suites.length + ' test ' + plural(reg.suites.length, 'file') + ' · ' + reg.total_tests + ' tests</div>' +
554
571
  '</div>' +
555
572
  '<div class="reg-right">' +
@@ -805,7 +822,8 @@
805
822
  const width = Math.round(80 + ratio * 160); // 80..240
806
823
  var bg = f.pass_rate >= 95 ? '#276749' : f.pass_rate >= 80 ? '#975a16' : '#9b2c2c';
807
824
  var bgLight = f.pass_rate >= 95 ? '#38a169' : f.pass_rate >= 80 ? '#d69e2e' : '#e53e3e';
808
- return '<div class="treemap-cell" style="width:' + width + 'px;background:' + bgLight + ';" title="' + f.file_name + ': ' + f.pass_rate.toFixed(1) + '% pass rate (' + f.total_runs + ' runs)">' +
825
+ const clickAttr = f.last_run_id ? ' onclick="goToRun(\'' + f.last_run_id + '\')"' : '';
826
+ return '<div class="treemap-cell" style="width:' + width + 'px;background:' + bgLight + ';" title="' + f.file_name + ': ' + f.pass_rate.toFixed(1) + '% pass rate (' + f.total_runs + ' runs)' + (f.last_run_id ? ' - Click to view latest run' : '') + '"' + clickAttr + '>' +
809
827
  '<div class="treemap-rate">' + f.pass_rate.toFixed(0) + '%</div>' +
810
828
  '<div class="treemap-name">' + f.file_name + '</div>' +
811
829
  '</div>';
@@ -895,6 +913,9 @@
895
913
  HISTORY TAB
896
914
  ═══════════════════════════════════════════════════ */
897
915
 
916
+ // Global state for history table sorting
917
+ var historyData = { runs: [], fileTrends: {}, sortColumn: 'timestamp', sortDir: 'desc' };
918
+
898
919
  function renderHistory(suites) {
899
920
  if (!suites || typeof suites !== 'object') return '<p class="empty-section">No run history yet.</p>';
900
921
  const names = Object.keys(suites);
@@ -917,12 +938,88 @@
917
938
 
918
939
  if (recentRuns.length === 0) return '<p class="empty-section">No runs yet.</p>';
919
940
 
941
+ // Store data globally for sorting
942
+ historyData.runs = recentRuns;
943
+ historyData.fileTrends = fileTrends;
944
+ historyData.sortColumn = 'timestamp';
945
+ historyData.sortDir = 'desc';
946
+
947
+ return '<div class="section">' +
948
+ '<div class="section-head">' +
949
+ '<h3>All Test File Runs</h3>' +
950
+ '<span style="font-size:13px;color:var(--muted)">' + recentRuns.length + ' recent runs across ' + names.length + ' test ' + plural(names.length, 'file') + '</span>' +
951
+ '</div>' +
952
+ '<div class="section-body-flush">' +
953
+ '<table class="hist-table" id="historyTable">' +
954
+ '<thead><tr>' +
955
+ '<th class="sortable desc" style="width:90px" onclick="sortHistory(\'timestamp\')">Date</th>' +
956
+ '<th style="width:60px">Time</th>' +
957
+ '<th class="sortable" onclick="sortHistory(\'file_name\')">Test File</th>' +
958
+ '<th class="sortable" style="width:100px" onclick="sortHistory(\'regression_tag\')">Tag</th>' +
959
+ '<th class="sortable" style="width:80px" onclick="sortHistory(\'pass_rate\')">Pass Rate</th>' +
960
+ '<th class="sortable" style="width:60px" onclick="sortHistory(\'total_tests\')">Tests</th>' +
961
+ '<th class="sortable" style="width:60px" onclick="sortHistory(\'failed\')">Failed</th>' +
962
+ '<th class="sortable" style="width:80px" onclick="sortHistory(\'duration_seconds\')">Duration</th>' +
963
+ '<th style="width:90px">Trend</th>' +
964
+ '</tr></thead>' +
965
+ '<tbody id="historyTableBody"></tbody>' +
966
+ '</table>' +
967
+ '</div>' +
968
+ '</div>';
969
+ }
970
+
971
+ function sortHistory(column) {
972
+ // Toggle direction if same column, otherwise default to desc
973
+ if (historyData.sortColumn === column) {
974
+ historyData.sortDir = historyData.sortDir === 'asc' ? 'desc' : 'asc';
975
+ } else {
976
+ historyData.sortColumn = column;
977
+ historyData.sortDir = column === 'timestamp' ? 'desc' : 'asc';
978
+ }
979
+
980
+ // Sort the data
981
+ historyData.runs.sort(function(a, b) {
982
+ var aVal = a[column];
983
+ var bVal = b[column];
984
+
985
+ // Handle null/undefined
986
+ if (aVal == null) aVal = '';
987
+ if (bVal == null) bVal = '';
988
+
989
+ var cmp = 0;
990
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
991
+ cmp = aVal - bVal;
992
+ } else {
993
+ cmp = String(aVal).localeCompare(String(bVal));
994
+ }
995
+
996
+ return historyData.sortDir === 'asc' ? cmp : -cmp;
997
+ });
998
+
999
+ // Update header classes
1000
+ var headers = document.querySelectorAll('#historyTable th.sortable');
1001
+ headers.forEach(function(th) {
1002
+ th.classList.remove('asc', 'desc');
1003
+ });
1004
+ var activeHeader = Array.from(headers).find(function(th) {
1005
+ return th.textContent.toLowerCase().includes(column.replace('_', ' '));
1006
+ });
1007
+ if (activeHeader) {
1008
+ activeHeader.classList.add(historyData.sortDir);
1009
+ }
1010
+
1011
+ // Re-render table body
1012
+ renderHistoryTableBody();
1013
+ }
1014
+
1015
+ function renderHistoryTableBody() {
920
1016
  const sparklineIds = {};
921
- const rows = recentRuns.map(function(run, i) {
1017
+ const rows = historyData.runs.map(function(run, i) {
922
1018
  const pc = pillClass(run.pass_rate);
923
1019
  const sparkId = 'hist-spark-' + i;
924
- sparklineIds[sparkId] = fileTrends[run.file_name] || [];
925
- return '<tr>' +
1020
+ const runUrl = 'run.html?r=' + encodeURIComponent(run.run_id) + '&p=' + encodeURIComponent(projectName);
1021
+ sparklineIds[sparkId] = historyData.fileTrends[run.file_name] || [];
1022
+ return '<tr onclick="window.location.href=\'' + runUrl + '\'">' +
926
1023
  '<td style="font-size:12px">' + fmtDate(run.timestamp) + '</td>' +
927
1024
  '<td style="font-size:12px;color:var(--muted)">' + fmtTime(run.timestamp) + '</td>' +
928
1025
  '<td><div style="font-weight:600;font-size:13px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + run.file_name + '">' + run.file_name + '</div></td>' +
@@ -932,10 +1029,12 @@
932
1029
  '<td style="color:var(--red);text-align:center">' + run.failed + '</td>' +
933
1030
  '<td style="color:var(--muted);font-size:12px">' + fmtDur(run.duration_seconds) + '</td>' +
934
1031
  '<td class="sparkline-cell"><canvas id="' + sparkId + '" width="80" height="30"></canvas></td>' +
935
- '<td style="text-align:center"><a href="run.html?r=' + encodeURIComponent(run.run_id) + '&p=' + encodeURIComponent(projectName) + '" style="white-space:nowrap">View →</a></td>' +
936
1032
  '</tr>';
937
1033
  }).join('');
938
1034
 
1035
+ document.getElementById('historyTableBody').innerHTML = rows;
1036
+
1037
+ // Draw sparklines after DOM update
939
1038
  setTimeout(function() {
940
1039
  for (var sid in sparklineIds) {
941
1040
  var canvas = document.getElementById(sid);
@@ -956,30 +1055,6 @@
956
1055
  });
957
1056
  }
958
1057
  }, 60);
959
-
960
- return '<div class="section">' +
961
- '<div class="section-head">' +
962
- '<h3>All Test File Runs</h3>' +
963
- '<span style="font-size:13px;color:var(--muted)">' + recentRuns.length + ' recent runs across ' + names.length + ' test ' + plural(names.length, 'file') + '</span>' +
964
- '</div>' +
965
- '<div class="section-body-flush">' +
966
- '<table class="hist-table">' +
967
- '<thead><tr>' +
968
- '<th style="width:90px">Date</th>' +
969
- '<th style="width:60px">Time</th>' +
970
- '<th>Test File</th>' +
971
- '<th style="width:100px">Tag</th>' +
972
- '<th style="width:80px">Pass Rate</th>' +
973
- '<th style="width:60px">Tests</th>' +
974
- '<th style="width:60px">Failed</th>' +
975
- '<th style="width:80px">Duration</th>' +
976
- '<th style="width:90px">Trend</th>' +
977
- '<th style="width:70px"></th>' +
978
- '</tr></thead>' +
979
- '<tbody>' + rows + '</tbody>' +
980
- '</table>' +
981
- '</div>' +
982
- '</div>';
983
1058
  }
984
1059
 
985
1060
  /* ═══════════════════════════════════════════════════
@@ -1142,7 +1217,9 @@
1142
1217
  }
1143
1218
 
1144
1219
  document.getElementById('pageTitle').textContent = proj.name;
1145
- document.getElementById('lastRun').textContent = 'Last run ' + relTime(proj.last_run);
1220
+ const lastRunEl = document.getElementById('lastRun');
1221
+ lastRunEl.textContent = 'Last run ' + relTime(proj.last_run);
1222
+ lastRunEl.title = 'All times shown in your local timezone';
1146
1223
  document.title = proj.name + ' - Test Dashboard';
1147
1224
 
1148
1225
  const analytics = proj.analytics || {};
@@ -1166,6 +1243,11 @@
1166
1243
  if (totalEl) animateCounter(totalEl, lastReg ? lastReg.total_tests : 0, 0);
1167
1244
  if (failEl) animateCounter(failEl, lastReg ? lastReg.failed : 0, 0);
1168
1245
 
1246
+ // Render history table body if data exists
1247
+ if (historyData.runs.length > 0) {
1248
+ renderHistoryTableBody();
1249
+ }
1250
+
1169
1251
  // Draw charts after DOM settles
1170
1252
  setTimeout(function() { drawCharts(proj); }, 60);
1171
1253
  });
@@ -6,7 +6,7 @@
6
6
  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
7
7
  <meta http-equiv="Pragma" content="no-cache" />
8
8
  <meta http-equiv="Expires" content="0" />
9
- <title>Run Details - Test Dashboard</title>
9
+ <title>Run Details - Test Result Dashboard</title>
10
10
  <style>
11
11
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
12
 
@@ -413,7 +413,9 @@
413
413
  document.title = (run.suite_name || 'Run') + ' - Test Dashboard';
414
414
  document.getElementById('pageTitle').textContent =
415
415
  (run.suite_name || 'Run') + ' · ' + (run.project_name || '');
416
- document.getElementById('runTime').textContent = fmtDate(run.timestamp);
416
+ var runTimeEl = document.getElementById('runTime');
417
+ runTimeEl.textContent = fmtDate(run.timestamp);
418
+ runTimeEl.title = 'Time shown in your local timezone';
417
419
 
418
420
  var backHref = projectName
419
421
  ? 'project.html?p=' + encodeURIComponent(projectName)
@@ -9,7 +9,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
9
9
 
10
10
  setup(
11
11
  name='test-reporting',
12
- version='3.6.0',
12
+ version='3.6.2',
13
13
  description='Multi-project test reporting dashboard — collect results locally or push to S3',
14
14
  long_description=long_description,
15
15
  long_description_content_type="text/markdown",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: test-reporting
3
- Version: 3.6.0
3
+ Version: 3.6.2
4
4
  Summary: Multi-project test reporting dashboard — collect results locally or push to S3
5
5
  Home-page: https://github.com/amahdy77/test-reporting.git
6
6
  Author: Ashfaqur Mahdy
File without changes
File without changes
File without changes