slik-report 1.0.2 → 1.0.3
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.
- package/dist/index.js +585 -327
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -25,19 +25,24 @@ function stripAnsi(str) {
|
|
|
25
25
|
const ansiRegex = /[\u001b\u009b][[()#;?]*.{0,2}(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
26
26
|
return str.replace(ansiRegex, '');
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Escapes a JSON string for safe embedding within an HTML <script> tag.
|
|
30
|
-
* This prevents the JSON from prematurely closing the script block (e.g., if it contains </script>).
|
|
31
|
-
*/
|
|
32
28
|
function escapeJsonForHtml(jsonString) {
|
|
33
29
|
return jsonString.replace(/<\/script>/g, '<\\/script>');
|
|
34
30
|
}
|
|
31
|
+
function calculateStdDev(values) {
|
|
32
|
+
if (values.length < 2)
|
|
33
|
+
return 0;
|
|
34
|
+
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
35
|
+
const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / (values.length - 1);
|
|
36
|
+
return Math.sqrt(variance);
|
|
37
|
+
}
|
|
35
38
|
class AggregatorReporter {
|
|
36
39
|
constructor(opts) {
|
|
37
40
|
this.options = opts || {};
|
|
38
41
|
this.input = (this.options.input) || './playwright-report/report.json';
|
|
39
42
|
this.outputHtml = (this.options.output) || './summary-report.html';
|
|
40
43
|
this.historyDir = (this.options.history) || './reports/reports_history';
|
|
44
|
+
// New Option: Path to external HTML report
|
|
45
|
+
this.anotherHTML = (this.options.externalHtml) || './playwright-report/reports/index.html';
|
|
41
46
|
}
|
|
42
47
|
async onEnd(result) {
|
|
43
48
|
try {
|
|
@@ -48,12 +53,21 @@ class AggregatorReporter {
|
|
|
48
53
|
}
|
|
49
54
|
console.log('AggregatorReporter: JSON file found. Reading file...');
|
|
50
55
|
const report = JSON.parse(fs.readFileSync(this.input, 'utf8'));
|
|
51
|
-
|
|
56
|
+
// Check if external HTML exists
|
|
57
|
+
const hasExternalHtml = fs.existsSync(this.anotherHTML);
|
|
58
|
+
// Ensure we use a relative path for the HTML src attribute
|
|
59
|
+
const externalHtmlPath = this.anotherHTML;
|
|
60
|
+
console.log('AggregatorReporter: JSON data read. Processing...');
|
|
52
61
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
53
62
|
const historyFile = path.join(this.historyDir, `run-${timestamp}.json`);
|
|
54
63
|
writeFile(historyFile, safeJson(report));
|
|
55
64
|
const history = await this.loadHistory();
|
|
56
65
|
const processedReport = this.processReportWithHistory(report, history);
|
|
66
|
+
// Inject external HTML config
|
|
67
|
+
processedReport.config = {
|
|
68
|
+
hasExternalHtml: hasExternalHtml,
|
|
69
|
+
externalHtmlPath: externalHtmlPath
|
|
70
|
+
};
|
|
57
71
|
const htmlContent = this.generateReportHtml(processedReport);
|
|
58
72
|
writeFile(this.outputHtml, htmlContent);
|
|
59
73
|
console.log('AggregatorReporter: Report generation successful!');
|
|
@@ -62,39 +76,22 @@ class AggregatorReporter {
|
|
|
62
76
|
console.error('AggregatorReporter failed:', e);
|
|
63
77
|
}
|
|
64
78
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Helper function to robustly get the array of top-level test suites.
|
|
67
|
-
* This handles standard Playwright and various custom/merged report formats.
|
|
68
|
-
*/
|
|
69
79
|
getTestSuites(report) {
|
|
70
80
|
if (!report)
|
|
71
81
|
return [];
|
|
72
|
-
|
|
73
|
-
if (report.suites && Array.isArray(report.suites)) {
|
|
82
|
+
if (report.suites && Array.isArray(report.suites))
|
|
74
83
|
return report.suites;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return [{ title: 'Root Suite (Flat Specs)', suites: [], specs: report.specs, file: 'N/A' }];
|
|
80
|
-
}
|
|
81
|
-
// 3. Check for a Playwright V2/aggregator structure where tests are flatly listed
|
|
82
|
-
if (report.tests && Array.isArray(report.tests) && report.tests.length > 0) {
|
|
83
|
-
console.log('AggregatorReporter: Using flat "tests" array as report root. (Playwright V2 or custom)');
|
|
84
|
+
if (report.specs && Array.isArray(report.specs))
|
|
85
|
+
return [{ title: 'Root (Specs)', suites: [], specs: report.specs, file: 'N/A' }];
|
|
86
|
+
// Handle flattened 'tests' structure if strictly flat
|
|
87
|
+
if (report.tests && Array.isArray(report.tests)) {
|
|
84
88
|
return [{
|
|
85
|
-
title: 'Root
|
|
89
|
+
title: 'Root (Flat Tests)',
|
|
86
90
|
suites: [],
|
|
87
|
-
specs: report.tests.map(t => ({
|
|
88
|
-
title: t.title,
|
|
89
|
-
file: t.file,
|
|
90
|
-
tests: [t]
|
|
91
|
-
})),
|
|
91
|
+
specs: report.tests.map(t => ({ title: t.title, file: t.file, tests: [t] })),
|
|
92
92
|
file: 'N/A'
|
|
93
93
|
}];
|
|
94
94
|
}
|
|
95
|
-
if (report.stats) {
|
|
96
|
-
console.warn('AggregatorReporter: Could not find "suites" or alternative test structure. Returning empty suites list.');
|
|
97
|
-
}
|
|
98
95
|
return [];
|
|
99
96
|
}
|
|
100
97
|
findAllSpecs(suites) {
|
|
@@ -104,9 +101,8 @@ class AggregatorReporter {
|
|
|
104
101
|
for (const suite of suites) {
|
|
105
102
|
if (suite.specs)
|
|
106
103
|
specs.push(...suite.specs);
|
|
107
|
-
if (suite.suites)
|
|
104
|
+
if (suite.suites)
|
|
108
105
|
specs.push(...this.findAllSpecs(suite.suites));
|
|
109
|
-
}
|
|
110
106
|
}
|
|
111
107
|
return specs;
|
|
112
108
|
}
|
|
@@ -122,16 +118,8 @@ class AggregatorReporter {
|
|
|
122
118
|
const filePath = path.join(this.historyDir, file);
|
|
123
119
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
124
120
|
const dateStr = file.replace('run-', '').replace('.json', '');
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
dateStr.slice(11, 13) + ':' +
|
|
128
|
-
dateStr.slice(14, 16) + ':' +
|
|
129
|
-
dateStr.slice(17, 19) + '.' +
|
|
130
|
-
dateStr.slice(20) + 'Z';
|
|
131
|
-
history.push({
|
|
132
|
-
date: new Date(validIso).toLocaleDateString(),
|
|
133
|
-
report: data
|
|
134
|
-
});
|
|
121
|
+
const validIso = dateStr.slice(0, 10) + 'T' + dateStr.slice(11, 13) + ':' + dateStr.slice(14, 16) + ':' + dateStr.slice(17, 19) + '.' + dateStr.slice(20);
|
|
122
|
+
history.push({ date: new Date(validIso).toLocaleDateString(), report: data });
|
|
135
123
|
}
|
|
136
124
|
catch (e) {
|
|
137
125
|
console.warn(`Could not read history file: ${file}`, e);
|
|
@@ -141,56 +129,110 @@ class AggregatorReporter {
|
|
|
141
129
|
}
|
|
142
130
|
processReportWithHistory(report, history) {
|
|
143
131
|
const stats = this.processStats(report);
|
|
132
|
+
const historyData = this.processHistoryData(history, report);
|
|
144
133
|
const suites = this.processSuites(report, history);
|
|
145
134
|
const categories = this.processCategories(report);
|
|
146
|
-
const historyData = this.processHistoryData(history, report);
|
|
147
135
|
const timeline = this.processTimeline(report);
|
|
148
136
|
const metadata = this.processMetadata(report);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
137
|
+
const tagStats = this.processTags(report); // New Tag Processor
|
|
138
|
+
// KPI Calculations
|
|
139
|
+
const lastFivePassRates = historyData.slice(-5).map(h => h.passRate);
|
|
140
|
+
stats.passRateConsistency = calculateStdDev(lastFivePassRates).toFixed(2);
|
|
141
|
+
let flakyCount = 0;
|
|
142
|
+
let failedAndReproduced = 0;
|
|
143
|
+
suites.forEach(suite => {
|
|
144
|
+
suite.specs.forEach(spec => {
|
|
145
|
+
const test = spec.tests?.[0];
|
|
146
|
+
if (test) {
|
|
147
|
+
if (test.flaky)
|
|
148
|
+
flakyCount++;
|
|
149
|
+
if (test.status === 'failed' && test.previousStatus === 'failed')
|
|
150
|
+
failedAndReproduced++;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
const totalTests = stats.total;
|
|
155
|
+
stats.flakyCount = flakyCount;
|
|
156
|
+
stats.flakyPercentage = totalTests > 0 ? ((flakyCount / totalTests) * 100).toFixed(2) + '%' : 'N/A';
|
|
157
|
+
const currentFailedTests = stats.failed;
|
|
158
|
+
stats.failureReproducibility = currentFailedTests > 0
|
|
159
|
+
? ((failedAndReproduced / currentFailedTests) * 100).toFixed(2) + '%'
|
|
160
|
+
: 'N/A';
|
|
161
|
+
return { stats, suites, categories, history: historyData, timeline, metadata, tagStats };
|
|
157
162
|
}
|
|
158
163
|
processStats(report) {
|
|
159
164
|
const stats = report.stats || {};
|
|
160
165
|
const { expected = 0, unexpected = 0, skipped = 0, duration = 0 } = stats;
|
|
166
|
+
let retries = 0;
|
|
167
|
+
const allSpecs = this.findAllSpecs(this.getTestSuites(report));
|
|
168
|
+
allSpecs.forEach(spec => {
|
|
169
|
+
if (spec.tests) {
|
|
170
|
+
spec.tests.forEach(test => { if (test.results)
|
|
171
|
+
retries += Math.max(0, test.results.length - 1); });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
161
174
|
const total = expected + unexpected + skipped;
|
|
162
|
-
const passRate = total > 0 ? `${((expected / total) * 100).toFixed(0)}%` : 'N/A';
|
|
163
|
-
const avgDuration = total > 0 ? `${(duration / total / 1000).toFixed(2)}s` : 'N/A';
|
|
164
175
|
return {
|
|
165
|
-
total,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
avgDuration,
|
|
171
|
-
totalDuration: `${(duration / 1000).toFixed(2)}s`
|
|
176
|
+
total, passed: expected, failed: unexpected, skipped, retries,
|
|
177
|
+
passRate: total > 0 ? `${((expected / total) * 100).toFixed(0)}%` : 'N/A',
|
|
178
|
+
avgDuration: total > 0 ? `${(duration / total / 1000).toFixed(2)}s` : 'N/A',
|
|
179
|
+
totalDuration: `${(duration / 1000).toFixed(2)}s`,
|
|
180
|
+
passRateConsistency: 'N/A', flakyPercentage: 'N/A', failureReproducibility: 'N/A',
|
|
172
181
|
};
|
|
173
182
|
}
|
|
174
183
|
processMetadata(report) {
|
|
175
|
-
// Safely access project name from the projects array
|
|
176
184
|
const projectName = report.config?.projects?.[0]?.name || report.config?.metadata?.project || 'N/A';
|
|
177
185
|
return {
|
|
178
186
|
browser: projectName,
|
|
179
|
-
os: report.config?.metadata?.platform || 'N/A',
|
|
187
|
+
os: report.config?.metadata?.platform || 'N/A',
|
|
180
188
|
startTime: report.stats?.startTime ? new Date(report.stats.startTime).toLocaleString() : 'N/A',
|
|
181
189
|
totalDuration: report.stats?.duration ? `${(report.stats.duration / 1000).toFixed(2)}s` : 'N/A'
|
|
182
190
|
};
|
|
183
191
|
}
|
|
192
|
+
processTags(report) {
|
|
193
|
+
const tagMap = {};
|
|
194
|
+
const allSpecs = this.findAllSpecs(this.getTestSuites(report));
|
|
195
|
+
allSpecs.forEach(spec => {
|
|
196
|
+
// Collect tags from spec or test level
|
|
197
|
+
const tags = spec.tags || [];
|
|
198
|
+
if (spec.tests && spec.tests[0] && spec.tests[0].tags) {
|
|
199
|
+
tags.push(...spec.tests[0].tags);
|
|
200
|
+
}
|
|
201
|
+
const uniqueTags = [...new Set(tags)]; // Dedup
|
|
202
|
+
const result = spec.tests?.[0]?.results?.[0];
|
|
203
|
+
const status = result?.status || 'skipped';
|
|
204
|
+
const duration = result?.duration || 0;
|
|
205
|
+
uniqueTags.forEach(tag => {
|
|
206
|
+
// Clean tag (remove @ if present for display)
|
|
207
|
+
const tagName = tag.startsWith('@') ? tag.substring(1) : tag;
|
|
208
|
+
if (!tagMap[tagName]) {
|
|
209
|
+
tagMap[tagName] = { name: tagName, total: 0, passed: 0, failed: 0, skipped: 0, duration: 0, tests: [] };
|
|
210
|
+
}
|
|
211
|
+
tagMap[tagName].total++;
|
|
212
|
+
if (status === 'passed')
|
|
213
|
+
tagMap[tagName].passed++;
|
|
214
|
+
else if (status === 'failed' || status === 'timedOut')
|
|
215
|
+
tagMap[tagName].failed++;
|
|
216
|
+
else
|
|
217
|
+
tagMap[tagName].skipped++;
|
|
218
|
+
tagMap[tagName].duration += duration;
|
|
219
|
+
tagMap[tagName].tests.push({
|
|
220
|
+
title: spec.title,
|
|
221
|
+
status: status,
|
|
222
|
+
duration: duration
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
return Object.values(tagMap);
|
|
227
|
+
}
|
|
184
228
|
processSuites(report, history) {
|
|
185
229
|
const historyDataMap = new Map();
|
|
186
230
|
history.forEach(run => {
|
|
187
231
|
const allSpecs = this.findAllSpecs(this.getTestSuites(run.report));
|
|
188
232
|
allSpecs.forEach(spec => {
|
|
189
233
|
const testId = `${spec.file}::${spec.title}`;
|
|
190
|
-
if (!historyDataMap.has(testId))
|
|
234
|
+
if (!historyDataMap.has(testId))
|
|
191
235
|
historyDataMap.set(testId, []);
|
|
192
|
-
}
|
|
193
|
-
// Safely get status, prioritizing unexpected over passed/skipped if multiple results exist
|
|
194
236
|
const resultStatus = spec.tests?.[0]?.results?.[0]?.status || 'skipped';
|
|
195
237
|
historyDataMap.get(testId).push(resultStatus);
|
|
196
238
|
});
|
|
@@ -208,26 +250,31 @@ class AggregatorReporter {
|
|
|
208
250
|
const currentTest = spec.tests?.[0];
|
|
209
251
|
const currentResult = currentTest?.results?.[0];
|
|
210
252
|
const currentStatus = currentResult?.status || 'skipped';
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
253
|
+
const previousStatus = historyStatuses[historyStatuses.length - 1] || 'N/A';
|
|
254
|
+
const allRuns = [...historyStatuses, currentStatus];
|
|
255
|
+
const displayRuns = allRuns.slice(-5);
|
|
256
|
+
const isFlaky = new Set(displayRuns).size > 1;
|
|
257
|
+
const logs = (currentResult?.stdout || []).map(s => stripAnsi(s.text || s)).join('');
|
|
258
|
+
// Extract Stack Trace
|
|
259
|
+
let stack = '';
|
|
260
|
+
if (currentResult?.error?.stack) {
|
|
261
|
+
stack = stripAnsi(currentResult.error.stack);
|
|
216
262
|
}
|
|
217
|
-
else if (
|
|
218
|
-
|
|
263
|
+
else if (currentResult?.errors && currentResult.errors.length > 0) {
|
|
264
|
+
stack = currentResult.errors.map(e => stripAnsi(e.stack || e.message)).join('\n');
|
|
219
265
|
}
|
|
220
|
-
const isFlaky = new Set(lastRuns).size > 1;
|
|
221
|
-
const logs = (currentResult?.stdout || []).map(s => stripAnsi(s.text || '')).join('');
|
|
222
266
|
return {
|
|
223
267
|
...spec,
|
|
224
268
|
tests: spec.tests.map(test => ({
|
|
225
269
|
...test,
|
|
226
|
-
history:
|
|
270
|
+
history: displayRuns,
|
|
271
|
+
previousStatus: previousStatus,
|
|
227
272
|
flaky: isFlaky,
|
|
228
273
|
status: currentStatus,
|
|
229
274
|
duration: currentResult?.duration || 0,
|
|
230
|
-
logs: logs
|
|
275
|
+
logs: logs,
|
|
276
|
+
stack: stack, // Added Stack
|
|
277
|
+
results: test.results || [],
|
|
231
278
|
}))
|
|
232
279
|
};
|
|
233
280
|
})
|
|
@@ -246,30 +293,19 @@ class AggregatorReporter {
|
|
|
246
293
|
allSpecs.forEach(spec => {
|
|
247
294
|
const result = spec.tests?.[0]?.results?.[0];
|
|
248
295
|
if (result?.status === 'failed' && result.error) {
|
|
249
|
-
// Use the raw error message to preserve newlines and ANSI for stripping/display
|
|
250
296
|
const cleanMessage = stripAnsi(result.error.message || result.error.stack || 'Unknown error');
|
|
251
297
|
let errorName;
|
|
252
298
|
const expectMatch = cleanMessage.match(/Expected: (.*)\s*Received: (.*)/);
|
|
253
299
|
if (expectMatch) {
|
|
254
|
-
|
|
255
|
-
const received = (expectMatch[2] || '').trim();
|
|
256
|
-
errorName = `AssertionError: Expected '${expected}' but received '${received}'`;
|
|
300
|
+
errorName = `AssertionError: Expected '${expectMatch[1].trim()}' but received '${expectMatch[2].trim()}'`;
|
|
257
301
|
}
|
|
258
302
|
else {
|
|
259
303
|
errorName = (cleanMessage.split('\n')[0] || 'Unknown Error').replace('Error: ', '').trim();
|
|
260
304
|
}
|
|
261
|
-
if (!categories[errorName])
|
|
262
|
-
categories[errorName] = {
|
|
263
|
-
name: errorName,
|
|
264
|
-
count: 0,
|
|
265
|
-
tests: []
|
|
266
|
-
};
|
|
267
|
-
}
|
|
305
|
+
if (!categories[errorName])
|
|
306
|
+
categories[errorName] = { name: errorName, count: 0, tests: [] };
|
|
268
307
|
categories[errorName].count++;
|
|
269
|
-
categories[errorName].tests.push({
|
|
270
|
-
title: `${spec.file} -> ${spec.title}`,
|
|
271
|
-
trace: cleanMessage
|
|
272
|
-
});
|
|
308
|
+
categories[errorName].tests.push({ title: `${spec.file} -> ${spec.title}`, trace: cleanMessage });
|
|
273
309
|
}
|
|
274
310
|
});
|
|
275
311
|
return Object.values(categories);
|
|
@@ -277,22 +313,15 @@ class AggregatorReporter {
|
|
|
277
313
|
processHistoryData(history, currentReport) {
|
|
278
314
|
const allRuns = [...history];
|
|
279
315
|
const today = new Date().toLocaleDateString();
|
|
280
|
-
// Add current report to historical runs for chart, checking for duplicates
|
|
281
316
|
if (!history.find(h => h.report.stats?.startTime === currentReport.stats?.startTime)) {
|
|
282
|
-
allRuns.push({
|
|
283
|
-
date: today,
|
|
284
|
-
report: currentReport,
|
|
285
|
-
});
|
|
317
|
+
allRuns.push({ date: today, report: currentReport });
|
|
286
318
|
}
|
|
287
319
|
return allRuns.map(run => {
|
|
288
320
|
const stats = run.report.stats || {};
|
|
289
321
|
const { expected = 0, unexpected = 0, skipped = 0 } = stats;
|
|
290
322
|
const total = expected + unexpected + skipped;
|
|
291
323
|
return {
|
|
292
|
-
date: run.date,
|
|
293
|
-
passed: expected,
|
|
294
|
-
failed: unexpected,
|
|
295
|
-
skipped,
|
|
324
|
+
date: run.date, passed: expected, failed: unexpected, skipped,
|
|
296
325
|
passRate: total > 0 ? (expected / total) * 100 : 0
|
|
297
326
|
};
|
|
298
327
|
});
|
|
@@ -304,27 +333,20 @@ class AggregatorReporter {
|
|
|
304
333
|
allSpecs.forEach(spec => {
|
|
305
334
|
const result = spec.tests?.[0]?.results?.[0];
|
|
306
335
|
const duration = result?.duration || 0;
|
|
307
|
-
timeline.push({
|
|
308
|
-
name: spec.title,
|
|
309
|
-
start: cumulativeDuration,
|
|
310
|
-
duration: duration,
|
|
311
|
-
status: result?.status || 'skipped'
|
|
312
|
-
});
|
|
336
|
+
timeline.push({ name: spec.title, start: cumulativeDuration, duration: duration, status: result?.status || 'skipped' });
|
|
313
337
|
cumulativeDuration += duration;
|
|
314
338
|
});
|
|
315
339
|
return timeline;
|
|
316
340
|
}
|
|
317
341
|
generateReportHtml(report) {
|
|
318
|
-
// 1. Stringify the report object
|
|
319
342
|
const reportJson = JSON.stringify(report);
|
|
320
|
-
// 2. Escape the stringified JSON for safe HTML embedding (CRITICAL FIX)
|
|
321
343
|
const safeReportJson = escapeJsonForHtml(reportJson);
|
|
322
344
|
return `
|
|
323
345
|
<!doctype html>
|
|
324
346
|
<html>
|
|
325
347
|
<head>
|
|
326
348
|
<meta charset="utf-8"/>
|
|
327
|
-
<title>DIS API Test Summary
|
|
349
|
+
<title>DIS API Test Summary</title>
|
|
328
350
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
329
351
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
|
|
330
352
|
<style>
|
|
@@ -333,59 +355,75 @@ class AggregatorReporter {
|
|
|
333
355
|
--pass-bg:#e6ffed; --pass-text:#0a7a0a; --fail-bg:#fff0f0; --fail-text:#b00020;
|
|
334
356
|
--skipped-bg:#f0f0f0; --skipped-text:#6B7280; --shadow: 0 6px 18px rgba(15,23,42,0.08);
|
|
335
357
|
--table-header-bg: rgba(15,23,42,0.03); --table-border: rgba(15,23,42,0.04);
|
|
336
|
-
--log-bg: #eef0f3;
|
|
358
|
+
--log-bg: #eef0f3; --retry-bg: #fffbe6; --retry-text: #8a6d3b;
|
|
359
|
+
--consistency-bg: #e0f7fa; --consistency-text: #00838f;
|
|
360
|
+
--flaky-bg: #fff3e0; --flaky-text: #ff9800;
|
|
361
|
+
--repro-bg: #f3e5f5; --repro-text: #4a148c;
|
|
337
362
|
}
|
|
338
363
|
[data-theme="dark"]{
|
|
339
364
|
--bg:#1a202c; --card:#2d3748; --text:#e2e8f0; --muted:#a0aec0; --accent:#63b3ed;
|
|
340
365
|
--pass-bg:#2A4838; --pass-text:#68D391; --fail-bg:#582C2C; --fail-text:#FC8181;
|
|
341
366
|
--skipped-bg:#4a5568; --skipped-text:#e2e8f0; --shadow: 0 6px 18px rgba(0,0,0,0.4);
|
|
342
367
|
--table-header-bg: rgba(255,255,255,0.05); --table-border: rgba(255,255,255,0.08);
|
|
343
|
-
--log-bg: #1e293b;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
line-height: 1.6; color: var(--text); background-color: var(--bg); transition: background-color 0.3s, color 0.3s;
|
|
368
|
+
--log-bg: #1e293b; --retry-bg: #4c3c13; --retry-text: #ffeb3b;
|
|
369
|
+
--consistency-bg: #1a4e58; --consistency-text: #80deea;
|
|
370
|
+
--flaky-bg: #5f4a13; --flaky-text: #ffc107;
|
|
371
|
+
--repro-bg: #40234a; --repro-text: #ce93d8;
|
|
348
372
|
}
|
|
373
|
+
body { margin: 0; padding: 0; font-family: 'Inter', sans-serif; font-size: 14px; line-height: 1.6; color: var(--text); background-color: var(--bg); transition: background-color 0.4s, color 0.4s; }
|
|
349
374
|
.app-container { display: flex; min-height: 100vh; }
|
|
350
|
-
.
|
|
375
|
+
.download-buttons{display:flex;gap:10px}.download-buttons button{padding:8px 14px;border:1px solid var(--table-border);background:linear-gradient(135deg,var(--card),color-mix(in srgb,var(--card) 90%,black));color:var(--text);border-radius:10px;cursor:pointer;transition:transform .2s,box-shadow .2s,background .25s}.download-buttons button:hover{transform:translateY(-2px);background:linear-gradient(135deg,var(--table-header-bg),var(--card));box-shadow:0 6px 16px rgba(0,0,0,.15)}.download-buttons button:active{transform:translateY(0);box-shadow:0 3px 8px rgba(0,0,0,.2) inset}
|
|
376
|
+
.sidebar { width: 200px; background-color: var(--card); padding: 24px; box-shadow: var(--shadow); z-index: 10; flex-shrink: 0; }
|
|
351
377
|
.sidebar-header { font-weight: 700; font-size: 1.5em; margin-bottom: 30px; color: var(--accent); }
|
|
352
378
|
.sidebar-menu { list-style: none; padding: 0; }
|
|
353
379
|
.sidebar-menu-item { margin-bottom: 12px; }
|
|
354
|
-
.sidebar-menu-item a { display: block; padding:
|
|
355
|
-
.sidebar-menu-item a:hover { background-color: var(--table-header-bg); }
|
|
380
|
+
.sidebar-menu-item a { display: block; padding: 8px 16px; color: var(--text); text-decoration: none; border-radius: 8px; transition: background-color 0.2s, transform 0.1s; }
|
|
381
|
+
.sidebar-menu-item a:hover { background-color: var(--table-header-bg); transform: translateX(4px); }
|
|
356
382
|
.sidebar-menu-item a.active { background-color: var(--accent); color: white; }
|
|
357
383
|
.main-content { flex-grow: 1; padding: 32px; overflow-x: auto; }
|
|
358
384
|
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 16px;}
|
|
359
385
|
.header h1 { font-size: 2em; font-weight: 600; margin: 0; }
|
|
360
386
|
.filters, .actions { display: flex; gap: 16px; align-items: center; }
|
|
361
387
|
.filters label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
|
362
|
-
.card { background-color: var(--card); border-radius: 12px; padding:
|
|
388
|
+
.card { background-color: var(--card); border-radius: 12px; padding: 20px; box-shadow: var(--shadow); margin-bottom: 24px; animation: fadeIn 0.5s ease-out; }
|
|
363
389
|
h2 { font-size: 1.5em; font-weight: 600; margin: 0 0 20px; display: flex; justify-content: space-between; align-items: center;}
|
|
364
390
|
h3 { font-size: 1.1em; font-weight: 600; margin: 0 0 12px; text-align: center; }
|
|
365
391
|
.header-stats { display: flex; gap: 8px; font-size: 0.7em; font-weight: normal; }
|
|
366
392
|
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; }
|
|
367
|
-
.kpi-card { text-align: center; padding: 16px; border-radius: 8px; background-color: var(--bg); border: 1px solid var(--table-border); }
|
|
393
|
+
.kpi-card { text-align: center; padding: 16px; border-radius: 8px; background-color: var(--bg); border: 1px solid var(--table-border); transition: transform 0.2s, box-shadow 0.2s; cursor: pointer; }
|
|
394
|
+
.kpi-card:hover { transform: translateY(-4px); box-shadow: 0 8px 25px rgba(15,23,42,0.1); }
|
|
368
395
|
.kpi-value { font-size: 2em; font-weight: 700; color: var(--text); }
|
|
369
396
|
.kpi-label { color: var(--muted); font-size: 0.9em; }
|
|
370
|
-
.kpi-pass-rate .kpi-value { color: var(--pass-text); }
|
|
397
|
+
.kpi-pass-rate .kpi-value, .kpi-passed .kpi-value { color: var(--pass-text); }
|
|
371
398
|
.kpi-failed .kpi-value { color: var(--fail-text); }
|
|
399
|
+
.kpi-retries .kpi-value { color: var(--retry-text); }
|
|
400
|
+
.kpi-retries { border-left: 5px solid var(--retry-text); }
|
|
401
|
+
.kpi-consistency { border-left: 5px solid var(--consistency-text); background-color: var(--consistency-bg); }
|
|
402
|
+
.kpi-flaky-percent { border-left: 5px solid var(--flaky-text); background-color: var(--flaky-bg); }
|
|
403
|
+
.kpi-reproducibility { border-left: 5px solid var(--repro-text); background-color: var(--repro-bg); }
|
|
404
|
+
.kpi-consistency .kpi-value { color: var(--consistency-text); }
|
|
405
|
+
.kpi-flaky-percent .kpi-value { color: var(--flaky-text); }
|
|
406
|
+
.kpi-reproducibility .kpi-value { color: var(--repro-text); }
|
|
372
407
|
.status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-weight: 600; font-size: 0.8em; text-transform: uppercase; }
|
|
373
408
|
.status-passed { background-color: var(--pass-bg); color: var(--pass-text); }
|
|
374
409
|
.status-failed { background-color: var(--fail-bg); color: var(--fail-text); }
|
|
375
410
|
.status-skipped { background-color: var(--skipped-bg); color: var(--skipped-text); }
|
|
411
|
+
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: hidden; background-color: rgba(0,0,0,0.6); animation: modalFadeIn 0.3s; }
|
|
412
|
+
.modal-content { background-color: var(--card); margin: 5vh auto; padding: 20px; border: 1px solid var(--table-border); width: 90%; max-width: 1200px; border-radius: 12px; position: relative; max-height: 90vh; display: flex; flex-direction: column; }
|
|
413
|
+
.close-button { color: var(--muted); float: right; font-size: 28px; font-weight: bold; cursor: pointer; position: absolute; top: 15px; right: 20px; }
|
|
414
|
+
.modal-body { overflow-y: auto; flex-grow: 1; }
|
|
415
|
+
.modal table { font-size: 0.9em; min-width: 100%; }
|
|
376
416
|
table { width: 100%; border-collapse: collapse; }
|
|
377
417
|
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--table-border); }
|
|
378
418
|
th { background-color: var(--table-header-bg); font-weight: 600; }
|
|
379
|
-
tr:last-child td { border-bottom: none; }
|
|
380
419
|
.suite-header { cursor: pointer; padding: 12px 16px; background-color: var(--table-header-bg); border-bottom: 1px solid var(--table-border); display: flex; align-items: center; gap: 8px; font-weight: 600; }
|
|
381
420
|
.collapse-icon { width: 1em; height: 1em; text-align: center; transition: transform 0.2s; }
|
|
382
421
|
.collapse-icon.expanded { transform: rotate(90deg); }
|
|
383
|
-
.test-card-header { cursor: pointer; display: grid; grid-template-columns: 1fr auto auto auto; gap: 16px; align-items: center; padding: 8px 12px;}
|
|
422
|
+
.test-card-header { cursor: pointer; display: grid; grid-template-columns: 1fr auto auto auto auto; gap: 16px; align-items: center; padding: 8px 12px;}
|
|
384
423
|
.test-card-header:hover { background-color: var(--table-header-bg); }
|
|
385
424
|
.test-card { border-bottom: 1px solid var(--table-border); }
|
|
386
|
-
.log-row { background-color: var(--table-header-bg); }
|
|
425
|
+
.log-row, .stack-row { background-color: var(--table-header-bg); display: none; }
|
|
387
426
|
.log-container { padding: 16px; }
|
|
388
|
-
.log-container h4 { margin: 0 0 8px; text-align: left;}
|
|
389
427
|
.log-container pre { white-space: pre-wrap; word-wrap: break-word; background: var(--log-bg); padding: 12px; border-radius: 8px; max-height: 300px; overflow-y: auto; color: var(--muted); font-family: monospace; }
|
|
390
428
|
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 4px; }
|
|
391
429
|
.dot.passed { background-color: #4CAF50; }
|
|
@@ -396,19 +434,19 @@ tr:last-child td { border-bottom: none; }
|
|
|
396
434
|
.dark-mode-toggle:hover { background-color: var(--table-header-bg); }
|
|
397
435
|
.tab-content { display: none; }
|
|
398
436
|
.tab-content.active { display: block; }
|
|
399
|
-
.download-buttons { display: flex; gap: 10px; }
|
|
400
|
-
.download-buttons button { padding: 10px 16px; border: 1px solid var(--table-border); background-color: var(--card); color: var(--text); border-radius: 8px; cursor: pointer; transition: background-color 0.2s; }
|
|
401
|
-
.download-buttons button:hover { background-color: var(--table-header-bg); }
|
|
402
437
|
.chart-container { position: relative; display: flex; align-items: center; justify-content: center; color: var(--muted); }
|
|
403
438
|
.category-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 12px; border-bottom: 1px solid var(--table-border); }
|
|
404
439
|
.category-content { padding: 0 12px; }
|
|
405
440
|
.category-test { border-bottom: 1px solid var(--table-border); padding: 12px 0; }
|
|
406
|
-
.category-test:last-child { border-bottom: none; }
|
|
407
|
-
.category-test pre { background-color: var(--bg); padding: 8px; border-radius: 6px; margin-top: 8px; font-family: monospace; }
|
|
408
441
|
.metadata-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 20px;}
|
|
409
442
|
.metadata-item { background-color: var(--bg); padding: 12px; border-radius: 8px; border: 1px solid var(--table-border); }
|
|
410
443
|
.metadata-label { font-weight: 600; color: var(--muted); display: block; margin-bottom: 4px;}
|
|
411
|
-
.metadata-value { font-size: 1.
|
|
444
|
+
.metadata-value { font-size: 1.0em;}
|
|
445
|
+
.action-btn { background: none; border: 1px solid var(--muted); border-radius: 4px; padding: 2px 6px; cursor: pointer; font-size: 0.8em; margin-left: 5px; color: var(--text); }
|
|
446
|
+
.action-btn:hover { background-color: var(--table-header-bg); }
|
|
447
|
+
iframe { width: 100%; height: 80vh; border: none; border-radius: 8px; box-shadow: var(--shadow); }
|
|
448
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
449
|
+
@keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
412
450
|
</style>
|
|
413
451
|
</head>
|
|
414
452
|
<body>
|
|
@@ -423,6 +461,8 @@ tr:last-child td { border-bottom: none; }
|
|
|
423
461
|
<li class="sidebar-menu-item"><a href="#" data-tab="graphs">Graphs</a></li>
|
|
424
462
|
<li class="sidebar-menu-item"><a href="#" data-tab="timeline">Timeline</a></li>
|
|
425
463
|
<li class="sidebar-menu-item"><a href="#" data-tab="history">Historical Runs</a></li>
|
|
464
|
+
<li class="sidebar-menu-item" id="tagsMenu" style="display:none;"><a href="#" data-tab="tags">Tags</a></li>
|
|
465
|
+
<li class="sidebar-menu-item" id="htmlReportMenu" style="display:none;"><a href="#" data-tab="external-html">HTML Report</a></li>
|
|
426
466
|
</ul>
|
|
427
467
|
</div>
|
|
428
468
|
|
|
@@ -433,7 +473,6 @@ tr:last-child td { border-bottom: none; }
|
|
|
433
473
|
<button id="downloadHtml">Download HTML</button>
|
|
434
474
|
<button id="downloadCsv">Download CSV</button>
|
|
435
475
|
<button id="downloadJson">Download JSON</button>
|
|
436
|
-
<button id="downloadPdf">Download PDF</button>
|
|
437
476
|
</div>
|
|
438
477
|
<div class="actions">
|
|
439
478
|
<div class="filters">
|
|
@@ -478,21 +517,18 @@ tr:last-child td { border-bottom: none; }
|
|
|
478
517
|
<h3>Pass/Fail/Skip</h3>
|
|
479
518
|
<div class="chart-container" style="height: 300px;">
|
|
480
519
|
<canvas id="statusChart"></canvas>
|
|
481
|
-
<div>Loading Chart...</div>
|
|
482
520
|
</div>
|
|
483
521
|
</div>
|
|
484
522
|
<div style="flex: 1 1 400px; min-width: 400px;">
|
|
485
523
|
<h3>Duration Distribution</h3>
|
|
486
524
|
<div class="chart-container" style="height: 300px;">
|
|
487
525
|
<canvas id="durationHistogram"></canvas>
|
|
488
|
-
<div>Loading Chart...</div>
|
|
489
526
|
</div>
|
|
490
527
|
</div>
|
|
491
528
|
<div style="flex: 1 1 100%; min-width: 400px;">
|
|
492
529
|
<h3>Pass Rate Trend</h3>
|
|
493
530
|
<div class="chart-container" style="height: 300px;">
|
|
494
531
|
<canvas id="historyChart"></canvas>
|
|
495
|
-
<div>Loading Chart...</div>
|
|
496
532
|
</div>
|
|
497
533
|
</div>
|
|
498
534
|
</div>
|
|
@@ -504,7 +540,6 @@ tr:last-child td { border-bottom: none; }
|
|
|
504
540
|
<h2>Test Execution Timeline</h2>
|
|
505
541
|
<div id="timelineContainer" class="chart-container" style="height: 600px;">
|
|
506
542
|
<canvas id="timelineChart"></canvas>
|
|
507
|
-
<div>Loading Chart...</div>
|
|
508
543
|
</div>
|
|
509
544
|
</div>
|
|
510
545
|
</div>
|
|
@@ -514,25 +549,69 @@ tr:last-child td { border-bottom: none; }
|
|
|
514
549
|
<h2 id="historyRunsTitle">Historical Runs</h2>
|
|
515
550
|
<div id="historyGraphContainer" class="chart-container" style="height: 400px;">
|
|
516
551
|
<canvas id="historicalRunsChart"></canvas>
|
|
517
|
-
<div>Loading Chart...</div>
|
|
518
552
|
</div>
|
|
519
553
|
</div>
|
|
520
554
|
</div>
|
|
521
555
|
|
|
556
|
+
<div id="tags" class="tab-content">
|
|
557
|
+
<div class="card">
|
|
558
|
+
<h2>Tag Analysis</h2>
|
|
559
|
+
<div style="display: flex; flex-wrap: wrap; gap: 24px; margin-bottom: 24px;">
|
|
560
|
+
<div style="flex: 1 1 300px;">
|
|
561
|
+
<h3>Distribution by Tag</h3>
|
|
562
|
+
<div class="chart-container" style="height: 300px;">
|
|
563
|
+
<canvas id="tagsPieChart"></canvas>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
<div style="flex: 1 1 300px;">
|
|
567
|
+
<h3>Duration by Tag (Total)</h3>
|
|
568
|
+
<div class="chart-container" style="height: 300px;">
|
|
569
|
+
<canvas id="tagsBarChart"></canvas>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
<h3>Test List by Tag</h3>
|
|
574
|
+
<div id="tagsTableContainer"></div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div id="external-html" class="tab-content">
|
|
579
|
+
<div class="card">
|
|
580
|
+
<h2 style="margin-bottom: 10px;">Full HTML Report</h2>
|
|
581
|
+
<div id="iframeContainer" style="width:100%; height:80vh;"></div>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
|
|
522
585
|
</div>
|
|
523
586
|
</div>
|
|
524
587
|
|
|
588
|
+
<div id="testModal" class="modal">
|
|
589
|
+
<div class="modal-content">
|
|
590
|
+
<span class="close-button">×</span>
|
|
591
|
+
<h2 id="modalTitle"></h2>
|
|
592
|
+
<div class="modal-body">
|
|
593
|
+
<table>
|
|
594
|
+
<thead id="modalTableHeader"></thead>
|
|
595
|
+
<tbody id="modalTableBody"></tbody>
|
|
596
|
+
</table>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
525
600
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
526
601
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
527
602
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
|
528
603
|
|
|
529
604
|
<script>
|
|
530
605
|
const reportData = ${safeReportJson};
|
|
606
|
+
let allTests = [];
|
|
531
607
|
|
|
532
608
|
(function() {
|
|
533
609
|
const root = document.documentElement;
|
|
534
610
|
const darkModeToggle = document.getElementById('darkModeToggle');
|
|
535
|
-
const
|
|
611
|
+
const modal = document.getElementById('testModal');
|
|
612
|
+
const closeBtn = document.querySelector('.close-button');
|
|
613
|
+
const modalBody = document.getElementById('modalTableBody');
|
|
614
|
+
const modalHeader = document.getElementById('modalTableHeader');
|
|
536
615
|
|
|
537
616
|
function applyTheme(theme) {
|
|
538
617
|
if (theme === 'dark') {
|
|
@@ -551,12 +630,21 @@ const reportData = ${safeReportJson};
|
|
|
551
630
|
}
|
|
552
631
|
|
|
553
632
|
darkModeToggle.addEventListener('click', toggleTheme);
|
|
554
|
-
|
|
633
|
+
|
|
555
634
|
const savedTheme = localStorage.getItem('theme');
|
|
556
635
|
if (savedTheme) {
|
|
557
636
|
applyTheme(savedTheme);
|
|
558
637
|
}
|
|
559
|
-
|
|
638
|
+
|
|
639
|
+
// --- Show/Hide Dynamic Menus ---
|
|
640
|
+
if(reportData.config.hasExternalHtml) {
|
|
641
|
+
document.getElementById('htmlReportMenu').style.display = 'block';
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if(reportData.tagStats && reportData.tagStats.length > 0) {
|
|
645
|
+
document.getElementById('tagsMenu').style.display = 'block';
|
|
646
|
+
}
|
|
647
|
+
|
|
560
648
|
const tabs = document.querySelectorAll('.sidebar-menu-item a');
|
|
561
649
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
562
650
|
|
|
@@ -568,32 +656,150 @@ const reportData = ${safeReportJson};
|
|
|
568
656
|
e.target.classList.add('active');
|
|
569
657
|
tabContents.forEach(c => c.classList.remove('active'));
|
|
570
658
|
document.getElementById(targetId).classList.add('active');
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (targetId === '
|
|
574
|
-
|
|
659
|
+
|
|
660
|
+
// Fix: Handle External HTML rendering specifically
|
|
661
|
+
if (targetId === 'external-html') {
|
|
662
|
+
renderIframe();
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (targetId === 'graphs') { renderCharts(); }
|
|
667
|
+
if (targetId === 'timeline') { renderTimeline(); }
|
|
668
|
+
if (targetId === 'history') { renderHistoryGraph(); }
|
|
669
|
+
if (targetId === 'tags') { renderTags(); }
|
|
575
670
|
});
|
|
576
671
|
});
|
|
672
|
+
|
|
673
|
+
function getAllTestsFlat() {
|
|
674
|
+
if (allTests.length > 0) return allTests;
|
|
675
|
+
|
|
676
|
+
reportData.suites.forEach(suite => {
|
|
677
|
+
suite.specs.forEach(spec => {
|
|
678
|
+
spec.tests.forEach(test => {
|
|
679
|
+
const result = test.results[0];
|
|
680
|
+
const status = result?.status || 'skipped';
|
|
681
|
+
const isRetry = test.results.length > 1;
|
|
682
|
+
|
|
683
|
+
allTests.push({
|
|
684
|
+
suite: suite.title,
|
|
685
|
+
test: spec.title,
|
|
686
|
+
duration: result?.duration || 0,
|
|
687
|
+
status: status,
|
|
688
|
+
isRetry: isRetry
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
return allTests;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function openTestModal(filterStatus) {
|
|
697
|
+
const allTests = getAllTestsFlat();
|
|
698
|
+
let filteredTests = [];
|
|
699
|
+
let title = '';
|
|
700
|
+
|
|
701
|
+
if (filterStatus === 'total') {
|
|
702
|
+
filteredTests = allTests;
|
|
703
|
+
title = 'All Test Cases';
|
|
704
|
+
} else if (filterStatus === 'retries') {
|
|
705
|
+
filteredTests = allTests.filter(t => t.isRetry);
|
|
706
|
+
title = 'Tests With Retries (' + reportData.stats.retries + ')';
|
|
707
|
+
} else {
|
|
708
|
+
filteredTests = allTests.filter(t => t.status === filterStatus);
|
|
709
|
+
const count = reportData.stats[filterStatus];
|
|
710
|
+
title = filterStatus.charAt(0).toUpperCase() + filterStatus.slice(1) + ' Test Cases (' + count + ')';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
document.getElementById('modalTitle').textContent = title;
|
|
714
|
+
|
|
715
|
+
// FIX: String concatenation for safety
|
|
716
|
+
modalHeader.innerHTML = '<tr><th>Suite</th><th>Test Case</th><th>Status</th><th>Duration (s)</th></tr>';
|
|
717
|
+
|
|
718
|
+
modalBody.innerHTML = filteredTests.map(t =>
|
|
719
|
+
'<tr>' +
|
|
720
|
+
'<td>' + t.suite + '</td>' +
|
|
721
|
+
'<td>' + t.test + '</td>' +
|
|
722
|
+
'<td><span class="status-badge status-' + t.status + '">' + t.status + '</span></td>' +
|
|
723
|
+
'<td>' + (t.duration / 1000).toFixed(2) + '</td>' +
|
|
724
|
+
'</tr>'
|
|
725
|
+
).join('');
|
|
726
|
+
|
|
727
|
+
modal.style.display = 'block';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
closeBtn.onclick = function() {
|
|
731
|
+
modal.style.display = 'none';
|
|
732
|
+
}
|
|
733
|
+
window.onclick = function(event) {
|
|
734
|
+
if (event.target == modal) {
|
|
735
|
+
modal.style.display = 'none';
|
|
736
|
+
}
|
|
737
|
+
}
|
|
577
738
|
|
|
578
739
|
function renderOverview() {
|
|
740
|
+
getAllTestsFlat();
|
|
741
|
+
|
|
579
742
|
const { stats, metadata } = reportData;
|
|
580
743
|
const kpiGrid = document.getElementById('kpiGrid');
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
<div class="kpi-card kpi-
|
|
584
|
-
<div class="kpi-card kpi-
|
|
585
|
-
<div class="kpi-card kpi-
|
|
586
|
-
<div class="kpi-card kpi-
|
|
587
|
-
<div class="kpi-card kpi-
|
|
588
|
-
|
|
744
|
+
|
|
745
|
+
kpiGrid.innerHTML =
|
|
746
|
+
'<div class="kpi-card kpi-total" data-status="total"><div class="kpi-value">' + stats.total + '</div><div class="kpi-label">Total Tests</div></div>' +
|
|
747
|
+
'<div class="kpi-card kpi-passed" data-status="passed"><div class="kpi-value">' + stats.passed + '</div><div class="kpi-label">Passed</div></div>' +
|
|
748
|
+
'<div class="kpi-card kpi-failed" data-status="failed"><div class="kpi-value">' + stats.failed + '</div><div class="kpi-label">Failed</div></div>' +
|
|
749
|
+
'<div class="kpi-card kpi-retries" data-status="retries"><div class="kpi-value">' + stats.retries + '</div><div class="kpi-label">Retries</div></div>' +
|
|
750
|
+
'<div class="kpi-card kpi-skipped" data-status="skipped"><div class="kpi-value">' + stats.skipped + '</div><div class="kpi-label">Skipped</div></div>' +
|
|
751
|
+
'<div class="kpi-card kpi-pass-rate"><div class="kpi-value">' + stats.passRate + '</div><div class="kpi-label">Pass Rate</div></div>' +
|
|
752
|
+
'<div class="kpi-card kpi-duration"><div class="kpi-value">' + stats.totalDuration + '</div><div class="kpi-label">Total Duration</div></div>'+
|
|
753
|
+
'<div class="kpi-card kpi-consistency"><div class="kpi-value">' + stats.passRateConsistency + '</div><div class="kpi-label">Pass Rate Consistency (SD)</div></div>' +
|
|
754
|
+
'<div class="kpi-card kpi-flaky-percent"><div class="kpi-value">' + stats.flakyPercentage + '</div><div class="kpi-label">Flaky Test Percentage</div></div>' +
|
|
755
|
+
'<div class="kpi-card kpi-reproducibility"><div class="kpi-value">' + stats.failureReproducibility + '</div><div class="kpi-label">Failure Reproducibility</div></div>';
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
document.querySelectorAll('.kpi-card').forEach(card => {
|
|
759
|
+
const status = card.getAttribute('data-status');
|
|
760
|
+
if (status) {
|
|
761
|
+
card.addEventListener('click', () => openTestModal(status));
|
|
762
|
+
}
|
|
763
|
+
});
|
|
589
764
|
|
|
590
765
|
const metadataGrid = document.getElementById('metadataGrid');
|
|
591
|
-
metadataGrid.innerHTML =
|
|
592
|
-
<div class="metadata-item"><span class="metadata-label">Start Time</span><span class="metadata-value"
|
|
593
|
-
<div class="metadata-item"><span class="metadata-label">Total Duration</span><span class="metadata-value"
|
|
594
|
-
<div class="metadata-item"><span class="metadata-label">Browser</span><span class="metadata-value"
|
|
595
|
-
<div class="metadata-item"><span class="metadata-label">Operating System</span><span class="metadata-value"
|
|
596
|
-
|
|
766
|
+
metadataGrid.innerHTML =
|
|
767
|
+
'<div class="metadata-item"><span class="metadata-label">Start Time</span><span class="metadata-value">' + metadata.startTime + '</span></div>' +
|
|
768
|
+
'<div class="metadata-item"><span class="metadata-label">Total Duration</span><span class="metadata-value">' + metadata.totalDuration + '</span></div>' +
|
|
769
|
+
'<div class="metadata-item"><span class="metadata-label">Browser</span><span class="metadata-value">' + metadata.browser + '</span></div>' +
|
|
770
|
+
'<div class="metadata-item"><span class="metadata-label">Operating System</span><span class="metadata-value">' + metadata.os + '</span></div>';
|
|
771
|
+
|
|
772
|
+
const kpiValues = kpiGrid.querySelectorAll('.kpi-value');
|
|
773
|
+
kpiValues.forEach(kpiValueEl => {
|
|
774
|
+
const targetText = kpiValueEl.textContent;
|
|
775
|
+
const isPercentage = targetText.includes('%');
|
|
776
|
+
const isDuration = targetText.includes('s');
|
|
777
|
+
const isDecimal = targetText.includes('.');
|
|
778
|
+
let targetValue = parseFloat(targetText.replace(/[^0-9.]/g, ''));
|
|
779
|
+
if (isNaN(targetValue)) targetValue = 0;
|
|
780
|
+
const startValue = 0;
|
|
781
|
+
const duration = 2000;
|
|
782
|
+
let startTime = null;
|
|
783
|
+
function animate(currentTime) {
|
|
784
|
+
if (!startTime) startTime = currentTime;
|
|
785
|
+
const progress = Math.min((currentTime - startTime) / duration, 1);
|
|
786
|
+
const easedProgress = 1 - Math.pow(1 - progress, 3);
|
|
787
|
+
let currentValue = startValue + (targetValue - startValue) * easedProgress;
|
|
788
|
+
if (progress < 1) {
|
|
789
|
+
if (isPercentage || isDecimal) {
|
|
790
|
+
kpiValueEl.textContent = currentValue.toFixed(2) + (isPercentage ? '%' : '');
|
|
791
|
+
} else if (isDuration) {
|
|
792
|
+
kpiValueEl.textContent = currentValue.toFixed(2) + 's';
|
|
793
|
+
} else {
|
|
794
|
+
kpiValueEl.textContent = Math.round(currentValue);
|
|
795
|
+
}
|
|
796
|
+
requestAnimationFrame(animate);
|
|
797
|
+
} else {
|
|
798
|
+
kpiValueEl.textContent = targetText;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
requestAnimationFrame(animate);
|
|
802
|
+
});
|
|
597
803
|
}
|
|
598
804
|
|
|
599
805
|
function renderCategories() {
|
|
@@ -603,22 +809,22 @@ const reportData = ${safeReportJson};
|
|
|
603
809
|
categoryList.innerHTML = '<p>No failed tests categorized.</p>';
|
|
604
810
|
return;
|
|
605
811
|
}
|
|
606
|
-
categoryList.innerHTML = categories.map(cat =>
|
|
607
|
-
<div class="card">
|
|
608
|
-
<div class="category-header">
|
|
609
|
-
<span
|
|
610
|
-
<span class="status-badge status-failed"
|
|
611
|
-
</div>
|
|
612
|
-
<div class="category-content" style="display: none;">
|
|
613
|
-
|
|
614
|
-
<div class="category-test">
|
|
615
|
-
<strong
|
|
616
|
-
<pre
|
|
617
|
-
</div>
|
|
618
|
-
|
|
619
|
-
</div>
|
|
620
|
-
</div>
|
|
621
|
-
|
|
812
|
+
categoryList.innerHTML = categories.map(cat =>
|
|
813
|
+
'<div class="card">' +
|
|
814
|
+
'<div class="category-header">' +
|
|
815
|
+
'<span>' + cat.name + '</span>' +
|
|
816
|
+
'<span class="status-badge status-failed">' + cat.count + ' failed</span>' +
|
|
817
|
+
'</div>' +
|
|
818
|
+
'<div class="category-content" style="display: none;">' +
|
|
819
|
+
cat.tests.map(test =>
|
|
820
|
+
'<div class="category-test">' +
|
|
821
|
+
'<strong>' + test.title + '</strong>' +
|
|
822
|
+
'<pre>' + test.trace + '</pre>' +
|
|
823
|
+
'</div>'
|
|
824
|
+
).join('') +
|
|
825
|
+
'</div>' +
|
|
826
|
+
'</div>'
|
|
827
|
+
).join('');
|
|
622
828
|
|
|
623
829
|
document.querySelectorAll('.category-header').forEach(header => {
|
|
624
830
|
header.addEventListener('click', () => {
|
|
@@ -633,44 +839,54 @@ const reportData = ${safeReportJson};
|
|
|
633
839
|
const suitesContainer = document.getElementById('suitesContainer');
|
|
634
840
|
const suitesHeader = document.getElementById('suitesHeader');
|
|
635
841
|
|
|
636
|
-
suitesHeader.innerHTML =
|
|
637
|
-
Suites & Tests
|
|
638
|
-
<div class="header-stats">
|
|
639
|
-
<span class="status-badge status-passed"
|
|
640
|
-
<span class="status-badge status-failed"
|
|
641
|
-
<span class="status-badge status-skipped"
|
|
642
|
-
</div>
|
|
643
|
-
\`;
|
|
842
|
+
suitesHeader.innerHTML =
|
|
843
|
+
'Suites & Tests' +
|
|
844
|
+
'<div class="header-stats">' +
|
|
845
|
+
'<span class="status-badge status-passed">' + stats.passed + ' Passed</span>' +
|
|
846
|
+
'<span class="status-badge status-failed">' + stats.failed + ' Failed</span>' +
|
|
847
|
+
'<span class="status-badge status-skipped">' + stats.skipped + ' Skipped</span>' +
|
|
848
|
+
'</div>';
|
|
644
849
|
|
|
645
|
-
suitesContainer.innerHTML = suites.map(suite =>
|
|
646
|
-
<div class="card">
|
|
647
|
-
<div class="suite-header">
|
|
648
|
-
<svg class="collapse-icon" fill="currentColor" viewBox="0 0 20 20" style="width:20px; height:20px;"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
|
|
649
|
-
|
|
650
|
-
</div>
|
|
651
|
-
<div class="suite-content" style="display: none;">
|
|
652
|
-
|
|
653
|
-
spec.tests.map(test =>
|
|
654
|
-
<div class="test-card">
|
|
655
|
-
<div class="test-card-header">
|
|
656
|
-
<span style="font-weight: 500;"
|
|
657
|
-
<span class="status-badge status
|
|
658
|
-
<span
|
|
659
|
-
<span
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
850
|
+
suitesContainer.innerHTML = suites.map(suite =>
|
|
851
|
+
'<div class="card">' +
|
|
852
|
+
'<div class="suite-header">' +
|
|
853
|
+
'<svg class="collapse-icon" fill="currentColor" viewBox="0 0 20 20" style="width:20px; height:20px;"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>' +
|
|
854
|
+
' ' + suite.title +
|
|
855
|
+
'</div>' +
|
|
856
|
+
'<div class="suite-content" style="display: none;">' +
|
|
857
|
+
suite.specs.flatMap(spec =>
|
|
858
|
+
spec.tests.map(test =>
|
|
859
|
+
'<div class="test-card">' +
|
|
860
|
+
'<div class="test-card-header">' +
|
|
861
|
+
'<span style="font-weight: 500;">' + spec.title + '</span>' +
|
|
862
|
+
'<span class="status-badge status-' + test.status + '">' + test.status + ' ' + (test.flaky ? '<span class="flaky-badge">FLAKY</span>' : '') + '</span>' +
|
|
863
|
+
'<span>' + (test.duration / 1000).toFixed(2) + 's</span>' +
|
|
864
|
+
'<span>' + test.history.map(h => '<span class="dot ' + h + '"></span>').join('') + '</span>' +
|
|
865
|
+
'<span>' +
|
|
866
|
+
(test.logs ? '<button class="action-btn toggle-logs">Logs</button>' : '') +
|
|
867
|
+
(test.stack ? '<button class="action-btn toggle-stack">Stack</button>' : '') +
|
|
868
|
+
'</span>' +
|
|
869
|
+
'</div>' +
|
|
870
|
+
(test.logs ?
|
|
871
|
+
'<div class="log-row" style="display: none;">' +
|
|
872
|
+
'<div class="log-container">' +
|
|
873
|
+
'<h4>Logs</h4>' +
|
|
874
|
+
'<pre>' + test.logs.replace(/</g, "<").replace(/>/g, ">") + '</pre>' +
|
|
875
|
+
'</div>' +
|
|
876
|
+
'</div>' : '') +
|
|
877
|
+
(test.stack ?
|
|
878
|
+
'<div class="stack-row" style="display: none;">' +
|
|
879
|
+
'<div class="log-container">' +
|
|
880
|
+
'<h4>Error Stack</h4>' +
|
|
881
|
+
'<pre>' + test.stack.replace(/</g, "<").replace(/>/g, ">") + '</pre>' +
|
|
882
|
+
'</div>' +
|
|
883
|
+
'</div>' : '') +
|
|
884
|
+
'</div>'
|
|
885
|
+
)
|
|
886
|
+
).join('') +
|
|
887
|
+
'</div>' +
|
|
888
|
+
'</div>'
|
|
889
|
+
).join('');
|
|
674
890
|
|
|
675
891
|
document.querySelectorAll('.suite-header').forEach(header => {
|
|
676
892
|
header.addEventListener('click', () => {
|
|
@@ -682,22 +898,117 @@ const reportData = ${safeReportJson};
|
|
|
682
898
|
});
|
|
683
899
|
});
|
|
684
900
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
901
|
+
// Toggle Logs
|
|
902
|
+
document.querySelectorAll('.toggle-logs').forEach(btn => {
|
|
903
|
+
btn.addEventListener('click', (e) => {
|
|
904
|
+
e.stopPropagation();
|
|
905
|
+
const card = btn.closest('.test-card');
|
|
906
|
+
const logRow = card.querySelector('.log-row');
|
|
907
|
+
if(logRow) logRow.style.display = logRow.style.display === 'none' ? 'block' : 'none';
|
|
691
908
|
});
|
|
692
909
|
});
|
|
910
|
+
|
|
911
|
+
// Toggle Stack
|
|
912
|
+
document.querySelectorAll('.toggle-stack').forEach(btn => {
|
|
913
|
+
btn.addEventListener('click', (e) => {
|
|
914
|
+
e.stopPropagation();
|
|
915
|
+
const card = btn.closest('.test-card');
|
|
916
|
+
const stackRow = card.querySelector('.stack-row');
|
|
917
|
+
if(stackRow) stackRow.style.display = stackRow.style.display === 'none' ? 'block' : 'none';
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// Make entire test-card-header clickable to toggle both Logs and Stack
|
|
922
|
+
document.querySelectorAll('.test-card-header').forEach(header => {
|
|
923
|
+
header.addEventListener('click', (e) => {
|
|
924
|
+
const card = header.closest('.test-card');
|
|
925
|
+
if (!card) return;
|
|
926
|
+
const logRow = card.querySelector('.log-row');
|
|
927
|
+
const stackRow = card.querySelector('.stack-row');
|
|
928
|
+
if (logRow) logRow.style.display = logRow.style.display === 'none' ? 'block' : 'none';
|
|
929
|
+
if (stackRow) stackRow.style.display = stackRow.style.display === 'none' ? 'block' : 'none';
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function renderIframe() {
|
|
935
|
+
const container = document.getElementById('iframeContainer');
|
|
936
|
+
// Always refresh if path exists to prevent blank display on tab switch
|
|
937
|
+
if (reportData.config.hasExternalHtml && reportData.config.externalHtmlPath) {
|
|
938
|
+
let src = reportData.config.externalHtmlPath;
|
|
939
|
+
// Optimization: only set innerHTML once, but ensure it's correct
|
|
940
|
+
if (container.innerHTML === "") {
|
|
941
|
+
container.innerHTML = '<iframe src="' + src + '" style="height:80vh; width:100%; border:none;"></iframe>';
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
container.innerHTML = '<p style="padding:20px; text-align:center; color:var(--muted);">External HTML report not found at ' + reportData.config.externalHtmlPath + '</p>';
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function renderTags() {
|
|
949
|
+
const stats = reportData.tagStats;
|
|
950
|
+
if (!stats || stats.length === 0) return;
|
|
951
|
+
|
|
952
|
+
// 1. Pie Chart
|
|
953
|
+
const pieCtx = document.getElementById('tagsPieChart');
|
|
954
|
+
if (pieCtx && !pieCtx.chart) {
|
|
955
|
+
new Chart(pieCtx, {
|
|
956
|
+
type: 'pie',
|
|
957
|
+
data: {
|
|
958
|
+
labels: stats.map(s => s.name),
|
|
959
|
+
datasets: [{
|
|
960
|
+
data: stats.map(s => s.total),
|
|
961
|
+
backgroundColor: [
|
|
962
|
+
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40'
|
|
963
|
+
]
|
|
964
|
+
}]
|
|
965
|
+
},
|
|
966
|
+
options: { responsive: true, maintainAspectRatio: false }
|
|
967
|
+
});
|
|
968
|
+
pieCtx.chart = true; // Flag to prevent re-render
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// 2. Bar Chart (Duration)
|
|
972
|
+
const barCtx = document.getElementById('tagsBarChart');
|
|
973
|
+
if (barCtx && !barCtx.chart) {
|
|
974
|
+
new Chart(barCtx, {
|
|
975
|
+
type: 'bar',
|
|
976
|
+
data: {
|
|
977
|
+
labels: stats.map(s => s.name),
|
|
978
|
+
datasets: [{
|
|
979
|
+
label: 'Total Duration (s)',
|
|
980
|
+
data: stats.map(s => s.duration / 1000),
|
|
981
|
+
backgroundColor: '#36A2EB'
|
|
982
|
+
}]
|
|
983
|
+
},
|
|
984
|
+
options: { responsive: true, maintainAspectRatio: false }
|
|
985
|
+
});
|
|
986
|
+
barCtx.chart = true;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// 3. Table
|
|
990
|
+
const tableContainer = document.getElementById('tagsTableContainer');
|
|
991
|
+
let tableHtml = '<table><thead><tr><th>Tag</th><th>Test Case</th><th>Status</th><th>Duration (s)</th></tr></thead><tbody>';
|
|
992
|
+
|
|
993
|
+
stats.forEach(tag => {
|
|
994
|
+
tag.tests.forEach(t => {
|
|
995
|
+
tableHtml += '<tr>' +
|
|
996
|
+
'<td><span class="status-badge" style="background:#eee; color:#333;">' + tag.name + '</span></td>' +
|
|
997
|
+
'<td>' + t.title + '</td>' +
|
|
998
|
+
'<td><span class="status-badge status-' + t.status + '">' + t.status + '</span></td>' +
|
|
999
|
+
'<td>' + (t.duration / 1000).toFixed(2) + '</td>' +
|
|
1000
|
+
'</tr>';
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
tableHtml += '</tbody></table>';
|
|
1004
|
+
tableContainer.innerHTML = tableHtml;
|
|
693
1005
|
}
|
|
694
1006
|
|
|
695
1007
|
let statusChart, durationChart, historyChart, timelineChart, historicalRunsChart;
|
|
696
1008
|
|
|
697
1009
|
function renderCharts() {
|
|
698
1010
|
const statusCtx = document.getElementById('statusChart');
|
|
699
|
-
|
|
700
|
-
statusChart = new Chart(statusCtx, {
|
|
1011
|
+
statusChart = new Chart(statusCtx, {
|
|
701
1012
|
type: 'doughnut',
|
|
702
1013
|
data: {
|
|
703
1014
|
labels: ['Passed', 'Failed', 'Skipped'],
|
|
@@ -706,16 +1017,22 @@ const reportData = ${safeReportJson};
|
|
|
706
1017
|
backgroundColor: ['#4CAF50', '#F44336', '#9E9E9E'],
|
|
707
1018
|
}]
|
|
708
1019
|
},
|
|
709
|
-
options: {
|
|
1020
|
+
options: {
|
|
1021
|
+
responsive: true,
|
|
1022
|
+
maintainAspectRatio: false,
|
|
1023
|
+
animation: {
|
|
1024
|
+
animateRotate: true,
|
|
1025
|
+
animateScale: true,
|
|
1026
|
+
duration: 1500,
|
|
1027
|
+
easing: 'easeOutQuart'
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
710
1030
|
});
|
|
711
1031
|
|
|
712
|
-
// Duration Histogram
|
|
713
1032
|
const histogramCtx = document.getElementById('durationHistogram');
|
|
714
|
-
histogramCtx.nextElementSibling.style.display = 'none';
|
|
715
1033
|
const durations = reportData.suites.flatMap(s => s.specs.flatMap(sp => sp.tests.map(t => t.duration / 1000)));
|
|
716
1034
|
const labels = ['<1s', '1-2s', '2-5s', '5-10s', '10-30s', '30-60s', '>60s'];
|
|
717
1035
|
const data = new Array(labels.length).fill(0);
|
|
718
|
-
|
|
719
1036
|
durations.forEach(d => {
|
|
720
1037
|
if (d < 1) data[0]++;
|
|
721
1038
|
else if (d < 2) data[1]++;
|
|
@@ -725,7 +1042,6 @@ const reportData = ${safeReportJson};
|
|
|
725
1042
|
else if (d < 60) data[5]++;
|
|
726
1043
|
else data[6]++;
|
|
727
1044
|
});
|
|
728
|
-
|
|
729
1045
|
new Chart(histogramCtx, {
|
|
730
1046
|
type: 'bar',
|
|
731
1047
|
data: {
|
|
@@ -737,15 +1053,16 @@ const reportData = ${safeReportJson};
|
|
|
737
1053
|
}]
|
|
738
1054
|
},
|
|
739
1055
|
options: {
|
|
740
|
-
responsive: true,
|
|
741
|
-
|
|
1056
|
+
responsive: true, maintainAspectRatio: false,
|
|
1057
|
+
animation: {
|
|
1058
|
+
duration: 1200,
|
|
1059
|
+
easing: 'easeOutQuart'
|
|
1060
|
+
},
|
|
742
1061
|
scales: { y: { beginAtZero: true, title: { display: true, text: 'Test Count' } } }
|
|
743
1062
|
}
|
|
744
1063
|
});
|
|
745
1064
|
|
|
746
|
-
|
|
747
1065
|
const historyCtx = document.getElementById('historyChart');
|
|
748
|
-
historyCtx.nextElementSibling.style.display = 'none';
|
|
749
1066
|
historyChart = new Chart(historyCtx, {
|
|
750
1067
|
type: 'line',
|
|
751
1068
|
data: {
|
|
@@ -758,19 +1075,24 @@ const reportData = ${safeReportJson};
|
|
|
758
1075
|
tension: 0.1
|
|
759
1076
|
}]
|
|
760
1077
|
},
|
|
761
|
-
options: {
|
|
1078
|
+
options: {
|
|
1079
|
+
responsive: true,
|
|
1080
|
+
maintainAspectRatio: false,
|
|
1081
|
+
animation: {
|
|
1082
|
+
duration: 1200,
|
|
1083
|
+
easing: 'easeOutQuart'
|
|
1084
|
+
},
|
|
1085
|
+
scales: { y: { beginAtZero: true, max: 100 } }
|
|
1086
|
+
}
|
|
762
1087
|
});
|
|
763
1088
|
}
|
|
764
1089
|
|
|
765
1090
|
function renderTimeline() {
|
|
766
1091
|
const timelineCtx = document.getElementById('timelineChart');
|
|
767
|
-
timelineCtx.nextElementSibling.style.display = 'none';
|
|
768
|
-
|
|
769
|
-
// Transform timeline data for Chart.js gantt-like chart
|
|
770
1092
|
const labels = reportData.timeline.map(t => t.name);
|
|
771
1093
|
const datasets = [{
|
|
772
1094
|
label: 'Execution Time',
|
|
773
|
-
data: reportData.timeline.map(t => [t.start, t.start + t.duration]),
|
|
1095
|
+
data: reportData.timeline.map(t => [t.start / 1000, (t.start + t.duration) / 1000]),
|
|
774
1096
|
backgroundColor: reportData.timeline.map(t => {
|
|
775
1097
|
if (t.status === 'passed') return 'rgba(76, 175, 80, 0.8)';
|
|
776
1098
|
if (t.status === 'failed') return 'rgba(244, 67, 54, 0.8)';
|
|
@@ -778,23 +1100,20 @@ const reportData = ${safeReportJson};
|
|
|
778
1100
|
}),
|
|
779
1101
|
}];
|
|
780
1102
|
|
|
781
|
-
// Only render chart if there is data
|
|
782
1103
|
if (reportData.timeline.length > 0) {
|
|
783
|
-
timelineCtx.parentElement.style.height = (reportData.timeline.length * 30 + 100) + 'px';
|
|
1104
|
+
timelineCtx.parentElement.style.height = (reportData.timeline.length * 30 + 100) + 'px';
|
|
784
1105
|
timelineChart = new Chart(timelineCtx, {
|
|
785
1106
|
type: 'bar',
|
|
786
|
-
data: {
|
|
787
|
-
labels: labels,
|
|
788
|
-
datasets: datasets
|
|
789
|
-
},
|
|
1107
|
+
data: { labels: labels, datasets: datasets },
|
|
790
1108
|
options: {
|
|
791
1109
|
indexAxis: 'y',
|
|
792
|
-
responsive: true,
|
|
793
|
-
maintainAspectRatio: false,
|
|
1110
|
+
responsive: true, maintainAspectRatio: false,
|
|
794
1111
|
plugins: { legend: { display: false } },
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1112
|
+
animation: {
|
|
1113
|
+
duration: 2000,
|
|
1114
|
+
easing: 'easeOutQuart'
|
|
1115
|
+
},
|
|
1116
|
+
scales: { x: { min: 0, title: { display: true, text: 'Time (s)' } } }
|
|
798
1117
|
}
|
|
799
1118
|
});
|
|
800
1119
|
} else {
|
|
@@ -804,11 +1123,8 @@ const reportData = ${safeReportJson};
|
|
|
804
1123
|
|
|
805
1124
|
function renderHistoryGraph() {
|
|
806
1125
|
const history = reportData.history;
|
|
807
|
-
document.getElementById('historyRunsTitle').textContent =
|
|
1126
|
+
document.getElementById('historyRunsTitle').textContent = 'Historical Runs (Count: ' + history.length + ')';
|
|
808
1127
|
const historyCtx = document.getElementById('historicalRunsChart');
|
|
809
|
-
historyCtx.nextElementSibling.style.display = 'none';
|
|
810
|
-
|
|
811
|
-
// Only render chart if there is data
|
|
812
1128
|
if (history.length > 0) {
|
|
813
1129
|
historicalRunsChart = new Chart(historyCtx, {
|
|
814
1130
|
type: 'bar',
|
|
@@ -816,60 +1132,24 @@ const reportData = ${safeReportJson};
|
|
|
816
1132
|
labels: history.map(h => h.date),
|
|
817
1133
|
datasets: [
|
|
818
1134
|
{
|
|
819
|
-
type: 'line',
|
|
820
|
-
|
|
821
|
-
data: history.map(h => h.passRate),
|
|
822
|
-
borderColor: 'rgb(255, 159, 64)',
|
|
823
|
-
yAxisID: 'y1',
|
|
824
|
-
tension: 0.1,
|
|
825
|
-
pointRadius: 5,
|
|
826
|
-
pointBackgroundColor: 'rgb(255, 159, 64)',
|
|
827
|
-
order: 0, // Draw line on top of bars
|
|
1135
|
+
type: 'line', label: 'Pass Rate', data: history.map(h => h.passRate),
|
|
1136
|
+
borderColor: 'rgb(255, 159, 64)', yAxisID: 'y1', tension: 0.1, pointRadius: 5, pointBackgroundColor: 'rgb(255, 159, 64)', order: 0,
|
|
828
1137
|
},
|
|
829
|
-
{
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
data: history.map(h => h.passed),
|
|
833
|
-
backgroundColor: 'rgba(75, 192, 192, 0.8)',
|
|
834
|
-
order: 1,
|
|
835
|
-
},
|
|
836
|
-
{
|
|
837
|
-
type: 'bar',
|
|
838
|
-
label: 'Failed',
|
|
839
|
-
data: history.map(h => h.failed),
|
|
840
|
-
backgroundColor: 'rgba(255, 99, 132, 0.8)',
|
|
841
|
-
order: 1,
|
|
842
|
-
},
|
|
843
|
-
{
|
|
844
|
-
type: 'bar',
|
|
845
|
-
label: 'Skipped',
|
|
846
|
-
data: history.map(h => h.skipped),
|
|
847
|
-
backgroundColor: 'rgba(158, 158, 158, 0.8)',
|
|
848
|
-
order: 1,
|
|
849
|
-
}
|
|
1138
|
+
{ type: 'bar', label: 'Passed', data: history.map(h => h.passed), backgroundColor: '#4CAF50', order: 1 },
|
|
1139
|
+
{ type: 'bar', label: 'Failed', data: history.map(h => h.failed), backgroundColor: 'rgba(255, 99, 132, 0.8)', order: 1 },
|
|
1140
|
+
{ type: 'bar', label: 'Skipped', data: history.map(h => h.skipped), backgroundColor: 'rgba(158, 158, 158, 0.8)', order: 1 }
|
|
850
1141
|
]
|
|
851
1142
|
},
|
|
852
1143
|
options: {
|
|
853
|
-
responsive: true,
|
|
854
|
-
|
|
1144
|
+
responsive: true, maintainAspectRatio: false,
|
|
1145
|
+
animation: {
|
|
1146
|
+
duration: 1200,
|
|
1147
|
+
easing: 'easeOutQuart'
|
|
1148
|
+
},
|
|
855
1149
|
scales: {
|
|
856
1150
|
x: { stacked: true, },
|
|
857
|
-
y: {
|
|
858
|
-
|
|
859
|
-
beginAtZero: true,
|
|
860
|
-
title: { display: true, text: 'Test Count' }
|
|
861
|
-
},
|
|
862
|
-
y1: {
|
|
863
|
-
position: 'right',
|
|
864
|
-
beginAtZero: true,
|
|
865
|
-
max: 100,
|
|
866
|
-
title: { display: true, text: 'Pass Rate (%)' },
|
|
867
|
-
grid: { drawOnChartArea: false }
|
|
868
|
-
}
|
|
869
|
-
},
|
|
870
|
-
tooltips: {
|
|
871
|
-
mode: 'index',
|
|
872
|
-
intersect: false
|
|
1151
|
+
y: { stacked: true, beginAtZero: true, title: { display: true, text: 'Test Count' } },
|
|
1152
|
+
y1: { position: 'right', beginAtZero: true, max: 100, title: { display: true, text: 'Pass Rate (%)' }, grid: { drawOnChartArea: false } }
|
|
873
1153
|
}
|
|
874
1154
|
}
|
|
875
1155
|
});
|
|
@@ -886,22 +1166,16 @@ const reportData = ${safeReportJson};
|
|
|
886
1166
|
const showPassed = filterPassed.checked;
|
|
887
1167
|
const showFailed = filterFailed.checked;
|
|
888
1168
|
const showSkipped = filterSkipped.checked;
|
|
889
|
-
|
|
890
|
-
// Hide all log rows initially when filters change
|
|
891
|
-
document.querySelectorAll('.log-row').forEach(logRow => {
|
|
892
|
-
logRow.style.display = 'none';
|
|
893
|
-
});
|
|
894
|
-
|
|
1169
|
+
document.querySelectorAll('.log-row').forEach(logRow => { logRow.style.display = 'none'; });
|
|
895
1170
|
document.querySelectorAll('.test-card').forEach(card => {
|
|
896
1171
|
const header = card.querySelector('.test-card-header');
|
|
897
1172
|
const status = header.querySelector('.status-badge').textContent.toLowerCase();
|
|
898
1173
|
let display = false;
|
|
899
|
-
if (status
|
|
900
|
-
if (status
|
|
901
|
-
if (status
|
|
1174
|
+
if (status.includes('passed') && showPassed) display = true;
|
|
1175
|
+
if (status.includes('failed') && showFailed) display = true;
|
|
1176
|
+
if (status.includes('skipped') && showSkipped) display = true;
|
|
902
1177
|
card.style.display = display ? '' : 'none';
|
|
903
1178
|
|
|
904
|
-
// Hide the parent suite card if all its tests are hidden
|
|
905
1179
|
let parentSuiteContent = card.closest('.suite-content');
|
|
906
1180
|
if (parentSuiteContent) {
|
|
907
1181
|
let visibleTests = Array.from(parentSuiteContent.querySelectorAll('.test-card')).filter(t => t.style.display !== 'none');
|
|
@@ -935,40 +1209,24 @@ const reportData = ${safeReportJson};
|
|
|
935
1209
|
URL.revokeObjectURL(a.href);
|
|
936
1210
|
});
|
|
937
1211
|
document.getElementById('downloadCsv').addEventListener('click', () => {
|
|
938
|
-
let csvContent = "data:text/csv;charset=utf-8,Suite,Test,Status,Duration (s),History\\n";
|
|
1212
|
+
let csvContent = "data:text/csv;charset=utf-8,Suite,Test,Status,Duration (s),Flaky,History\\n";
|
|
939
1213
|
reportData.suites.forEach(suite => {
|
|
940
1214
|
suite.specs.forEach(spec => {
|
|
941
1215
|
const testData = spec.tests && spec.tests[0];
|
|
942
1216
|
const status = testData ? testData.status : 'N/A';
|
|
943
1217
|
const duration = testData ? (testData.duration / 1000).toFixed(2) : 'N/A';
|
|
1218
|
+
const isFlaky = testData?.flaky ? 'Yes' : 'No';
|
|
944
1219
|
const history = testData && testData.history ? testData.history.join(" | ") : 'N/A';
|
|
945
|
-
|
|
946
|
-
const
|
|
1220
|
+
const safeSuiteTitle = '"' + suite.title.replace(/"/g,'""') + '"';
|
|
1221
|
+
const safeSpecTitle = '"' + spec.title.replace(/"/g,'""') + '"';
|
|
1222
|
+
const row = [safeSuiteTitle, safeSpecTitle, status, duration, isFlaky, history].join(",");
|
|
947
1223
|
csvContent += row + '\\n';
|
|
948
1224
|
});
|
|
949
1225
|
});
|
|
950
1226
|
const encodedUri = encodeURI(csvContent);
|
|
951
1227
|
const a = document.createElement('a'); a.href = encodedUri; a.download = 'test_report.csv'; a.click();
|
|
952
1228
|
});
|
|
953
|
-
|
|
954
|
-
const doc = new window.jspdf.jsPDF();
|
|
955
|
-
const element = document.querySelector('.main-content');
|
|
956
|
-
html2canvas(element, { scale: 2 }).then(canvas => {
|
|
957
|
-
const imgData = canvas.toDataURL('image/png');
|
|
958
|
-
const imgWidth = 210; const pageHeight = 295;
|
|
959
|
-
const imgHeight = canvas.height * imgWidth / canvas.width;
|
|
960
|
-
let heightLeft = imgHeight; let position = 0;
|
|
961
|
-
doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
|
962
|
-
heightLeft -= pageHeight;
|
|
963
|
-
while (heightLeft >= 0) {
|
|
964
|
-
position = heightLeft - imgHeight;
|
|
965
|
-
doc.addPage();
|
|
966
|
-
doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
|
967
|
-
}
|
|
968
|
-
doc.save('report.pdf');
|
|
969
|
-
});
|
|
970
|
-
});
|
|
971
|
-
|
|
1229
|
+
|
|
972
1230
|
renderOverview();
|
|
973
1231
|
renderCategories();
|
|
974
1232
|
renderSuites();
|