@flash-ai-team/flash-test-framework 0.0.15 → 0.0.17
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.
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
declare class CustomReporter implements Reporter {
|
|
3
3
|
private results;
|
|
4
|
-
private
|
|
4
|
+
private executionLogBuffer;
|
|
5
5
|
private reportFolder;
|
|
6
6
|
private log;
|
|
7
7
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
8
8
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
9
|
+
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
|
|
10
|
+
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
|
|
9
11
|
onEnd(result: FullResult): void;
|
|
10
12
|
}
|
|
11
13
|
export default CustomReporter;
|
|
@@ -39,12 +39,12 @@ const ReporterUtils_1 = require("./ReporterUtils");
|
|
|
39
39
|
class CustomReporter {
|
|
40
40
|
constructor() {
|
|
41
41
|
this.results = [];
|
|
42
|
-
this.
|
|
42
|
+
this.executionLogBuffer = []; // Unified buffer
|
|
43
43
|
this.reportFolder = '';
|
|
44
44
|
}
|
|
45
45
|
log(message) {
|
|
46
46
|
console.log(message);
|
|
47
|
-
this.
|
|
47
|
+
this.executionLogBuffer.push(message + '\n');
|
|
48
48
|
}
|
|
49
49
|
onBegin(config, suite) {
|
|
50
50
|
this.reportFolder = (0, ReporterUtils_1.getReportFolder)(suite);
|
|
@@ -93,17 +93,15 @@ class CustomReporter {
|
|
|
93
93
|
lines.forEach(line => this.log(line));
|
|
94
94
|
// Write to individual file
|
|
95
95
|
if (this.reportFolder) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Safest is to ensure it exists.
|
|
99
|
-
if (!fs.existsSync(this.reportFolder)) {
|
|
96
|
+
const logsDir = path.join(this.reportFolder, 'logs');
|
|
97
|
+
if (!fs.existsSync(logsDir)) {
|
|
100
98
|
try {
|
|
101
|
-
fs.mkdirSync(
|
|
99
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
102
100
|
}
|
|
103
101
|
catch (e) { /* ignore race */ }
|
|
104
102
|
}
|
|
105
103
|
const sanitizedTitle = test.title.replace(/[^a-z0-9]/gi, '_');
|
|
106
|
-
const individualLogPath = path.join(
|
|
104
|
+
const individualLogPath = path.join(logsDir, `execution_${sanitizedTitle}.log`);
|
|
107
105
|
try {
|
|
108
106
|
fs.writeFileSync(individualLogPath, lines.join('\n'));
|
|
109
107
|
}
|
|
@@ -112,17 +110,31 @@ class CustomReporter {
|
|
|
112
110
|
}
|
|
113
111
|
}
|
|
114
112
|
}
|
|
113
|
+
onStdOut(chunk, test, result) {
|
|
114
|
+
const text = chunk.toString();
|
|
115
|
+
process.stdout.write(text); // Print to real console
|
|
116
|
+
this.executionLogBuffer.push(text); // Add raw text to unified log
|
|
117
|
+
}
|
|
118
|
+
onStdErr(chunk, test, result) {
|
|
119
|
+
const text = chunk.toString();
|
|
120
|
+
process.stderr.write(text); // Print to real stderr
|
|
121
|
+
this.executionLogBuffer.push(text); // Add raw text to unified log
|
|
122
|
+
}
|
|
115
123
|
onEnd(result) {
|
|
116
124
|
if (!this.reportFolder)
|
|
117
125
|
return;
|
|
118
126
|
const reportPath = path.join(this.reportFolder, 'custom-report.json');
|
|
119
|
-
const
|
|
127
|
+
const logsDir = path.join(this.reportFolder, 'logs');
|
|
128
|
+
const logPath = path.join(this.reportFolder, 'execution.log'); // Unified execution log
|
|
120
129
|
if (!fs.existsSync(this.reportFolder)) {
|
|
121
130
|
fs.mkdirSync(this.reportFolder, { recursive: true });
|
|
122
131
|
}
|
|
132
|
+
if (!fs.existsSync(logsDir)) {
|
|
133
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
134
|
+
}
|
|
123
135
|
fs.writeFileSync(reportPath, JSON.stringify(this.results, null, 2));
|
|
124
136
|
this.log(`Custom JSON Report generated at: ${reportPath}`);
|
|
125
|
-
fs.writeFileSync(logPath, this.
|
|
137
|
+
fs.writeFileSync(logPath, this.executionLogBuffer.join('')); // Join with empty string to preserve raw chunks and newlines
|
|
126
138
|
console.log(`Execution Log generated at: ${logPath}`);
|
|
127
139
|
}
|
|
128
140
|
}
|
|
@@ -86,6 +86,31 @@ class HtmlReporter {
|
|
|
86
86
|
}
|
|
87
87
|
fs.writeFileSync(this.reportPath, html);
|
|
88
88
|
console.log(`HTML Report generated at: ${this.reportPath}`);
|
|
89
|
+
// Check if we should open the report
|
|
90
|
+
let openReport = true; // Default to true
|
|
91
|
+
try {
|
|
92
|
+
const configPath = path.join(process.cwd(), 'report.config.json');
|
|
93
|
+
if (fs.existsSync(configPath)) {
|
|
94
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
95
|
+
if (config.openReport === false) {
|
|
96
|
+
openReport = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (e) { /* ignore */ }
|
|
101
|
+
if (openReport) {
|
|
102
|
+
try {
|
|
103
|
+
// Write a marker file for VS Code extension to detect and open Latest Run view
|
|
104
|
+
const markerPath = path.join(process.cwd(), '.flash-report-ready');
|
|
105
|
+
fs.writeFileSync(markerPath, JSON.stringify({
|
|
106
|
+
reportPath: this.reportPath,
|
|
107
|
+
timestamp: Date.now()
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
console.log('Could not write report marker file.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
89
114
|
}
|
|
90
115
|
onTestEnd(test, result) {
|
|
91
116
|
let suite = test.parent;
|
|
@@ -104,14 +129,19 @@ class HtmlReporter {
|
|
|
104
129
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
105
130
|
}
|
|
106
131
|
// Helper to copy attachment
|
|
107
|
-
const copyAttachment = (name, matchFn) => {
|
|
132
|
+
const copyAttachment = (name, subDir, matchFn) => {
|
|
108
133
|
const attachment = result.attachments.find(matchFn);
|
|
109
134
|
if (attachment && attachment.path && reportDir) {
|
|
135
|
+
const targetDir = path.join(reportDir, subDir);
|
|
136
|
+
if (!fs.existsSync(targetDir)) {
|
|
137
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
138
|
+
}
|
|
110
139
|
const fileName = `${name}_${test.title.replace(/[^a-z0-9]/gi, '_')}_${Date.now()}${path.extname(attachment.path)}`;
|
|
111
|
-
const destPath = path.join(
|
|
140
|
+
const destPath = path.join(targetDir, fileName);
|
|
112
141
|
try {
|
|
113
142
|
fs.copyFileSync(attachment.path, destPath);
|
|
114
|
-
|
|
143
|
+
// Return relative path including subfolder
|
|
144
|
+
return `${subDir}/${fileName}`;
|
|
115
145
|
}
|
|
116
146
|
catch (e) {
|
|
117
147
|
console.error(`Failed to copy ${name} attachment`, e);
|
|
@@ -120,10 +150,22 @@ class HtmlReporter {
|
|
|
120
150
|
return undefined;
|
|
121
151
|
};
|
|
122
152
|
// Handle Video
|
|
123
|
-
const videoRelativePath = copyAttachment('video', a => a.name === 'video' && !!a.path);
|
|
153
|
+
const videoRelativePath = copyAttachment('video', 'videos', a => a.name === 'video' && !!a.path);
|
|
124
154
|
// Handle Screenshot (usually named 'screenshot' or has image content type)
|
|
125
|
-
const screenshotRelativePath = copyAttachment('screenshot', a => (a.name === 'screenshot' || a.contentType.startsWith('image/')) && !!a.path);
|
|
126
|
-
|
|
155
|
+
const screenshotRelativePath = copyAttachment('screenshot', 'screenshots', a => (a.name === 'screenshot' || a.contentType.startsWith('image/')) && !!a.path);
|
|
156
|
+
// Capture Times
|
|
157
|
+
const start = result.startTime instanceof Date ? result.startTime : new Date(result.startTime);
|
|
158
|
+
const startTime = start.toLocaleTimeString();
|
|
159
|
+
const endTime = new Date(start.getTime() + result.duration).toLocaleTimeString();
|
|
160
|
+
this.testResults.push({
|
|
161
|
+
test,
|
|
162
|
+
result,
|
|
163
|
+
suiteName,
|
|
164
|
+
videoPath: videoRelativePath,
|
|
165
|
+
screenshotPath: screenshotRelativePath,
|
|
166
|
+
startTime,
|
|
167
|
+
endTime
|
|
168
|
+
});
|
|
127
169
|
}
|
|
128
170
|
ansiToHtml(text) {
|
|
129
171
|
if (!text)
|
|
@@ -214,6 +256,44 @@ class HtmlReporter {
|
|
|
214
256
|
});
|
|
215
257
|
}
|
|
216
258
|
generateHtml() {
|
|
259
|
+
// Load Config
|
|
260
|
+
let config = { theme: 'dark', language: 'en' };
|
|
261
|
+
try {
|
|
262
|
+
const configPath = path.join(process.cwd(), 'report.config.json');
|
|
263
|
+
if (fs.existsSync(configPath)) {
|
|
264
|
+
config = { ...config, ...JSON.parse(fs.readFileSync(configPath, 'utf-8')) };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (e) { /* ignore */ }
|
|
268
|
+
// Translations
|
|
269
|
+
const translations = {
|
|
270
|
+
en: {
|
|
271
|
+
tests: 'TESTS', passed: 'Passed', failed: 'Failed', skipped: 'Skipped',
|
|
272
|
+
all: 'All', failures: 'Failures', searchTests: 'Search tests...',
|
|
273
|
+
executionSteps: 'Execution Steps', errorDetails: 'Error Details', artifacts: 'Artifacts',
|
|
274
|
+
screenshot: 'Screenshot', video: 'Video', goToArtifacts: 'Go to Artifacts',
|
|
275
|
+
openRawTab: 'Open Raw Tab', copy: 'Copy', start: 'Start', end: 'End', duration: 'Duration',
|
|
276
|
+
user: 'User', os: 'OS', ip: 'IP', beforeHooks: 'Before Hooks', afterHooks: 'After Hooks', workerCleanup: 'Worker Cleanup'
|
|
277
|
+
},
|
|
278
|
+
fr: {
|
|
279
|
+
tests: 'TESTS', passed: 'Réussi', failed: 'Échoué', skipped: 'Ignoré',
|
|
280
|
+
all: 'Tous', failures: 'Échecs', searchTests: 'Rechercher...',
|
|
281
|
+
executionSteps: "Étapes d'exécution", errorDetails: "Détails de l'erreur", artifacts: 'Artéfacts',
|
|
282
|
+
screenshot: "Capture d'écran", video: 'Vidéo', goToArtifacts: 'Voir les artéfacts',
|
|
283
|
+
openRawTab: 'Ouvrir onglet brut', copy: 'Copier', start: 'Début', end: 'Fin', duration: 'Durée',
|
|
284
|
+
user: 'Utilisateur', os: 'Système', ip: 'IP', beforeHooks: 'Hooks Avant', afterHooks: 'Hooks Après', workerCleanup: 'Nettoyage'
|
|
285
|
+
},
|
|
286
|
+
zh: {
|
|
287
|
+
tests: '测试', passed: '通过', failed: '失败', skipped: '跳过',
|
|
288
|
+
all: '全部', failures: '失败项', searchTests: '搜索测试...',
|
|
289
|
+
executionSteps: '执行步骤', errorDetails: '错误详情', artifacts: '产物',
|
|
290
|
+
screenshot: '截图', video: '视频', goToArtifacts: '查看产物',
|
|
291
|
+
openRawTab: '打开原始标签', copy: '复制', start: '开始', end: '结束', duration: '耗时',
|
|
292
|
+
user: '用户', os: '系统', ip: 'IP地址', beforeHooks: '前置钩子', afterHooks: '后置钩子', workerCleanup: '清理'
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const lang = config.language && translations[config.language] ? config.language : 'en';
|
|
296
|
+
const t = translations[lang];
|
|
217
297
|
const totalTests = this.testResults.length;
|
|
218
298
|
const passedTests = this.testResults.filter(r => r.result.status === 'passed').length;
|
|
219
299
|
const failedTests = this.testResults.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length;
|
|
@@ -228,6 +308,8 @@ class HtmlReporter {
|
|
|
228
308
|
title: item.test.title,
|
|
229
309
|
status: item.result.status,
|
|
230
310
|
duration: item.result.duration,
|
|
311
|
+
startTime: item.startTime,
|
|
312
|
+
endTime: item.endTime,
|
|
231
313
|
steps: this.processSteps(item.result.steps),
|
|
232
314
|
error: item.result.error ? this.ansiToHtml(item.result.error.stack || item.result.error.message || '') : undefined,
|
|
233
315
|
screenshot: item.screenshotPath,
|
|
@@ -242,6 +324,7 @@ class HtmlReporter {
|
|
|
242
324
|
<title>Test Result</title>
|
|
243
325
|
<style>
|
|
244
326
|
:root {
|
|
327
|
+
/* Theme: ${config.theme} (Default) */
|
|
245
328
|
--bg-color: #1e1e1e;
|
|
246
329
|
--sidebar-bg: #252526;
|
|
247
330
|
--text-color: #d4d4d4;
|
|
@@ -252,6 +335,129 @@ class HtmlReporter {
|
|
|
252
335
|
--skip-color: #ff9800;
|
|
253
336
|
--blue-accent: #007acc;
|
|
254
337
|
--code-bg: #0d1117;
|
|
338
|
+
|
|
339
|
+
/* Detailed Colors */
|
|
340
|
+
--card-bg: #252526;
|
|
341
|
+
--header-bg: #2d2d2d;
|
|
342
|
+
--text-secondary: #888;
|
|
343
|
+
--border-dim: #333;
|
|
344
|
+
--step-hover: #2a2d2e;
|
|
345
|
+
--input-bg: #1e1e1e;
|
|
346
|
+
--input-text: #fff;
|
|
347
|
+
--code-text: #ccc;
|
|
348
|
+
--info-box-bg: #2a2d2e;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* Theme Definitions */
|
|
352
|
+
[data-theme="light"] {
|
|
353
|
+
--bg-color: #ffffff;
|
|
354
|
+
--sidebar-bg: #f3f3f3;
|
|
355
|
+
--text-color: #333333;
|
|
356
|
+
--border-color: #e5e5e5;
|
|
357
|
+
--hover-color: #e8e8e8;
|
|
358
|
+
--success-color: #2e7d32;
|
|
359
|
+
--fail-color: #d32f2f;
|
|
360
|
+
--skip-color: #ed6c02;
|
|
361
|
+
--blue-accent: #1976d2;
|
|
362
|
+
--code-bg: #f5f5f5;
|
|
363
|
+
|
|
364
|
+
--card-bg: #ffffff;
|
|
365
|
+
--header-bg: #f5f5f5;
|
|
366
|
+
--text-secondary: #666;
|
|
367
|
+
--border-dim: #eee;
|
|
368
|
+
--step-hover: #f0f0f0;
|
|
369
|
+
--input-bg: #ffffff;
|
|
370
|
+
--input-text: #333;
|
|
371
|
+
--code-text: #333;
|
|
372
|
+
--info-box-bg: #f5f5f5;
|
|
373
|
+
}
|
|
374
|
+
[data-theme="muted-light"] {
|
|
375
|
+
--bg-color: #f5f3ef;
|
|
376
|
+
--sidebar-bg: #ebe8e3;
|
|
377
|
+
--text-color: #4a4540;
|
|
378
|
+
--border-color: #d9d5cf;
|
|
379
|
+
--hover-color: #e5e2dc;
|
|
380
|
+
--success-color: #558b2f;
|
|
381
|
+
--fail-color: #c62828;
|
|
382
|
+
--skip-color: #e65100;
|
|
383
|
+
--blue-accent: #1565c0;
|
|
384
|
+
--code-bg: #e8e5e0;
|
|
385
|
+
|
|
386
|
+
--card-bg: #faf8f5;
|
|
387
|
+
--header-bg: #ebe8e3;
|
|
388
|
+
--text-secondary: #7a756e;
|
|
389
|
+
--border-dim: #d9d5cf;
|
|
390
|
+
--step-hover: #e5e2dc;
|
|
391
|
+
--input-bg: #faf8f5;
|
|
392
|
+
--input-text: #4a4540;
|
|
393
|
+
--code-text: #4a4540;
|
|
394
|
+
--info-box-bg: #ebe8e3;
|
|
395
|
+
}
|
|
396
|
+
[data-theme="midnight"] {
|
|
397
|
+
--bg-color: #0f172a;
|
|
398
|
+
--sidebar-bg: #1e293b;
|
|
399
|
+
--text-color: #e2e8f0;
|
|
400
|
+
--border-color: #334155;
|
|
401
|
+
--hover-color: #334155;
|
|
402
|
+
--success-color: #22c55e;
|
|
403
|
+
--fail-color: #ef4444;
|
|
404
|
+
--skip-color: #f59e0b;
|
|
405
|
+
--blue-accent: #6366f1;
|
|
406
|
+
--code-bg: #1e293b;
|
|
407
|
+
|
|
408
|
+
--card-bg: #1e293b;
|
|
409
|
+
--header-bg: #334155;
|
|
410
|
+
--text-secondary: #94a3b8;
|
|
411
|
+
--border-dim: #334155;
|
|
412
|
+
--step-hover: #334155;
|
|
413
|
+
--input-bg: #0f172a;
|
|
414
|
+
--input-text: #e2e8f0;
|
|
415
|
+
--code-text: #e2e8f0;
|
|
416
|
+
--info-box-bg: #1e293b;
|
|
417
|
+
}
|
|
418
|
+
[data-theme="forest"] {
|
|
419
|
+
--bg-color: #1a2f1a;
|
|
420
|
+
--sidebar-bg: #243d24;
|
|
421
|
+
--text-color: #e8f5e9;
|
|
422
|
+
--border-color: #2e4d2e;
|
|
423
|
+
--hover-color: #2e4d2e;
|
|
424
|
+
--success-color: #66bb6a;
|
|
425
|
+
--fail-color: #ef5350;
|
|
426
|
+
--skip-color: #ffa726;
|
|
427
|
+
--blue-accent: #4caf50;
|
|
428
|
+
--code-bg: #1b3320;
|
|
429
|
+
|
|
430
|
+
--card-bg: #243d24;
|
|
431
|
+
--header-bg: #2e4d2e;
|
|
432
|
+
--text-secondary: #a5d6a7;
|
|
433
|
+
--border-dim: #2e4d2e;
|
|
434
|
+
--step-hover: #2e4d2e;
|
|
435
|
+
--input-bg: #1a2f1a;
|
|
436
|
+
--input-text: #e8f5e9;
|
|
437
|
+
--code-text: #e8f5e9;
|
|
438
|
+
--info-box-bg: #243d24;
|
|
439
|
+
}
|
|
440
|
+
[data-theme="dracula"] {
|
|
441
|
+
--bg-color: #282a36;
|
|
442
|
+
--sidebar-bg: #44475a;
|
|
443
|
+
--text-color: #f8f8f2;
|
|
444
|
+
--border-color: #6272a4;
|
|
445
|
+
--hover-color: #44475a;
|
|
446
|
+
--success-color: #50fa7b;
|
|
447
|
+
--fail-color: #ff5555;
|
|
448
|
+
--skip-color: #ffb86c;
|
|
449
|
+
--blue-accent: #bd93f9;
|
|
450
|
+
--code-bg: #282a36;
|
|
451
|
+
|
|
452
|
+
--card-bg: #44475a;
|
|
453
|
+
--header-bg: #6272a4;
|
|
454
|
+
--text-secondary: #bd93f9;
|
|
455
|
+
--border-dim: #6272a4;
|
|
456
|
+
--step-hover: #6272a4;
|
|
457
|
+
--input-bg: #282a36;
|
|
458
|
+
--input-text: #f8f8f2;
|
|
459
|
+
--code-text: #f8f8f2;
|
|
460
|
+
--info-box-bg: #44475a;
|
|
255
461
|
}
|
|
256
462
|
body {
|
|
257
463
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -306,7 +512,7 @@ class HtmlReporter {
|
|
|
306
512
|
justify-content: center;
|
|
307
513
|
}
|
|
308
514
|
.total-text { font-size: 1.5em; font-weight: bold; }
|
|
309
|
-
.total-label { font-size: 0.8em; color:
|
|
515
|
+
.total-label { font-size: 0.8em; color: var(--text-secondary); }
|
|
310
516
|
|
|
311
517
|
.stats-row {
|
|
312
518
|
display: flex;
|
|
@@ -336,9 +542,9 @@ class HtmlReporter {
|
|
|
336
542
|
}
|
|
337
543
|
.filter-btn {
|
|
338
544
|
flex: 1;
|
|
339
|
-
background:
|
|
545
|
+
background: var(--card-bg);
|
|
340
546
|
border: 1px solid var(--border-color);
|
|
341
|
-
color:
|
|
547
|
+
color: var(--text-color);
|
|
342
548
|
padding: 5px;
|
|
343
549
|
cursor: pointer;
|
|
344
550
|
font-size: 0.85em;
|
|
@@ -352,9 +558,9 @@ class HtmlReporter {
|
|
|
352
558
|
.search-box {
|
|
353
559
|
width: 100%;
|
|
354
560
|
padding: 10px;
|
|
355
|
-
background-color:
|
|
561
|
+
background-color: var(--input-bg);
|
|
356
562
|
border: 1px solid var(--border-color);
|
|
357
|
-
color:
|
|
563
|
+
color: var(--input-text);
|
|
358
564
|
border-radius: 4px;
|
|
359
565
|
box-sizing: border-box;
|
|
360
566
|
margin-top: 10px;
|
|
@@ -401,7 +607,7 @@ class HtmlReporter {
|
|
|
401
607
|
.detail-header-wrapper {
|
|
402
608
|
padding: 20px 30px;
|
|
403
609
|
border-bottom: 1px solid var(--border-color);
|
|
404
|
-
background-color:
|
|
610
|
+
background-color: var(--card-bg);
|
|
405
611
|
}
|
|
406
612
|
.detail-title { font-size: 1.6em; margin: 10px 0; font-weight: 300; }
|
|
407
613
|
.detail-badges { display: flex; gap: 10px; align-items: center; }
|
|
@@ -423,7 +629,7 @@ class HtmlReporter {
|
|
|
423
629
|
}
|
|
424
630
|
|
|
425
631
|
.section-card {
|
|
426
|
-
background-color:
|
|
632
|
+
background-color: var(--card-bg);
|
|
427
633
|
border: 1px solid var(--border-color);
|
|
428
634
|
border-radius: 6px;
|
|
429
635
|
margin-bottom: 25px;
|
|
@@ -431,7 +637,7 @@ class HtmlReporter {
|
|
|
431
637
|
}
|
|
432
638
|
.section-header {
|
|
433
639
|
padding: 10px 15px;
|
|
434
|
-
background-color:
|
|
640
|
+
background-color: var(--header-bg);
|
|
435
641
|
border-bottom: 1px solid var(--border-color);
|
|
436
642
|
font-weight: 600;
|
|
437
643
|
color: #bbb;
|
|
@@ -443,7 +649,7 @@ class HtmlReporter {
|
|
|
443
649
|
/* Steps */
|
|
444
650
|
.step-row {
|
|
445
651
|
padding: 8px 15px;
|
|
446
|
-
border-bottom: 1px solid
|
|
652
|
+
border-bottom: 1px solid var(--border-dim);
|
|
447
653
|
display: flex;
|
|
448
654
|
gap: 10px;
|
|
449
655
|
align-items: flex-start;
|
|
@@ -452,10 +658,10 @@ class HtmlReporter {
|
|
|
452
658
|
transition: background-color 0.2s;
|
|
453
659
|
}
|
|
454
660
|
.step-row:last-child { border-bottom: none; }
|
|
455
|
-
.step-row:hover { background-color:
|
|
661
|
+
.step-row:hover { background-color: var(--step-hover); }
|
|
456
662
|
.step-status { margin-top: 3px; font-size: 1.2em; line-height: 1; }
|
|
457
663
|
.step-content { flex: 1; }
|
|
458
|
-
.step-meta { color:
|
|
664
|
+
.step-meta { color: var(--text-secondary); font-size: 0.9em; margin-left: 10px; white-space: nowrap; }
|
|
459
665
|
|
|
460
666
|
/* Tree Toggle */
|
|
461
667
|
.toggle-icon {
|
|
@@ -489,7 +695,7 @@ class HtmlReporter {
|
|
|
489
695
|
padding: 15px;
|
|
490
696
|
border-radius: 4px;
|
|
491
697
|
font-family: Consolas, monospace;
|
|
492
|
-
color:
|
|
698
|
+
color: var(--code-text);
|
|
493
699
|
white-space: pre-wrap;
|
|
494
700
|
overflow-x: auto;
|
|
495
701
|
border: 1px solid #30363d;
|
|
@@ -498,12 +704,12 @@ class HtmlReporter {
|
|
|
498
704
|
/* Code Snippet */
|
|
499
705
|
.code-snippet {
|
|
500
706
|
margin-top: 10px;
|
|
501
|
-
background-color:
|
|
707
|
+
background-color: var(--code-bg);
|
|
502
708
|
border-radius: 4px;
|
|
503
709
|
padding: 10px;
|
|
504
710
|
font-family: Consolas, monospace;
|
|
505
711
|
font-size: 0.9em;
|
|
506
|
-
color:
|
|
712
|
+
color: var(--code-text);
|
|
507
713
|
white-space: pre;
|
|
508
714
|
overflow-x: auto;
|
|
509
715
|
border-left: 3px solid #ffcc00;
|
|
@@ -517,14 +723,14 @@ class HtmlReporter {
|
|
|
517
723
|
padding: 20px;
|
|
518
724
|
}
|
|
519
725
|
.media-item {
|
|
520
|
-
background-color:
|
|
726
|
+
background-color: var(--card-bg);
|
|
521
727
|
border-radius: 4px;
|
|
522
728
|
border: 1px solid var(--border-color);
|
|
523
729
|
overflow: hidden;
|
|
524
730
|
}
|
|
525
731
|
.media-header {
|
|
526
732
|
padding: 8px 12px;
|
|
527
|
-
background-color:
|
|
733
|
+
background-color: var(--header-bg);
|
|
528
734
|
color: #ccc;
|
|
529
735
|
font-size: 0.85em;
|
|
530
736
|
border-bottom: 1px solid #444;
|
|
@@ -547,43 +753,53 @@ class HtmlReporter {
|
|
|
547
753
|
<body>
|
|
548
754
|
<div class="sidebar">
|
|
549
755
|
<div class="sidebar-header">
|
|
756
|
+
<div style="width:100%; margin-bottom:15px; display:flex; justify-content:flex-end">
|
|
757
|
+
<select id="themeSelector" onchange="setTheme(this.value)" style="background:var(--input-bg); color:var(--input-text); border:1px solid var(--border-color); padding:2px 5px; border-radius:3px; outline:none; font-size:0.8em">
|
|
758
|
+
<option value="dark">Dark</option>
|
|
759
|
+
<option value="light">Light</option>
|
|
760
|
+
<option value="muted-light">Muted Light</option>
|
|
761
|
+
<option value="midnight">Midnight</option>
|
|
762
|
+
<option value="forest">Forest</option>
|
|
763
|
+
<option value="dracula">Dracula</option>
|
|
764
|
+
</select>
|
|
765
|
+
</div>
|
|
550
766
|
<div class="chart-container">
|
|
551
767
|
<div class="chart-inner">
|
|
552
768
|
<div class="total-text">${totalTests}</div>
|
|
553
|
-
<div class="total-label"
|
|
769
|
+
<div class="total-label">${t.tests}</div>
|
|
554
770
|
</div>
|
|
555
771
|
</div>
|
|
556
772
|
|
|
557
773
|
<div class="stats-row">
|
|
558
774
|
<div class="stat-item" onclick="setFilter('passed')" title="Show Passed">
|
|
559
775
|
<span class="stat-value" style="color:var(--success-color)">${passedTests}</span>
|
|
560
|
-
<span class="stat-label"
|
|
776
|
+
<span class="stat-label">${t.passed}</span>
|
|
561
777
|
</div>
|
|
562
778
|
<div class="stat-item" onclick="setFilter('failed')" title="Show Failed">
|
|
563
779
|
<span class="stat-value" style="color:var(--fail-color)">${failedTests}</span>
|
|
564
|
-
<span class="stat-label"
|
|
780
|
+
<span class="stat-label">${t.failed}</span>
|
|
565
781
|
</div>
|
|
566
782
|
<div class="stat-item" onclick="setFilter('skipped')" title="Show Skipped">
|
|
567
783
|
<span class="stat-value" style="color:var(--skip-color)">${skippedTests}</span>
|
|
568
|
-
<span class="stat-label"
|
|
784
|
+
<span class="stat-label">${t.skipped}</span>
|
|
569
785
|
</div>
|
|
570
786
|
</div>
|
|
571
787
|
|
|
572
788
|
<div style="margin-top:15px; font-size:0.8em; color:#666; text-align:center" id="reportTimestamp"></div>
|
|
573
789
|
|
|
574
790
|
<!-- Environment Info -->
|
|
575
|
-
<div style="margin-top:15px; padding:15px; background-color
|
|
576
|
-
<div style="margin-bottom:5px"><strong style="color
|
|
577
|
-
<div style="margin-bottom:5px"><strong style="color
|
|
578
|
-
<div><strong style="color
|
|
791
|
+
<div style="margin-top:15px; padding:15px; background-color:var(--info-box-bg); border-top:1px solid var(--border-color); font-size:0.85em; color:var(--text-secondary)">
|
|
792
|
+
<div style="margin-bottom:5px"><strong style="color:var(--text-secondary)">${t.user}:</strong> <span id="envUser">...</span></div>
|
|
793
|
+
<div style="margin-bottom:5px"><strong style="color:var(--text-secondary)">${t.os}:</strong> <span id="envOs">...</span></div>
|
|
794
|
+
<div><strong style="color:var(--text-secondary)">${t.ip}:</strong> <span id="envIp">...</span></div>
|
|
579
795
|
</div>
|
|
580
796
|
|
|
581
797
|
<div class="filter-group">
|
|
582
|
-
<button class="filter-btn active" onclick="setFilter('all')"
|
|
583
|
-
<button class="filter-btn" onclick="setFilter('failed')"
|
|
798
|
+
<button class="filter-btn active" onclick="setFilter('all')">${t.all}</button>
|
|
799
|
+
<button class="filter-btn" onclick="setFilter('failed')">${t.failures}</button>
|
|
584
800
|
</div>
|
|
585
801
|
|
|
586
|
-
<input type="text" class="search-box" placeholder="
|
|
802
|
+
<input type="text" class="search-box" placeholder="${t.searchTests}" id="searchInput" onkeyup="filterTests()">
|
|
587
803
|
</div>
|
|
588
804
|
<div class="test-list" id="testList"></div>
|
|
589
805
|
</div>
|
|
@@ -601,6 +817,7 @@ class HtmlReporter {
|
|
|
601
817
|
const data = ${resultsData};
|
|
602
818
|
const startTime = ${this.suiteStartTime};
|
|
603
819
|
const envInfo = ${JSON.stringify(this.envInfo)};
|
|
820
|
+
const t = ${JSON.stringify(t)};
|
|
604
821
|
let currentId = null;
|
|
605
822
|
let currentFilter = 'all';
|
|
606
823
|
|
|
@@ -713,19 +930,23 @@ class HtmlReporter {
|
|
|
713
930
|
const statusClass = test.status === 'passed' ? 'badge-passed' :
|
|
714
931
|
(test.status === 'failed' || test.status === 'timedOut') ? 'badge-failed' : 'badge-skipped';
|
|
715
932
|
|
|
933
|
+
// Clean error message (remove Call log)
|
|
934
|
+
const cleanError = test.error ? test.error.split('Call log:')[0].trim() : '';
|
|
935
|
+
|
|
716
936
|
// Steps (using tree renderer)
|
|
717
937
|
const stepsHtml = test.steps.map(generateStepTreeHtml).join('');
|
|
718
938
|
|
|
719
939
|
// Media
|
|
940
|
+
const hasArtifacts = test.screenshot || test.video;
|
|
720
941
|
const mediaHtml = \`
|
|
721
942
|
\${test.screenshot ? \`
|
|
722
943
|
<div class="media-item">
|
|
723
|
-
<div class="media-header"
|
|
944
|
+
<div class="media-header">\${t.screenshot} <a href="\${test.screenshot}" target="_blank" style="float:right; color:#4dabf7; text-decoration:none">${t.openRawTab}</a></div>
|
|
724
945
|
<img src="\${test.screenshot}" onclick="window.open(this.src)">
|
|
725
946
|
</div>\` : ''}
|
|
726
947
|
\${test.video ? \`
|
|
727
948
|
<div class="media-item">
|
|
728
|
-
<div class="media-header"
|
|
949
|
+
<div class="media-header">\${t.video}</div>
|
|
729
950
|
<video src="\${test.video}" controls></video>
|
|
730
951
|
</div>\` : ''}
|
|
731
952
|
\`;
|
|
@@ -735,31 +956,36 @@ class HtmlReporter {
|
|
|
735
956
|
<div class="detail-meta">
|
|
736
957
|
<span class="badge \${statusClass}">\${test.status}</span>
|
|
737
958
|
<span>\${test.suite}</span>
|
|
738
|
-
|
|
959
|
+
\${hasArtifacts ? \`<button class="filter-btn" onclick="document.getElementById('artifacts-section').scrollIntoView({behavior: 'smooth'})" style="margin-left:auto; padding:2px 8px; font-size:11px; height:auto; line-height:normal">\${t.goToArtifacts} ⬇</button>\` : ''}
|
|
739
960
|
</div>
|
|
740
961
|
<div class="detail-title">\${test.title}</div>
|
|
962
|
+
<div class="detail-timing" style="color:#888; font-size:0.9em; margin-top:5px">
|
|
963
|
+
<span style="margin-right:15px">\${t.start}: \${test.startTime}</span>
|
|
964
|
+
<span style="margin-right:15px">\${t.end}: \${test.endTime}</span>
|
|
965
|
+
<span>\${t.duration}: \${test.duration}ms</span>
|
|
966
|
+
</div>
|
|
741
967
|
</div>
|
|
742
968
|
|
|
743
969
|
<div class="detail-scroll-area">
|
|
744
|
-
\${
|
|
970
|
+
\${cleanError ? \`
|
|
745
971
|
<div class="section-card error-box">
|
|
746
972
|
<div style="display:flex; justify-content:space-between; margin-bottom:10px">
|
|
747
|
-
<strong style="color:var(--fail-color)"
|
|
973
|
+
<strong style="color:var(--fail-color)">\${t.errorDetails}</strong>
|
|
748
974
|
</div>
|
|
749
|
-
<div class="stack-trace">\${
|
|
975
|
+
<div class="stack-trace">\${cleanError}</div>
|
|
750
976
|
</div>
|
|
751
977
|
\` : ''}
|
|
752
978
|
|
|
753
979
|
<div class="section-card">
|
|
754
|
-
<div class="section-header"
|
|
980
|
+
<div class="section-header">\${t.executionSteps}</div>
|
|
755
981
|
<div class="steps-tree-container">
|
|
756
982
|
\${stepsHtml}
|
|
757
983
|
</div>
|
|
758
984
|
</div>
|
|
759
985
|
|
|
760
|
-
\${
|
|
761
|
-
<div class="section-card">
|
|
762
|
-
<div class="section-header"
|
|
986
|
+
\${hasArtifacts ? \`
|
|
987
|
+
<div id="artifacts-section" class="section-card">
|
|
988
|
+
<div class="section-header">\${t.artifacts}</div>
|
|
763
989
|
<div class="media-grid">\${mediaHtml}</div>
|
|
764
990
|
</div>
|
|
765
991
|
\` : ''}
|
|
@@ -768,6 +994,22 @@ class HtmlReporter {
|
|
|
768
994
|
content.classList.add('active');
|
|
769
995
|
}
|
|
770
996
|
|
|
997
|
+
function setTheme(themeName) {
|
|
998
|
+
document.documentElement.setAttribute('data-theme', themeName);
|
|
999
|
+
localStorage.setItem('report-theme', themeName);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Initialize Theme
|
|
1003
|
+
// We prioritize the theme from report.config.json if it's set.
|
|
1004
|
+
// This ensures the report respects the project configuration.
|
|
1005
|
+
const configTheme = '${config.theme}' || 'dark';
|
|
1006
|
+
|
|
1007
|
+
// On initial load, we follow the config.
|
|
1008
|
+
// However, we can check if there was a MANUALLY set preference in this session.
|
|
1009
|
+
// For now, let's just respect the config theme as requested.
|
|
1010
|
+
setTheme(configTheme);
|
|
1011
|
+
document.getElementById('themeSelector').value = configTheme;
|
|
1012
|
+
|
|
771
1013
|
// Initial render
|
|
772
1014
|
renderList(data);
|
|
773
1015
|
</script>
|