@flash-ai-team/flash-test-framework 0.0.14 → 0.0.16
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/README.md +7 -0
- package/dist/cli/index.js +99 -0
- package/dist/reporting/CustomReporter.d.ts +3 -1
- package/dist/reporting/CustomReporter.js +22 -10
- package/dist/reporting/HtmlReporter.js +260 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ Then you can use:
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
flash-test init <project-name>
|
|
21
|
+
flash-test run <suite-name> [-browser <browser>]
|
|
22
|
+
flash-test latest-run <suite-name>
|
|
21
23
|
flash-test history [suite-name]
|
|
22
24
|
flash-test --version
|
|
23
25
|
```
|
|
@@ -25,6 +27,11 @@ flash-test --version
|
|
|
25
27
|
### Commands
|
|
26
28
|
|
|
27
29
|
* `init <project-name>`: Initialize a new project structure.
|
|
30
|
+
* `run <suite-name>`: Run a specific test suite.
|
|
31
|
+
* Example: `flash-test run parallel_suite`
|
|
32
|
+
* Example: `flash-test run parallel_suite -browser firefox`
|
|
33
|
+
* `latest-run <suite-name>`: View the status of the most recent run for a suite.
|
|
34
|
+
* Example: `flash-test latest-run parallel_suite`
|
|
28
35
|
* `history [suite-name]`: View historical test runs and open the HTML dashboard.
|
|
29
36
|
* Example: `flash-test history` (All suites)
|
|
30
37
|
* Example: `flash-test history parallel_suite` (Specific suite)
|
package/dist/cli/index.js
CHANGED
|
@@ -20,8 +20,17 @@ else if (command === 'history') {
|
|
|
20
20
|
const suiteName = args[1];
|
|
21
21
|
showHistory(suiteName);
|
|
22
22
|
}
|
|
23
|
+
else if (command === 'run') {
|
|
24
|
+
runSuite(args.slice(1));
|
|
25
|
+
}
|
|
26
|
+
else if (command === 'latest-run') {
|
|
27
|
+
const suiteName = args[1];
|
|
28
|
+
showLatestRun(suiteName);
|
|
29
|
+
}
|
|
23
30
|
else {
|
|
24
31
|
console.log('Usage: flash-test init [project-name]');
|
|
32
|
+
console.log(' flash-test run <suite-name> [-browser <browser-name>]');
|
|
33
|
+
console.log(' flash-test latest-run <suite-name>');
|
|
25
34
|
console.log(' flash-test history [suite-name]');
|
|
26
35
|
console.log(' flash-test -v / --version');
|
|
27
36
|
process.exit(1);
|
|
@@ -336,3 +345,93 @@ function generateHistoryHtml(runs, reportsDir) {
|
|
|
336
345
|
console.log('Could not auto-open browser. Please open the file manually.');
|
|
337
346
|
}
|
|
338
347
|
}
|
|
348
|
+
function runSuite(args) {
|
|
349
|
+
const suiteName = args[0];
|
|
350
|
+
if (!suiteName) {
|
|
351
|
+
console.error('Error: Suite name is required.');
|
|
352
|
+
console.log('Usage: flash-test run <suite-name> [-browser <browser-name>]');
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
const suitePath = `tests/suites/${suiteName}.spec.ts`;
|
|
356
|
+
if (!fs_1.default.existsSync(path_1.default.join(process.cwd(), suitePath))) {
|
|
357
|
+
console.error(`Error: Suite file not found at ${suitePath}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
let cmd = `npx playwright test ${suitePath}`;
|
|
361
|
+
// Check for -browser flag
|
|
362
|
+
const browserIndex = args.indexOf('-browser');
|
|
363
|
+
if (browserIndex !== -1 && args[browserIndex + 1]) {
|
|
364
|
+
const browser = args[browserIndex + 1];
|
|
365
|
+
cmd += ` --project=${browser}`;
|
|
366
|
+
}
|
|
367
|
+
console.log(`Executing: ${cmd}`);
|
|
368
|
+
try {
|
|
369
|
+
(0, child_process_1.execSync)(cmd, { stdio: 'inherit' });
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function showLatestRun(suiteName) {
|
|
376
|
+
if (!suiteName) {
|
|
377
|
+
console.error('Error: Suite name is required for latest-run.');
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
const reportsDir = path_1.default.join(process.cwd(), 'reports', suiteName);
|
|
381
|
+
if (!fs_1.default.existsSync(reportsDir)) {
|
|
382
|
+
console.log(`No reports found for suite: ${suiteName}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// Find latest timestamp folder
|
|
386
|
+
const runs = fs_1.default.readdirSync(reportsDir)
|
|
387
|
+
.filter(f => f.startsWith('test_') && fs_1.default.statSync(path_1.default.join(reportsDir, f)).isDirectory())
|
|
388
|
+
.sort((a, b) => b.localeCompare(a)); // Reverse sort to get newest first
|
|
389
|
+
if (runs.length === 0) {
|
|
390
|
+
console.log(`No runs found for suite: ${suiteName}`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const latestRun = runs[0];
|
|
394
|
+
const runPath = path_1.default.join(reportsDir, latestRun);
|
|
395
|
+
const jsonPath = path_1.default.join(runPath, 'custom-report.json');
|
|
396
|
+
console.log(`\nLatest Run: ${suiteName}`);
|
|
397
|
+
console.log(`Timestamp: ${latestRun.replace('test_', '')}`);
|
|
398
|
+
console.log(`Report Path: ${runPath}`);
|
|
399
|
+
console.log('----------------------------------------');
|
|
400
|
+
if (fs_1.default.existsSync(jsonPath)) {
|
|
401
|
+
try {
|
|
402
|
+
const data = JSON.parse(fs_1.default.readFileSync(jsonPath, 'utf-8'));
|
|
403
|
+
let status = 'Unknown';
|
|
404
|
+
let passed = 0;
|
|
405
|
+
let failed = 0;
|
|
406
|
+
let duration = 0;
|
|
407
|
+
if (Array.isArray(data)) {
|
|
408
|
+
passed = data.filter((t) => t.status === 'passed').length;
|
|
409
|
+
failed = data.filter((t) => t.status === 'failed' || t.status === 'timedOut').length;
|
|
410
|
+
duration = data.reduce((acc, curr) => acc + (curr.duration || 0), 0);
|
|
411
|
+
status = failed > 0 ? 'Failed' : (passed > 0 ? 'Passed' : 'No Tests');
|
|
412
|
+
}
|
|
413
|
+
else if (data.stats) {
|
|
414
|
+
passed = data.stats.passed;
|
|
415
|
+
failed = data.stats.failed;
|
|
416
|
+
duration = data.stats.duration;
|
|
417
|
+
status = failed > 0 ? 'Failed' : 'Passed';
|
|
418
|
+
}
|
|
419
|
+
const colorFn = status === 'Failed' ? '\x1b[31m' : '\x1b[32m'; // Red or Green
|
|
420
|
+
const reset = '\x1b[0m';
|
|
421
|
+
console.log(`Status: ${colorFn}${status}${reset}`);
|
|
422
|
+
console.log(`Passed: ${passed}`);
|
|
423
|
+
console.log(`Failed: ${failed}`);
|
|
424
|
+
console.log(`Duration: ${(duration / 1000).toFixed(2)}s`);
|
|
425
|
+
if (failed > 0) {
|
|
426
|
+
console.log(`\n${colorFn}Errors detected! Check report for details.${reset}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (e) {
|
|
430
|
+
console.error('Error parsing report data.');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
console.log('Report data not found.');
|
|
435
|
+
}
|
|
436
|
+
console.log('');
|
|
437
|
+
}
|
|
@@ -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
|
}
|
|
@@ -104,14 +104,19 @@ class HtmlReporter {
|
|
|
104
104
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
105
105
|
}
|
|
106
106
|
// Helper to copy attachment
|
|
107
|
-
const copyAttachment = (name, matchFn) => {
|
|
107
|
+
const copyAttachment = (name, subDir, matchFn) => {
|
|
108
108
|
const attachment = result.attachments.find(matchFn);
|
|
109
109
|
if (attachment && attachment.path && reportDir) {
|
|
110
|
+
const targetDir = path.join(reportDir, subDir);
|
|
111
|
+
if (!fs.existsSync(targetDir)) {
|
|
112
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
113
|
+
}
|
|
110
114
|
const fileName = `${name}_${test.title.replace(/[^a-z0-9]/gi, '_')}_${Date.now()}${path.extname(attachment.path)}`;
|
|
111
|
-
const destPath = path.join(
|
|
115
|
+
const destPath = path.join(targetDir, fileName);
|
|
112
116
|
try {
|
|
113
117
|
fs.copyFileSync(attachment.path, destPath);
|
|
114
|
-
|
|
118
|
+
// Return relative path including subfolder
|
|
119
|
+
return `${subDir}/${fileName}`;
|
|
115
120
|
}
|
|
116
121
|
catch (e) {
|
|
117
122
|
console.error(`Failed to copy ${name} attachment`, e);
|
|
@@ -120,10 +125,22 @@ class HtmlReporter {
|
|
|
120
125
|
return undefined;
|
|
121
126
|
};
|
|
122
127
|
// Handle Video
|
|
123
|
-
const videoRelativePath = copyAttachment('video', a => a.name === 'video' && !!a.path);
|
|
128
|
+
const videoRelativePath = copyAttachment('video', 'videos', a => a.name === 'video' && !!a.path);
|
|
124
129
|
// 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
|
-
|
|
130
|
+
const screenshotRelativePath = copyAttachment('screenshot', 'screenshots', a => (a.name === 'screenshot' || a.contentType.startsWith('image/')) && !!a.path);
|
|
131
|
+
// Capture Times
|
|
132
|
+
const start = result.startTime instanceof Date ? result.startTime : new Date(result.startTime);
|
|
133
|
+
const startTime = start.toLocaleTimeString();
|
|
134
|
+
const endTime = new Date(start.getTime() + result.duration).toLocaleTimeString();
|
|
135
|
+
this.testResults.push({
|
|
136
|
+
test,
|
|
137
|
+
result,
|
|
138
|
+
suiteName,
|
|
139
|
+
videoPath: videoRelativePath,
|
|
140
|
+
screenshotPath: screenshotRelativePath,
|
|
141
|
+
startTime,
|
|
142
|
+
endTime
|
|
143
|
+
});
|
|
127
144
|
}
|
|
128
145
|
ansiToHtml(text) {
|
|
129
146
|
if (!text)
|
|
@@ -214,6 +231,44 @@ class HtmlReporter {
|
|
|
214
231
|
});
|
|
215
232
|
}
|
|
216
233
|
generateHtml() {
|
|
234
|
+
// Load Config
|
|
235
|
+
let config = { theme: 'dark', language: 'en' };
|
|
236
|
+
try {
|
|
237
|
+
const configPath = path.join(process.cwd(), 'report.config.json');
|
|
238
|
+
if (fs.existsSync(configPath)) {
|
|
239
|
+
config = { ...config, ...JSON.parse(fs.readFileSync(configPath, 'utf-8')) };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (e) { /* ignore */ }
|
|
243
|
+
// Translations
|
|
244
|
+
const translations = {
|
|
245
|
+
en: {
|
|
246
|
+
tests: 'TESTS', passed: 'Passed', failed: 'Failed', skipped: 'Skipped',
|
|
247
|
+
all: 'All', failures: 'Failures', searchTests: 'Search tests...',
|
|
248
|
+
executionSteps: 'Execution Steps', errorDetails: 'Error Details', artifacts: 'Artifacts',
|
|
249
|
+
screenshot: 'Screenshot', video: 'Video', goToArtifacts: 'Go to Artifacts',
|
|
250
|
+
openRawTab: 'Open Raw Tab', copy: 'Copy', start: 'Start', end: 'End', duration: 'Duration',
|
|
251
|
+
user: 'User', os: 'OS', ip: 'IP', beforeHooks: 'Before Hooks', afterHooks: 'After Hooks', workerCleanup: 'Worker Cleanup'
|
|
252
|
+
},
|
|
253
|
+
fr: {
|
|
254
|
+
tests: 'TESTS', passed: 'Réussi', failed: 'Échoué', skipped: 'Ignoré',
|
|
255
|
+
all: 'Tous', failures: 'Échecs', searchTests: 'Rechercher...',
|
|
256
|
+
executionSteps: "Étapes d'exécution", errorDetails: "Détails de l'erreur", artifacts: 'Artéfacts',
|
|
257
|
+
screenshot: "Capture d'écran", video: 'Vidéo', goToArtifacts: 'Voir les artéfacts',
|
|
258
|
+
openRawTab: 'Ouvrir onglet brut', copy: 'Copier', start: 'Début', end: 'Fin', duration: 'Durée',
|
|
259
|
+
user: 'Utilisateur', os: 'Système', ip: 'IP', beforeHooks: 'Hooks Avant', afterHooks: 'Hooks Après', workerCleanup: 'Nettoyage'
|
|
260
|
+
},
|
|
261
|
+
zh: {
|
|
262
|
+
tests: '测试', passed: '通过', failed: '失败', skipped: '跳过',
|
|
263
|
+
all: '全部', failures: '失败项', searchTests: '搜索测试...',
|
|
264
|
+
executionSteps: '执行步骤', errorDetails: '错误详情', artifacts: '产物',
|
|
265
|
+
screenshot: '截图', video: '视频', goToArtifacts: '查看产物',
|
|
266
|
+
openRawTab: '打开原始标签', copy: '复制', start: '开始', end: '结束', duration: '耗时',
|
|
267
|
+
user: '用户', os: '系统', ip: 'IP地址', beforeHooks: '前置钩子', afterHooks: '后置钩子', workerCleanup: '清理'
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const lang = config.language && translations[config.language] ? config.language : 'en';
|
|
271
|
+
const t = translations[lang];
|
|
217
272
|
const totalTests = this.testResults.length;
|
|
218
273
|
const passedTests = this.testResults.filter(r => r.result.status === 'passed').length;
|
|
219
274
|
const failedTests = this.testResults.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length;
|
|
@@ -228,6 +283,8 @@ class HtmlReporter {
|
|
|
228
283
|
title: item.test.title,
|
|
229
284
|
status: item.result.status,
|
|
230
285
|
duration: item.result.duration,
|
|
286
|
+
startTime: item.startTime,
|
|
287
|
+
endTime: item.endTime,
|
|
231
288
|
steps: this.processSteps(item.result.steps),
|
|
232
289
|
error: item.result.error ? this.ansiToHtml(item.result.error.stack || item.result.error.message || '') : undefined,
|
|
233
290
|
screenshot: item.screenshotPath,
|
|
@@ -242,6 +299,7 @@ class HtmlReporter {
|
|
|
242
299
|
<title>Test Result</title>
|
|
243
300
|
<style>
|
|
244
301
|
:root {
|
|
302
|
+
/* Theme: ${config.theme} (Default) */
|
|
245
303
|
--bg-color: #1e1e1e;
|
|
246
304
|
--sidebar-bg: #252526;
|
|
247
305
|
--text-color: #d4d4d4;
|
|
@@ -252,6 +310,129 @@ class HtmlReporter {
|
|
|
252
310
|
--skip-color: #ff9800;
|
|
253
311
|
--blue-accent: #007acc;
|
|
254
312
|
--code-bg: #0d1117;
|
|
313
|
+
|
|
314
|
+
/* Detailed Colors */
|
|
315
|
+
--card-bg: #252526;
|
|
316
|
+
--header-bg: #2d2d2d;
|
|
317
|
+
--text-secondary: #888;
|
|
318
|
+
--border-dim: #333;
|
|
319
|
+
--step-hover: #2a2d2e;
|
|
320
|
+
--input-bg: #1e1e1e;
|
|
321
|
+
--input-text: #fff;
|
|
322
|
+
--code-text: #ccc;
|
|
323
|
+
--info-box-bg: #2a2d2e;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Theme Definitions */
|
|
327
|
+
[data-theme="light"] {
|
|
328
|
+
--bg-color: #ffffff;
|
|
329
|
+
--sidebar-bg: #f3f3f3;
|
|
330
|
+
--text-color: #333333;
|
|
331
|
+
--border-color: #e5e5e5;
|
|
332
|
+
--hover-color: #e8e8e8;
|
|
333
|
+
--success-color: #2e7d32;
|
|
334
|
+
--fail-color: #d32f2f;
|
|
335
|
+
--skip-color: #ed6c02;
|
|
336
|
+
--blue-accent: #1976d2;
|
|
337
|
+
--code-bg: #f5f5f5;
|
|
338
|
+
|
|
339
|
+
--card-bg: #ffffff;
|
|
340
|
+
--header-bg: #f5f5f5;
|
|
341
|
+
--text-secondary: #666;
|
|
342
|
+
--border-dim: #eee;
|
|
343
|
+
--step-hover: #f0f0f0;
|
|
344
|
+
--input-bg: #ffffff;
|
|
345
|
+
--input-text: #333;
|
|
346
|
+
--code-text: #333;
|
|
347
|
+
--info-box-bg: #f5f5f5;
|
|
348
|
+
}
|
|
349
|
+
[data-theme="muted-light"] {
|
|
350
|
+
--bg-color: #f5f3ef;
|
|
351
|
+
--sidebar-bg: #ebe8e3;
|
|
352
|
+
--text-color: #4a4540;
|
|
353
|
+
--border-color: #d9d5cf;
|
|
354
|
+
--hover-color: #e5e2dc;
|
|
355
|
+
--success-color: #558b2f;
|
|
356
|
+
--fail-color: #c62828;
|
|
357
|
+
--skip-color: #e65100;
|
|
358
|
+
--blue-accent: #1565c0;
|
|
359
|
+
--code-bg: #e8e5e0;
|
|
360
|
+
|
|
361
|
+
--card-bg: #faf8f5;
|
|
362
|
+
--header-bg: #ebe8e3;
|
|
363
|
+
--text-secondary: #7a756e;
|
|
364
|
+
--border-dim: #d9d5cf;
|
|
365
|
+
--step-hover: #e5e2dc;
|
|
366
|
+
--input-bg: #faf8f5;
|
|
367
|
+
--input-text: #4a4540;
|
|
368
|
+
--code-text: #4a4540;
|
|
369
|
+
--info-box-bg: #ebe8e3;
|
|
370
|
+
}
|
|
371
|
+
[data-theme="midnight"] {
|
|
372
|
+
--bg-color: #0f172a;
|
|
373
|
+
--sidebar-bg: #1e293b;
|
|
374
|
+
--text-color: #e2e8f0;
|
|
375
|
+
--border-color: #334155;
|
|
376
|
+
--hover-color: #334155;
|
|
377
|
+
--success-color: #22c55e;
|
|
378
|
+
--fail-color: #ef4444;
|
|
379
|
+
--skip-color: #f59e0b;
|
|
380
|
+
--blue-accent: #6366f1;
|
|
381
|
+
--code-bg: #1e293b;
|
|
382
|
+
|
|
383
|
+
--card-bg: #1e293b;
|
|
384
|
+
--header-bg: #334155;
|
|
385
|
+
--text-secondary: #94a3b8;
|
|
386
|
+
--border-dim: #334155;
|
|
387
|
+
--step-hover: #334155;
|
|
388
|
+
--input-bg: #0f172a;
|
|
389
|
+
--input-text: #e2e8f0;
|
|
390
|
+
--code-text: #e2e8f0;
|
|
391
|
+
--info-box-bg: #1e293b;
|
|
392
|
+
}
|
|
393
|
+
[data-theme="forest"] {
|
|
394
|
+
--bg-color: #1a2f1a;
|
|
395
|
+
--sidebar-bg: #243d24;
|
|
396
|
+
--text-color: #e8f5e9;
|
|
397
|
+
--border-color: #2e4d2e;
|
|
398
|
+
--hover-color: #2e4d2e;
|
|
399
|
+
--success-color: #66bb6a;
|
|
400
|
+
--fail-color: #ef5350;
|
|
401
|
+
--skip-color: #ffa726;
|
|
402
|
+
--blue-accent: #4caf50;
|
|
403
|
+
--code-bg: #1b3320;
|
|
404
|
+
|
|
405
|
+
--card-bg: #243d24;
|
|
406
|
+
--header-bg: #2e4d2e;
|
|
407
|
+
--text-secondary: #a5d6a7;
|
|
408
|
+
--border-dim: #2e4d2e;
|
|
409
|
+
--step-hover: #2e4d2e;
|
|
410
|
+
--input-bg: #1a2f1a;
|
|
411
|
+
--input-text: #e8f5e9;
|
|
412
|
+
--code-text: #e8f5e9;
|
|
413
|
+
--info-box-bg: #243d24;
|
|
414
|
+
}
|
|
415
|
+
[data-theme="dracula"] {
|
|
416
|
+
--bg-color: #282a36;
|
|
417
|
+
--sidebar-bg: #44475a;
|
|
418
|
+
--text-color: #f8f8f2;
|
|
419
|
+
--border-color: #6272a4;
|
|
420
|
+
--hover-color: #44475a;
|
|
421
|
+
--success-color: #50fa7b;
|
|
422
|
+
--fail-color: #ff5555;
|
|
423
|
+
--skip-color: #ffb86c;
|
|
424
|
+
--blue-accent: #bd93f9;
|
|
425
|
+
--code-bg: #282a36;
|
|
426
|
+
|
|
427
|
+
--card-bg: #44475a;
|
|
428
|
+
--header-bg: #6272a4;
|
|
429
|
+
--text-secondary: #bd93f9;
|
|
430
|
+
--border-dim: #6272a4;
|
|
431
|
+
--step-hover: #6272a4;
|
|
432
|
+
--input-bg: #282a36;
|
|
433
|
+
--input-text: #f8f8f2;
|
|
434
|
+
--code-text: #f8f8f2;
|
|
435
|
+
--info-box-bg: #44475a;
|
|
255
436
|
}
|
|
256
437
|
body {
|
|
257
438
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -306,7 +487,7 @@ class HtmlReporter {
|
|
|
306
487
|
justify-content: center;
|
|
307
488
|
}
|
|
308
489
|
.total-text { font-size: 1.5em; font-weight: bold; }
|
|
309
|
-
.total-label { font-size: 0.8em; color:
|
|
490
|
+
.total-label { font-size: 0.8em; color: var(--text-secondary); }
|
|
310
491
|
|
|
311
492
|
.stats-row {
|
|
312
493
|
display: flex;
|
|
@@ -336,9 +517,9 @@ class HtmlReporter {
|
|
|
336
517
|
}
|
|
337
518
|
.filter-btn {
|
|
338
519
|
flex: 1;
|
|
339
|
-
background:
|
|
520
|
+
background: var(--card-bg);
|
|
340
521
|
border: 1px solid var(--border-color);
|
|
341
|
-
color:
|
|
522
|
+
color: var(--text-color);
|
|
342
523
|
padding: 5px;
|
|
343
524
|
cursor: pointer;
|
|
344
525
|
font-size: 0.85em;
|
|
@@ -352,9 +533,9 @@ class HtmlReporter {
|
|
|
352
533
|
.search-box {
|
|
353
534
|
width: 100%;
|
|
354
535
|
padding: 10px;
|
|
355
|
-
background-color:
|
|
536
|
+
background-color: var(--input-bg);
|
|
356
537
|
border: 1px solid var(--border-color);
|
|
357
|
-
color:
|
|
538
|
+
color: var(--input-text);
|
|
358
539
|
border-radius: 4px;
|
|
359
540
|
box-sizing: border-box;
|
|
360
541
|
margin-top: 10px;
|
|
@@ -401,7 +582,7 @@ class HtmlReporter {
|
|
|
401
582
|
.detail-header-wrapper {
|
|
402
583
|
padding: 20px 30px;
|
|
403
584
|
border-bottom: 1px solid var(--border-color);
|
|
404
|
-
background-color:
|
|
585
|
+
background-color: var(--card-bg);
|
|
405
586
|
}
|
|
406
587
|
.detail-title { font-size: 1.6em; margin: 10px 0; font-weight: 300; }
|
|
407
588
|
.detail-badges { display: flex; gap: 10px; align-items: center; }
|
|
@@ -423,7 +604,7 @@ class HtmlReporter {
|
|
|
423
604
|
}
|
|
424
605
|
|
|
425
606
|
.section-card {
|
|
426
|
-
background-color:
|
|
607
|
+
background-color: var(--card-bg);
|
|
427
608
|
border: 1px solid var(--border-color);
|
|
428
609
|
border-radius: 6px;
|
|
429
610
|
margin-bottom: 25px;
|
|
@@ -431,7 +612,7 @@ class HtmlReporter {
|
|
|
431
612
|
}
|
|
432
613
|
.section-header {
|
|
433
614
|
padding: 10px 15px;
|
|
434
|
-
background-color:
|
|
615
|
+
background-color: var(--header-bg);
|
|
435
616
|
border-bottom: 1px solid var(--border-color);
|
|
436
617
|
font-weight: 600;
|
|
437
618
|
color: #bbb;
|
|
@@ -443,7 +624,7 @@ class HtmlReporter {
|
|
|
443
624
|
/* Steps */
|
|
444
625
|
.step-row {
|
|
445
626
|
padding: 8px 15px;
|
|
446
|
-
border-bottom: 1px solid
|
|
627
|
+
border-bottom: 1px solid var(--border-dim);
|
|
447
628
|
display: flex;
|
|
448
629
|
gap: 10px;
|
|
449
630
|
align-items: flex-start;
|
|
@@ -452,10 +633,10 @@ class HtmlReporter {
|
|
|
452
633
|
transition: background-color 0.2s;
|
|
453
634
|
}
|
|
454
635
|
.step-row:last-child { border-bottom: none; }
|
|
455
|
-
.step-row:hover { background-color:
|
|
636
|
+
.step-row:hover { background-color: var(--step-hover); }
|
|
456
637
|
.step-status { margin-top: 3px; font-size: 1.2em; line-height: 1; }
|
|
457
638
|
.step-content { flex: 1; }
|
|
458
|
-
.step-meta { color:
|
|
639
|
+
.step-meta { color: var(--text-secondary); font-size: 0.9em; margin-left: 10px; white-space: nowrap; }
|
|
459
640
|
|
|
460
641
|
/* Tree Toggle */
|
|
461
642
|
.toggle-icon {
|
|
@@ -489,7 +670,7 @@ class HtmlReporter {
|
|
|
489
670
|
padding: 15px;
|
|
490
671
|
border-radius: 4px;
|
|
491
672
|
font-family: Consolas, monospace;
|
|
492
|
-
color:
|
|
673
|
+
color: var(--code-text);
|
|
493
674
|
white-space: pre-wrap;
|
|
494
675
|
overflow-x: auto;
|
|
495
676
|
border: 1px solid #30363d;
|
|
@@ -498,12 +679,12 @@ class HtmlReporter {
|
|
|
498
679
|
/* Code Snippet */
|
|
499
680
|
.code-snippet {
|
|
500
681
|
margin-top: 10px;
|
|
501
|
-
background-color:
|
|
682
|
+
background-color: var(--code-bg);
|
|
502
683
|
border-radius: 4px;
|
|
503
684
|
padding: 10px;
|
|
504
685
|
font-family: Consolas, monospace;
|
|
505
686
|
font-size: 0.9em;
|
|
506
|
-
color:
|
|
687
|
+
color: var(--code-text);
|
|
507
688
|
white-space: pre;
|
|
508
689
|
overflow-x: auto;
|
|
509
690
|
border-left: 3px solid #ffcc00;
|
|
@@ -517,14 +698,14 @@ class HtmlReporter {
|
|
|
517
698
|
padding: 20px;
|
|
518
699
|
}
|
|
519
700
|
.media-item {
|
|
520
|
-
background-color:
|
|
701
|
+
background-color: var(--card-bg);
|
|
521
702
|
border-radius: 4px;
|
|
522
703
|
border: 1px solid var(--border-color);
|
|
523
704
|
overflow: hidden;
|
|
524
705
|
}
|
|
525
706
|
.media-header {
|
|
526
707
|
padding: 8px 12px;
|
|
527
|
-
background-color:
|
|
708
|
+
background-color: var(--header-bg);
|
|
528
709
|
color: #ccc;
|
|
529
710
|
font-size: 0.85em;
|
|
530
711
|
border-bottom: 1px solid #444;
|
|
@@ -547,43 +728,53 @@ class HtmlReporter {
|
|
|
547
728
|
<body>
|
|
548
729
|
<div class="sidebar">
|
|
549
730
|
<div class="sidebar-header">
|
|
731
|
+
<div style="width:100%; margin-bottom:15px; display:flex; justify-content:flex-end">
|
|
732
|
+
<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">
|
|
733
|
+
<option value="dark">Dark</option>
|
|
734
|
+
<option value="light">Light</option>
|
|
735
|
+
<option value="muted-light">Muted Light</option>
|
|
736
|
+
<option value="midnight">Midnight</option>
|
|
737
|
+
<option value="forest">Forest</option>
|
|
738
|
+
<option value="dracula">Dracula</option>
|
|
739
|
+
</select>
|
|
740
|
+
</div>
|
|
550
741
|
<div class="chart-container">
|
|
551
742
|
<div class="chart-inner">
|
|
552
743
|
<div class="total-text">${totalTests}</div>
|
|
553
|
-
<div class="total-label"
|
|
744
|
+
<div class="total-label">${t.tests}</div>
|
|
554
745
|
</div>
|
|
555
746
|
</div>
|
|
556
747
|
|
|
557
748
|
<div class="stats-row">
|
|
558
749
|
<div class="stat-item" onclick="setFilter('passed')" title="Show Passed">
|
|
559
750
|
<span class="stat-value" style="color:var(--success-color)">${passedTests}</span>
|
|
560
|
-
<span class="stat-label"
|
|
751
|
+
<span class="stat-label">${t.passed}</span>
|
|
561
752
|
</div>
|
|
562
753
|
<div class="stat-item" onclick="setFilter('failed')" title="Show Failed">
|
|
563
754
|
<span class="stat-value" style="color:var(--fail-color)">${failedTests}</span>
|
|
564
|
-
<span class="stat-label"
|
|
755
|
+
<span class="stat-label">${t.failed}</span>
|
|
565
756
|
</div>
|
|
566
757
|
<div class="stat-item" onclick="setFilter('skipped')" title="Show Skipped">
|
|
567
758
|
<span class="stat-value" style="color:var(--skip-color)">${skippedTests}</span>
|
|
568
|
-
<span class="stat-label"
|
|
759
|
+
<span class="stat-label">${t.skipped}</span>
|
|
569
760
|
</div>
|
|
570
761
|
</div>
|
|
571
762
|
|
|
572
763
|
<div style="margin-top:15px; font-size:0.8em; color:#666; text-align:center" id="reportTimestamp"></div>
|
|
573
764
|
|
|
574
765
|
<!-- 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
|
|
766
|
+
<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)">
|
|
767
|
+
<div style="margin-bottom:5px"><strong style="color:var(--text-secondary)">${t.user}:</strong> <span id="envUser">...</span></div>
|
|
768
|
+
<div style="margin-bottom:5px"><strong style="color:var(--text-secondary)">${t.os}:</strong> <span id="envOs">...</span></div>
|
|
769
|
+
<div><strong style="color:var(--text-secondary)">${t.ip}:</strong> <span id="envIp">...</span></div>
|
|
579
770
|
</div>
|
|
580
771
|
|
|
581
772
|
<div class="filter-group">
|
|
582
|
-
<button class="filter-btn active" onclick="setFilter('all')"
|
|
583
|
-
<button class="filter-btn" onclick="setFilter('failed')"
|
|
773
|
+
<button class="filter-btn active" onclick="setFilter('all')">${t.all}</button>
|
|
774
|
+
<button class="filter-btn" onclick="setFilter('failed')">${t.failures}</button>
|
|
584
775
|
</div>
|
|
585
776
|
|
|
586
|
-
<input type="text" class="search-box" placeholder="
|
|
777
|
+
<input type="text" class="search-box" placeholder="${t.searchTests}" id="searchInput" onkeyup="filterTests()">
|
|
587
778
|
</div>
|
|
588
779
|
<div class="test-list" id="testList"></div>
|
|
589
780
|
</div>
|
|
@@ -601,6 +792,7 @@ class HtmlReporter {
|
|
|
601
792
|
const data = ${resultsData};
|
|
602
793
|
const startTime = ${this.suiteStartTime};
|
|
603
794
|
const envInfo = ${JSON.stringify(this.envInfo)};
|
|
795
|
+
const t = ${JSON.stringify(t)};
|
|
604
796
|
let currentId = null;
|
|
605
797
|
let currentFilter = 'all';
|
|
606
798
|
|
|
@@ -713,19 +905,23 @@ class HtmlReporter {
|
|
|
713
905
|
const statusClass = test.status === 'passed' ? 'badge-passed' :
|
|
714
906
|
(test.status === 'failed' || test.status === 'timedOut') ? 'badge-failed' : 'badge-skipped';
|
|
715
907
|
|
|
908
|
+
// Clean error message (remove Call log)
|
|
909
|
+
const cleanError = test.error ? test.error.split('Call log:')[0].trim() : '';
|
|
910
|
+
|
|
716
911
|
// Steps (using tree renderer)
|
|
717
912
|
const stepsHtml = test.steps.map(generateStepTreeHtml).join('');
|
|
718
913
|
|
|
719
914
|
// Media
|
|
915
|
+
const hasArtifacts = test.screenshot || test.video;
|
|
720
916
|
const mediaHtml = \`
|
|
721
917
|
\${test.screenshot ? \`
|
|
722
918
|
<div class="media-item">
|
|
723
|
-
<div class="media-header"
|
|
919
|
+
<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
920
|
<img src="\${test.screenshot}" onclick="window.open(this.src)">
|
|
725
921
|
</div>\` : ''}
|
|
726
922
|
\${test.video ? \`
|
|
727
923
|
<div class="media-item">
|
|
728
|
-
<div class="media-header"
|
|
924
|
+
<div class="media-header">\${t.video}</div>
|
|
729
925
|
<video src="\${test.video}" controls></video>
|
|
730
926
|
</div>\` : ''}
|
|
731
927
|
\`;
|
|
@@ -735,31 +931,36 @@ class HtmlReporter {
|
|
|
735
931
|
<div class="detail-meta">
|
|
736
932
|
<span class="badge \${statusClass}">\${test.status}</span>
|
|
737
933
|
<span>\${test.suite}</span>
|
|
738
|
-
|
|
934
|
+
\${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
935
|
</div>
|
|
740
936
|
<div class="detail-title">\${test.title}</div>
|
|
937
|
+
<div class="detail-timing" style="color:#888; font-size:0.9em; margin-top:5px">
|
|
938
|
+
<span style="margin-right:15px">\${t.start}: \${test.startTime}</span>
|
|
939
|
+
<span style="margin-right:15px">\${t.end}: \${test.endTime}</span>
|
|
940
|
+
<span>\${t.duration}: \${test.duration}ms</span>
|
|
941
|
+
</div>
|
|
741
942
|
</div>
|
|
742
943
|
|
|
743
944
|
<div class="detail-scroll-area">
|
|
744
|
-
\${
|
|
945
|
+
\${cleanError ? \`
|
|
745
946
|
<div class="section-card error-box">
|
|
746
947
|
<div style="display:flex; justify-content:space-between; margin-bottom:10px">
|
|
747
|
-
<strong style="color:var(--fail-color)"
|
|
948
|
+
<strong style="color:var(--fail-color)">\${t.errorDetails}</strong>
|
|
748
949
|
</div>
|
|
749
|
-
<div class="stack-trace">\${
|
|
950
|
+
<div class="stack-trace">\${cleanError}</div>
|
|
750
951
|
</div>
|
|
751
952
|
\` : ''}
|
|
752
953
|
|
|
753
954
|
<div class="section-card">
|
|
754
|
-
<div class="section-header"
|
|
955
|
+
<div class="section-header">\${t.executionSteps}</div>
|
|
755
956
|
<div class="steps-tree-container">
|
|
756
957
|
\${stepsHtml}
|
|
757
958
|
</div>
|
|
758
959
|
</div>
|
|
759
960
|
|
|
760
|
-
\${
|
|
761
|
-
<div class="section-card">
|
|
762
|
-
<div class="section-header"
|
|
961
|
+
\${hasArtifacts ? \`
|
|
962
|
+
<div id="artifacts-section" class="section-card">
|
|
963
|
+
<div class="section-header">\${t.artifacts}</div>
|
|
763
964
|
<div class="media-grid">\${mediaHtml}</div>
|
|
764
965
|
</div>
|
|
765
966
|
\` : ''}
|
|
@@ -768,6 +969,22 @@ class HtmlReporter {
|
|
|
768
969
|
content.classList.add('active');
|
|
769
970
|
}
|
|
770
971
|
|
|
972
|
+
function setTheme(themeName) {
|
|
973
|
+
document.documentElement.setAttribute('data-theme', themeName);
|
|
974
|
+
localStorage.setItem('report-theme', themeName);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Initialize Theme
|
|
978
|
+
// We prioritize the theme from report.config.json if it's set.
|
|
979
|
+
// This ensures the report respects the project configuration.
|
|
980
|
+
const configTheme = '${config.theme}' || 'dark';
|
|
981
|
+
|
|
982
|
+
// On initial load, we follow the config.
|
|
983
|
+
// However, we can check if there was a MANUALLY set preference in this session.
|
|
984
|
+
// For now, let's just respect the config theme as requested.
|
|
985
|
+
setTheme(configTheme);
|
|
986
|
+
document.getElementById('themeSelector').value = configTheme;
|
|
987
|
+
|
|
771
988
|
// Initial render
|
|
772
989
|
renderList(data);
|
|
773
990
|
</script>
|