test-reporting 4.0.0__tar.gz → 4.0.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.
- {test_reporting-4.0.0 → test_reporting-4.0.2}/PKG-INFO +3 -3
- {test_reporting-4.0.0 → test_reporting-4.0.2}/README.md +2 -2
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/config.py +0 -2
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/report_server.py +1 -8
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/templates/index.html +0 -12
- test_reporting-4.0.2/reporting/templates/monthly-reports.html +661 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/templates/report-generator.html +2 -48
- {test_reporting-4.0.0 → test_reporting-4.0.2}/setup.py +1 -1
- {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/PKG-INFO +3 -3
- {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/SOURCES.txt +1 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/LICENSE +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/__init__.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/classifier.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/cli.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/exporters/__init__.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/exporters/pdf_exporter.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/exporters/pptx_exporter.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/exporters/xlsx_exporter.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/monthly_report.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/plugin.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/publisher.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/storage.py +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/templates/project.html +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/templates/run.html +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/setup.cfg +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/dependency_links.txt +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/entry_points.txt +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/requires.txt +0 -0
- {test_reporting-4.0.0 → test_reporting-4.0.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: 4.0.
|
|
3
|
+
Version: 4.0.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
|
|
@@ -317,7 +317,7 @@ retention_days = 90 # Keep 90 days of artifacts and data
|
|
|
317
317
|
|
|
318
318
|
**`run.html?r=<id>&p=<name>`** — Run detail. Test file breakdown, every failing test with its error message, screenshot and trace links.
|
|
319
319
|
|
|
320
|
-
**`
|
|
320
|
+
**`monthly-reports.html`** — Monthly report generator. Generate comprehensive reports in PowerPoint, Excel, or PDF format. (Obscure URL, not linked from main pages)
|
|
321
321
|
|
|
322
322
|
---
|
|
323
323
|
|
|
@@ -336,7 +336,7 @@ test-report monthly-report --format pptx,xlsx,pdf
|
|
|
336
336
|
|
|
337
337
|
# Or use the UI
|
|
338
338
|
test-report serve
|
|
339
|
-
# Then open: http://localhost:
|
|
339
|
+
# Then open: http://localhost:5555/monthly-reports.html
|
|
340
340
|
```
|
|
341
341
|
|
|
342
342
|
### Features
|
|
@@ -274,7 +274,7 @@ retention_days = 90 # Keep 90 days of artifacts and data
|
|
|
274
274
|
|
|
275
275
|
**`run.html?r=<id>&p=<name>`** — Run detail. Test file breakdown, every failing test with its error message, screenshot and trace links.
|
|
276
276
|
|
|
277
|
-
**`
|
|
277
|
+
**`monthly-reports.html`** — Monthly report generator. Generate comprehensive reports in PowerPoint, Excel, or PDF format. (Obscure URL, not linked from main pages)
|
|
278
278
|
|
|
279
279
|
---
|
|
280
280
|
|
|
@@ -293,7 +293,7 @@ test-report monthly-report --format pptx,xlsx,pdf
|
|
|
293
293
|
|
|
294
294
|
# Or use the UI
|
|
295
295
|
test-report serve
|
|
296
|
-
# Then open: http://localhost:
|
|
296
|
+
# Then open: http://localhost:5555/monthly-reports.html
|
|
297
297
|
```
|
|
298
298
|
|
|
299
299
|
### Features
|
|
@@ -57,7 +57,6 @@ class ReportingConfig:
|
|
|
57
57
|
|
|
58
58
|
# Monthly report settings
|
|
59
59
|
monthly_section = self.config['monthly_report'] if 'monthly_report' in self.config else {}
|
|
60
|
-
self.ADMIN_KEY = monthly_section.get('admin_key', 'change-me-in-production')
|
|
61
60
|
self.REPORT_PERIOD_DAYS = int(monthly_section.get('report_period_days', 30))
|
|
62
61
|
self.REPORT_TOP_N = int(monthly_section.get('top_n_items', 10))
|
|
63
62
|
|
|
@@ -88,7 +87,6 @@ class ReportingConfig:
|
|
|
88
87
|
self.REGRESSION_TAG = os.getenv('REGRESSION_TAG') or None
|
|
89
88
|
|
|
90
89
|
# Monthly report defaults
|
|
91
|
-
self.ADMIN_KEY = os.getenv('ADMIN_KEY', 'change-me-in-production')
|
|
92
90
|
self.REPORT_PERIOD_DAYS = 30
|
|
93
91
|
self.REPORT_TOP_N = 10
|
|
94
92
|
|
|
@@ -56,13 +56,6 @@ def create_app(config=None):
|
|
|
56
56
|
def generate_report():
|
|
57
57
|
"""Generate monthly report based on parameters."""
|
|
58
58
|
try:
|
|
59
|
-
# Validate admin key
|
|
60
|
-
admin_key = request.headers.get('X-Admin-Key')
|
|
61
|
-
expected_key = getattr(config, 'ADMIN_KEY', 'change-me-in-production')
|
|
62
|
-
|
|
63
|
-
if admin_key != expected_key:
|
|
64
|
-
return jsonify({'error': 'Unauthorized'}), 401
|
|
65
|
-
|
|
66
59
|
# Parse parameters
|
|
67
60
|
params = request.json
|
|
68
61
|
start_date = params.get('start_date')
|
|
@@ -132,7 +125,7 @@ def run_server(host='127.0.0.1', port=5555, debug=False):
|
|
|
132
125
|
app = create_app(config)
|
|
133
126
|
|
|
134
127
|
print(f"[Report Server] Starting on http://{host}:{port}")
|
|
135
|
-
print(f"[Report Server]
|
|
128
|
+
print(f"[Report Server] Report generator: http://{host}:{port}/monthly-reports")
|
|
136
129
|
print(f"[Report Server] Press Ctrl+C to stop")
|
|
137
130
|
|
|
138
131
|
app.run(host=host, port=port, debug=debug)
|
|
@@ -502,18 +502,6 @@
|
|
|
502
502
|
'<p>Make sure <code>test-report deploy</code> and <code>test-report publish</code> have been run.</p>' +
|
|
503
503
|
'</div>';
|
|
504
504
|
});
|
|
505
|
-
|
|
506
|
-
/* ── Admin section ── */
|
|
507
|
-
const adminSection = document.createElement('div');
|
|
508
|
-
adminSection.style.cssText = 'margin-top: 50px; padding: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); text-align: center;';
|
|
509
|
-
adminSection.innerHTML =
|
|
510
|
-
'<h3 style="font-size: 16px; margin-bottom: 16px; color: var(--muted);">📊 Admin Tools</h3>' +
|
|
511
|
-
'<a href="report-generator.html?key=change-me-in-production" style="display: inline-block; padding: 12px 24px; background: var(--blue); color: white; text-decoration: none; border-radius: 6px; font-weight: 600; transition: all 0.2s;" onmouseover="this.style.background=\'#2c5aa0\'" onmouseout="this.style.background=\'var(--blue)\'">' +
|
|
512
|
-
'Generate Monthly Report' +
|
|
513
|
-
'</a>' +
|
|
514
|
-
'<p style="color: var(--muted); font-size: 12px; margin-top: 12px;">Admin access required</p>';
|
|
515
|
-
|
|
516
|
-
document.querySelector('.page').appendChild(adminSection);
|
|
517
505
|
</script>
|
|
518
506
|
</body>
|
|
519
507
|
</html>
|
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Monthly Report Generator - Admin</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #f4f6f9;
|
|
12
|
+
--surface: #ffffff;
|
|
13
|
+
--border: #e2e8f0;
|
|
14
|
+
--text: #1a202c;
|
|
15
|
+
--muted: #718096;
|
|
16
|
+
--green: #38a169;
|
|
17
|
+
--green-bg: #f0fff4;
|
|
18
|
+
--yellow: #d69e2e;
|
|
19
|
+
--yellow-bg: #fffff0;
|
|
20
|
+
--red: #e53e3e;
|
|
21
|
+
--red-bg: #fff5f5;
|
|
22
|
+
--blue: #3182ce;
|
|
23
|
+
--blue-bg: #ebf8ff;
|
|
24
|
+
--radius: 10px;
|
|
25
|
+
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
body {
|
|
29
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
30
|
+
background: var(--bg);
|
|
31
|
+
color: var(--text);
|
|
32
|
+
min-height: 100vh;
|
|
33
|
+
padding: 20px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.container {
|
|
37
|
+
max-width: 800px;
|
|
38
|
+
margin: 0 auto;
|
|
39
|
+
background: var(--surface);
|
|
40
|
+
border-radius: var(--radius);
|
|
41
|
+
box-shadow: var(--shadow);
|
|
42
|
+
padding: 40px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.header {
|
|
46
|
+
text-align: center;
|
|
47
|
+
margin-bottom: 40px;
|
|
48
|
+
padding-bottom: 20px;
|
|
49
|
+
border-bottom: 2px solid var(--border);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.header h1 {
|
|
53
|
+
font-size: 28px;
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
margin-bottom: 8px;
|
|
56
|
+
color: var(--text);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.header .subtitle {
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
color: var(--muted);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.admin-badge {
|
|
65
|
+
display: inline-block;
|
|
66
|
+
background: var(--blue-bg);
|
|
67
|
+
color: var(--blue);
|
|
68
|
+
padding: 4px 12px;
|
|
69
|
+
border-radius: 12px;
|
|
70
|
+
font-size: 12px;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
margin-top: 8px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.section {
|
|
76
|
+
margin-bottom: 32px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.section-title {
|
|
80
|
+
font-size: 16px;
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
margin-bottom: 16px;
|
|
83
|
+
color: var(--text);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.form-group {
|
|
87
|
+
margin-bottom: 20px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
label {
|
|
91
|
+
display: block;
|
|
92
|
+
font-size: 14px;
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
margin-bottom: 8px;
|
|
95
|
+
color: var(--text);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
input[type="text"],
|
|
99
|
+
input[type="date"],
|
|
100
|
+
select {
|
|
101
|
+
width: 100%;
|
|
102
|
+
padding: 10px 12px;
|
|
103
|
+
border: 1px solid var(--border);
|
|
104
|
+
border-radius: 6px;
|
|
105
|
+
font-size: 14px;
|
|
106
|
+
background: var(--surface);
|
|
107
|
+
color: var(--text);
|
|
108
|
+
transition: border-color 0.2s;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
input[type="text"]:focus,
|
|
112
|
+
input[type="date"]:focus,
|
|
113
|
+
select:focus {
|
|
114
|
+
outline: none;
|
|
115
|
+
border-color: var(--blue);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.radio-group {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
gap: 10px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.radio-option {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 8px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.radio-option input[type="radio"] {
|
|
131
|
+
width: 18px;
|
|
132
|
+
height: 18px;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.radio-option label {
|
|
137
|
+
margin: 0;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
font-weight: 400;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.date-range {
|
|
143
|
+
display: grid;
|
|
144
|
+
grid-template-columns: 1fr 1fr;
|
|
145
|
+
gap: 16px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.checkbox-group {
|
|
149
|
+
display: grid;
|
|
150
|
+
grid-template-columns: repeat(2, 1fr);
|
|
151
|
+
gap: 12px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.checkbox-option {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
gap: 8px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.checkbox-option input[type="checkbox"] {
|
|
161
|
+
width: 18px;
|
|
162
|
+
height: 18px;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.checkbox-option label {
|
|
167
|
+
margin: 0;
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
font-weight: 400;
|
|
170
|
+
font-size: 14px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.advanced-options {
|
|
174
|
+
background: var(--bg);
|
|
175
|
+
padding: 20px;
|
|
176
|
+
border-radius: 6px;
|
|
177
|
+
margin-top: 12px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.advanced-grid {
|
|
181
|
+
display: grid;
|
|
182
|
+
grid-template-columns: repeat(2, 1fr);
|
|
183
|
+
gap: 16px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.button-group {
|
|
187
|
+
display: flex;
|
|
188
|
+
gap: 12px;
|
|
189
|
+
margin-top: 32px;
|
|
190
|
+
padding-top: 24px;
|
|
191
|
+
border-top: 1px solid var(--border);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.btn {
|
|
195
|
+
flex: 1;
|
|
196
|
+
padding: 14px 24px;
|
|
197
|
+
border: none;
|
|
198
|
+
border-radius: 6px;
|
|
199
|
+
font-size: 15px;
|
|
200
|
+
font-weight: 600;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
transition: all 0.2s;
|
|
203
|
+
display: flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
justify-content: center;
|
|
206
|
+
gap: 8px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.btn-primary {
|
|
210
|
+
background: var(--blue);
|
|
211
|
+
color: white;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.btn-primary:hover {
|
|
215
|
+
background: #2c5aa0;
|
|
216
|
+
transform: translateY(-1px);
|
|
217
|
+
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.btn-primary:disabled {
|
|
221
|
+
background: var(--muted);
|
|
222
|
+
cursor: not-allowed;
|
|
223
|
+
transform: none;
|
|
224
|
+
box-shadow: none;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.btn-secondary {
|
|
228
|
+
background: var(--surface);
|
|
229
|
+
color: var(--text);
|
|
230
|
+
border: 1px solid var(--border);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.btn-secondary:hover {
|
|
234
|
+
background: var(--bg);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.status {
|
|
238
|
+
margin-top: 20px;
|
|
239
|
+
padding: 12px 16px;
|
|
240
|
+
border-radius: 6px;
|
|
241
|
+
font-size: 14px;
|
|
242
|
+
display: none;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.status.info {
|
|
246
|
+
background: var(--blue-bg);
|
|
247
|
+
color: var(--blue);
|
|
248
|
+
display: block;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.status.success {
|
|
252
|
+
background: var(--green-bg);
|
|
253
|
+
color: var(--green);
|
|
254
|
+
display: block;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.status.error {
|
|
258
|
+
background: var(--red-bg);
|
|
259
|
+
color: var(--red);
|
|
260
|
+
display: block;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.status.loading {
|
|
264
|
+
background: var(--yellow-bg);
|
|
265
|
+
color: var(--yellow);
|
|
266
|
+
display: block;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.spinner {
|
|
270
|
+
display: inline-block;
|
|
271
|
+
width: 16px;
|
|
272
|
+
height: 16px;
|
|
273
|
+
border: 2px solid currentColor;
|
|
274
|
+
border-top-color: transparent;
|
|
275
|
+
border-radius: 50%;
|
|
276
|
+
animation: spin 0.6s linear infinite;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@keyframes spin {
|
|
280
|
+
to { transform: rotate(360deg); }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.back-link {
|
|
284
|
+
display: inline-flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
gap: 6px;
|
|
287
|
+
color: var(--blue);
|
|
288
|
+
text-decoration: none;
|
|
289
|
+
font-size: 14px;
|
|
290
|
+
margin-bottom: 20px;
|
|
291
|
+
transition: color 0.2s;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.back-link:hover {
|
|
295
|
+
color: #2c5aa0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.access-denied {
|
|
299
|
+
text-align: center;
|
|
300
|
+
padding: 60px 20px;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.access-denied h1 {
|
|
304
|
+
font-size: 48px;
|
|
305
|
+
color: var(--red);
|
|
306
|
+
margin-bottom: 16px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.access-denied p {
|
|
310
|
+
color: var(--muted);
|
|
311
|
+
font-size: 16px;
|
|
312
|
+
margin-bottom: 24px;
|
|
313
|
+
}
|
|
314
|
+
</style>
|
|
315
|
+
</head>
|
|
316
|
+
<body>
|
|
317
|
+
<div id="app"></div>
|
|
318
|
+
|
|
319
|
+
<script>
|
|
320
|
+
function showReportGenerator() {
|
|
321
|
+
|
|
322
|
+
document.getElementById('app').innerHTML = `
|
|
323
|
+
<a href="index.html" class="back-link">← Back to Dashboard</a>
|
|
324
|
+
|
|
325
|
+
<div class="container">
|
|
326
|
+
<div class="header">
|
|
327
|
+
<h1>📊 Monthly Report Generator</h1>
|
|
328
|
+
<p class="subtitle">Generate comprehensive test reports with custom parameters</p>
|
|
329
|
+
<span class="admin-badge">ADMIN ACCESS</span>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<form id="reportForm">
|
|
333
|
+
<!-- Project Selection -->
|
|
334
|
+
<div class="section">
|
|
335
|
+
<div class="section-title">Project</div>
|
|
336
|
+
<div class="form-group">
|
|
337
|
+
<label for="project">Select Project</label>
|
|
338
|
+
<select id="project" name="project">
|
|
339
|
+
<option value="">All Projects</option>
|
|
340
|
+
</select>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<!-- Time Period -->
|
|
345
|
+
<div class="section">
|
|
346
|
+
<div class="section-title">Time Period</div>
|
|
347
|
+
<div class="form-group">
|
|
348
|
+
<div class="radio-group">
|
|
349
|
+
<div class="radio-option">
|
|
350
|
+
<input type="radio" id="period-7" name="period" value="7">
|
|
351
|
+
<label for="period-7">Last 7 days</label>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="radio-option">
|
|
354
|
+
<input type="radio" id="period-30" name="period" value="30" checked>
|
|
355
|
+
<label for="period-30">Last 30 days</label>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="radio-option">
|
|
358
|
+
<input type="radio" id="period-90" name="period" value="90">
|
|
359
|
+
<label for="period-90">Last 90 days</label>
|
|
360
|
+
</div>
|
|
361
|
+
<div class="radio-option">
|
|
362
|
+
<input type="radio" id="period-custom" name="period" value="custom">
|
|
363
|
+
<label for="period-custom">Custom Range</label>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
<div class="form-group" id="customRangeGroup" style="display: none;">
|
|
368
|
+
<div class="date-range">
|
|
369
|
+
<div>
|
|
370
|
+
<label for="startDate">Start Date</label>
|
|
371
|
+
<input type="date" id="startDate" name="startDate">
|
|
372
|
+
</div>
|
|
373
|
+
<div>
|
|
374
|
+
<label for="endDate">End Date</label>
|
|
375
|
+
<input type="date" id="endDate" name="endDate">
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<!-- Output Format -->
|
|
382
|
+
<div class="section">
|
|
383
|
+
<div class="section-title">Output Format</div>
|
|
384
|
+
<div class="form-group">
|
|
385
|
+
<div class="checkbox-group">
|
|
386
|
+
<div class="checkbox-option">
|
|
387
|
+
<input type="checkbox" id="format-pptx" name="format" value="pptx" checked>
|
|
388
|
+
<label for="format-pptx">PowerPoint (.pptx)</label>
|
|
389
|
+
</div>
|
|
390
|
+
<div class="checkbox-option">
|
|
391
|
+
<input type="checkbox" id="format-xlsx" name="format" value="xlsx">
|
|
392
|
+
<label for="format-xlsx">Excel (.xlsx)</label>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="checkbox-option">
|
|
395
|
+
<input type="checkbox" id="format-pdf" name="format" value="pdf">
|
|
396
|
+
<label for="format-pdf">PDF (.pdf)</label>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<!-- Include Sections -->
|
|
403
|
+
<div class="section">
|
|
404
|
+
<div class="section-title">Include Sections</div>
|
|
405
|
+
<div class="form-group">
|
|
406
|
+
<div class="checkbox-group">
|
|
407
|
+
<div class="checkbox-option">
|
|
408
|
+
<input type="checkbox" id="section-summary" name="section" value="summary" checked>
|
|
409
|
+
<label for="section-summary">Executive Summary</label>
|
|
410
|
+
</div>
|
|
411
|
+
<div class="checkbox-option">
|
|
412
|
+
<input type="checkbox" id="section-trends" name="section" value="trends" checked>
|
|
413
|
+
<label for="section-trends">Pass Rate Trends</label>
|
|
414
|
+
</div>
|
|
415
|
+
<div class="checkbox-option">
|
|
416
|
+
<input type="checkbox" id="section-failures" name="section" value="failures" checked>
|
|
417
|
+
<label for="section-failures">Failure Analysis</label>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="checkbox-option">
|
|
420
|
+
<input type="checkbox" id="section-flaky" name="section" value="flaky" checked>
|
|
421
|
+
<label for="section-flaky">Flaky Tests</label>
|
|
422
|
+
</div>
|
|
423
|
+
<div class="checkbox-option">
|
|
424
|
+
<input type="checkbox" id="section-slowest" name="section" value="slowest" checked>
|
|
425
|
+
<label for="section-slowest">Slowest Tests</label>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="checkbox-option">
|
|
428
|
+
<input type="checkbox" id="section-regressions" name="section" value="regressions" checked>
|
|
429
|
+
<label for="section-regressions">Performance Regressions</label>
|
|
430
|
+
</div>
|
|
431
|
+
<div class="checkbox-option">
|
|
432
|
+
<input type="checkbox" id="section-file-health" name="section" value="file_health" checked>
|
|
433
|
+
<label for="section-file-health">File Health Report</label>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="checkbox-option">
|
|
436
|
+
<input type="checkbox" id="section-recommendations" name="section" value="recommendations" checked>
|
|
437
|
+
<label for="section-recommendations">Recommendations</label>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<!-- Advanced Options -->
|
|
444
|
+
<div class="section">
|
|
445
|
+
<div class="section-title">Advanced Options</div>
|
|
446
|
+
<div class="advanced-options">
|
|
447
|
+
<div class="advanced-grid">
|
|
448
|
+
<div class="form-group">
|
|
449
|
+
<label for="topN">Top N Items</label>
|
|
450
|
+
<select id="topN" name="topN">
|
|
451
|
+
<option value="5">5</option>
|
|
452
|
+
<option value="10" selected>10</option>
|
|
453
|
+
<option value="15">15</option>
|
|
454
|
+
<option value="20">20</option>
|
|
455
|
+
</select>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="form-group">
|
|
458
|
+
<label for="chartTheme">Chart Theme</label>
|
|
459
|
+
<select id="chartTheme" name="chartTheme">
|
|
460
|
+
<option value="light" selected>Light</option>
|
|
461
|
+
<option value="dark">Dark</option>
|
|
462
|
+
</select>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Buttons -->
|
|
469
|
+
<div class="button-group">
|
|
470
|
+
<button type="submit" class="btn btn-primary" id="generateBtn">
|
|
471
|
+
<span id="btnText">Generate Report</span>
|
|
472
|
+
</button>
|
|
473
|
+
<button type="button" class="btn btn-secondary" onclick="resetForm()">
|
|
474
|
+
Reset
|
|
475
|
+
</button>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<!-- Status -->
|
|
479
|
+
<div id="status" class="status"></div>
|
|
480
|
+
</form>
|
|
481
|
+
</div>
|
|
482
|
+
`;
|
|
483
|
+
|
|
484
|
+
// Initialize form
|
|
485
|
+
initializeForm();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function initializeForm() {
|
|
489
|
+
// Load projects
|
|
490
|
+
loadProjects();
|
|
491
|
+
|
|
492
|
+
// Set default dates
|
|
493
|
+
const today = new Date();
|
|
494
|
+
const thirtyDaysAgo = new Date(today);
|
|
495
|
+
thirtyDaysAgo.setDate(today.getDate() - 30);
|
|
496
|
+
|
|
497
|
+
document.getElementById('endDate').value = today.toISOString().split('T')[0];
|
|
498
|
+
document.getElementById('startDate').value = thirtyDaysAgo.toISOString().split('T')[0];
|
|
499
|
+
|
|
500
|
+
// Period radio change handler
|
|
501
|
+
document.querySelectorAll('input[name="period"]').forEach(radio => {
|
|
502
|
+
radio.addEventListener('change', (e) => {
|
|
503
|
+
const customRangeGroup = document.getElementById('customRangeGroup');
|
|
504
|
+
if (e.target.value === 'custom') {
|
|
505
|
+
customRangeGroup.style.display = 'block';
|
|
506
|
+
} else {
|
|
507
|
+
customRangeGroup.style.display = 'none';
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Form submit handler
|
|
513
|
+
document.getElementById('reportForm').addEventListener('submit', handleSubmit);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function loadProjects() {
|
|
517
|
+
try {
|
|
518
|
+
const response = await fetch('data.json');
|
|
519
|
+
const data = await response.json();
|
|
520
|
+
const projects = Object.keys(data.projects || {});
|
|
521
|
+
|
|
522
|
+
const select = document.getElementById('project');
|
|
523
|
+
projects.forEach(project => {
|
|
524
|
+
const option = document.createElement('option');
|
|
525
|
+
option.value = project;
|
|
526
|
+
option.textContent = project;
|
|
527
|
+
select.appendChild(option);
|
|
528
|
+
});
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error('Failed to load projects:', error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function handleSubmit(e) {
|
|
535
|
+
e.preventDefault();
|
|
536
|
+
|
|
537
|
+
const generateBtn = document.getElementById('generateBtn');
|
|
538
|
+
const btnText = document.getElementById('btnText');
|
|
539
|
+
const status = document.getElementById('status');
|
|
540
|
+
|
|
541
|
+
// Disable button
|
|
542
|
+
generateBtn.disabled = true;
|
|
543
|
+
btnText.innerHTML = '<span class="spinner"></span> Generating...';
|
|
544
|
+
|
|
545
|
+
// Get form data
|
|
546
|
+
const formData = getFormData();
|
|
547
|
+
|
|
548
|
+
// Validate
|
|
549
|
+
if (!validateFormData(formData)) {
|
|
550
|
+
showStatus('error', 'Please select at least one output format');
|
|
551
|
+
generateBtn.disabled = false;
|
|
552
|
+
btnText.textContent = 'Generate Report';
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
showStatus('loading', 'Generating report... This may take a few moments.');
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
// Call API
|
|
560
|
+
const response = await fetch('http://localhost:5555/api/generate-report', {
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: {
|
|
563
|
+
'Content-Type': 'application/json'
|
|
564
|
+
},
|
|
565
|
+
body: JSON.stringify(formData)
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
if (!response.ok) {
|
|
569
|
+
throw new Error('Failed to generate report');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Download file
|
|
573
|
+
const blob = await response.blob();
|
|
574
|
+
const contentDisposition = response.headers.get('Content-Disposition');
|
|
575
|
+
const filename = contentDisposition
|
|
576
|
+
? contentDisposition.split('filename=')[1].replace(/"/g, '')
|
|
577
|
+
: 'monthly-report.zip';
|
|
578
|
+
|
|
579
|
+
const url = window.URL.createObjectURL(blob);
|
|
580
|
+
const a = document.createElement('a');
|
|
581
|
+
a.href = url;
|
|
582
|
+
a.download = filename;
|
|
583
|
+
document.body.appendChild(a);
|
|
584
|
+
a.click();
|
|
585
|
+
window.URL.revokeObjectURL(url);
|
|
586
|
+
document.body.removeChild(a);
|
|
587
|
+
|
|
588
|
+
showStatus('success', 'Report generated successfully! Download should start automatically.');
|
|
589
|
+
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.error('Error generating report:', error);
|
|
592
|
+
showStatus('error', 'Failed to generate report. Make sure the report server is running (test-report serve).');
|
|
593
|
+
} finally {
|
|
594
|
+
generateBtn.disabled = false;
|
|
595
|
+
btnText.textContent = 'Generate Report';
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function getFormData() {
|
|
600
|
+
const form = document.getElementById('reportForm');
|
|
601
|
+
const formData = new FormData(form);
|
|
602
|
+
|
|
603
|
+
// Get period
|
|
604
|
+
const period = formData.get('period');
|
|
605
|
+
let startDate, endDate;
|
|
606
|
+
|
|
607
|
+
if (period === 'custom') {
|
|
608
|
+
startDate = formData.get('startDate');
|
|
609
|
+
endDate = formData.get('endDate');
|
|
610
|
+
} else {
|
|
611
|
+
const today = new Date();
|
|
612
|
+
endDate = today.toISOString().split('T')[0];
|
|
613
|
+
const start = new Date(today);
|
|
614
|
+
start.setDate(today.getDate() - parseInt(period));
|
|
615
|
+
startDate = start.toISOString().split('T')[0];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Get formats
|
|
619
|
+
const formats = [];
|
|
620
|
+
document.querySelectorAll('input[name="format"]:checked').forEach(cb => {
|
|
621
|
+
formats.push(cb.value);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Get sections
|
|
625
|
+
const sections = {};
|
|
626
|
+
document.querySelectorAll('input[name="section"]').forEach(cb => {
|
|
627
|
+
sections[cb.value] = cb.checked;
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
project: formData.get('project') || null,
|
|
632
|
+
start_date: startDate,
|
|
633
|
+
end_date: endDate,
|
|
634
|
+
formats: formats,
|
|
635
|
+
sections: sections,
|
|
636
|
+
top_n: parseInt(formData.get('topN')),
|
|
637
|
+
chart_theme: formData.get('chartTheme')
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function validateFormData(data) {
|
|
642
|
+
return data.formats.length > 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function showStatus(type, message) {
|
|
646
|
+
const status = document.getElementById('status');
|
|
647
|
+
status.className = `status ${type}`;
|
|
648
|
+
status.textContent = message;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function resetForm() {
|
|
652
|
+
document.getElementById('reportForm').reset();
|
|
653
|
+
document.getElementById('customRangeGroup').style.display = 'none';
|
|
654
|
+
document.getElementById('status').className = 'status';
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Initialize on load
|
|
658
|
+
showReportGenerator();
|
|
659
|
+
</script>
|
|
660
|
+
</body>
|
|
661
|
+
</html>
|
|
@@ -317,46 +317,7 @@
|
|
|
317
317
|
<div id="app"></div>
|
|
318
318
|
|
|
319
319
|
<script>
|
|
320
|
-
// Check admin access
|
|
321
|
-
function checkAdminAccess() {
|
|
322
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
323
|
-
const key = urlParams.get('key');
|
|
324
|
-
|
|
325
|
-
// Get admin key from config (will be loaded from data.json or config)
|
|
326
|
-
const adminKey = getAdminKey();
|
|
327
|
-
|
|
328
|
-
if (!key || key !== adminKey) {
|
|
329
|
-
showAccessDenied();
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function getAdminKey() {
|
|
336
|
-
// Try to get from localStorage (set during deployment)
|
|
337
|
-
const storedKey = localStorage.getItem('admin_key');
|
|
338
|
-
if (storedKey) return storedKey;
|
|
339
|
-
|
|
340
|
-
// Default key (should be changed in production)
|
|
341
|
-
return 'change-me-in-production';
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function showAccessDenied() {
|
|
345
|
-
document.getElementById('app').innerHTML = `
|
|
346
|
-
<div class="access-denied">
|
|
347
|
-
<h1>🔒</h1>
|
|
348
|
-
<h2>Access Denied</h2>
|
|
349
|
-
<p>You need admin access to view this page.</p>
|
|
350
|
-
<a href="index.html" class="btn btn-primary" style="display: inline-flex; width: auto;">
|
|
351
|
-
Return to Dashboard
|
|
352
|
-
</a>
|
|
353
|
-
</div>
|
|
354
|
-
`;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
320
|
function showReportGenerator() {
|
|
358
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
359
|
-
const adminKey = urlParams.get('key');
|
|
360
321
|
|
|
361
322
|
document.getElementById('app').innerHTML = `
|
|
362
323
|
<a href="index.html" class="back-link">← Back to Dashboard</a>
|
|
@@ -595,16 +556,11 @@
|
|
|
595
556
|
showStatus('loading', 'Generating report... This may take a few moments.');
|
|
596
557
|
|
|
597
558
|
try {
|
|
598
|
-
// Get admin key from URL
|
|
599
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
600
|
-
const adminKey = urlParams.get('key');
|
|
601
|
-
|
|
602
559
|
// Call API
|
|
603
560
|
const response = await fetch('http://localhost:5555/api/generate-report', {
|
|
604
561
|
method: 'POST',
|
|
605
562
|
headers: {
|
|
606
|
-
'Content-Type': 'application/json'
|
|
607
|
-
'X-Admin-Key': adminKey
|
|
563
|
+
'Content-Type': 'application/json'
|
|
608
564
|
},
|
|
609
565
|
body: JSON.stringify(formData)
|
|
610
566
|
});
|
|
@@ -699,9 +655,7 @@
|
|
|
699
655
|
}
|
|
700
656
|
|
|
701
657
|
// Initialize on load
|
|
702
|
-
|
|
703
|
-
showReportGenerator();
|
|
704
|
-
}
|
|
658
|
+
showReportGenerator();
|
|
705
659
|
</script>
|
|
706
660
|
</body>
|
|
707
661
|
</html>
|
|
@@ -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='4.0.
|
|
12
|
+
version='4.0.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: 4.0.
|
|
3
|
+
Version: 4.0.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
|
|
@@ -317,7 +317,7 @@ retention_days = 90 # Keep 90 days of artifacts and data
|
|
|
317
317
|
|
|
318
318
|
**`run.html?r=<id>&p=<name>`** — Run detail. Test file breakdown, every failing test with its error message, screenshot and trace links.
|
|
319
319
|
|
|
320
|
-
**`
|
|
320
|
+
**`monthly-reports.html`** — Monthly report generator. Generate comprehensive reports in PowerPoint, Excel, or PDF format. (Obscure URL, not linked from main pages)
|
|
321
321
|
|
|
322
322
|
---
|
|
323
323
|
|
|
@@ -336,7 +336,7 @@ test-report monthly-report --format pptx,xlsx,pdf
|
|
|
336
336
|
|
|
337
337
|
# Or use the UI
|
|
338
338
|
test-report serve
|
|
339
|
-
# Then open: http://localhost:
|
|
339
|
+
# Then open: http://localhost:5555/monthly-reports.html
|
|
340
340
|
```
|
|
341
341
|
|
|
342
342
|
### Features
|
|
@@ -15,6 +15,7 @@ reporting/exporters/pdf_exporter.py
|
|
|
15
15
|
reporting/exporters/pptx_exporter.py
|
|
16
16
|
reporting/exporters/xlsx_exporter.py
|
|
17
17
|
reporting/templates/index.html
|
|
18
|
+
reporting/templates/monthly-reports.html
|
|
18
19
|
reporting/templates/project.html
|
|
19
20
|
reporting/templates/report-generator.html
|
|
20
21
|
reporting/templates/run.html
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|