@flash-ai-team/flash-test-framework 0.0.12 → 0.0.13

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/cli/index.js CHANGED
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
9
10
  const args = process.argv.slice(2);
10
11
  const command = args[0];
11
12
  if (command === 'init') {
@@ -15,8 +16,13 @@ if (command === 'init') {
15
16
  else if (command === '--version' || command === '-v') {
16
17
  printVersion();
17
18
  }
19
+ else if (command === 'history') {
20
+ const suiteName = args[1];
21
+ showHistory(suiteName);
22
+ }
18
23
  else {
19
24
  console.log('Usage: flash-test init [project-name]');
25
+ console.log(' flash-test history [suite-name]');
20
26
  console.log(' flash-test -v / --version');
21
27
  process.exit(1);
22
28
  }
@@ -149,3 +155,184 @@ function createFile(filePath, content) {
149
155
  console.log(`Created: ${path_1.default.basename(filePath)}`);
150
156
  }
151
157
  }
158
+ function showHistory(suiteFilter) {
159
+ const reportsDir = path_1.default.join(process.cwd(), 'reports');
160
+ if (!fs_1.default.existsSync(reportsDir)) {
161
+ console.log('No reports found.');
162
+ return;
163
+ }
164
+ let runs = [];
165
+ const suites = suiteFilter ? [suiteFilter] : fs_1.default.readdirSync(reportsDir).filter(f => fs_1.default.statSync(path_1.default.join(reportsDir, f)).isDirectory());
166
+ for (const suite of suites) {
167
+ const suitePath = path_1.default.join(reportsDir, suite);
168
+ if (!fs_1.default.existsSync(suitePath))
169
+ continue;
170
+ const testRuns = fs_1.default.readdirSync(suitePath).filter(f => f.startsWith('test_') && fs_1.default.statSync(path_1.default.join(suitePath, f)).isDirectory());
171
+ for (const run of testRuns) {
172
+ const runPath = path_1.default.join(suitePath, run);
173
+ let status = 'Unknown';
174
+ let duration = 0;
175
+ let total = 0;
176
+ let passed = 0;
177
+ let failed = 0;
178
+ // Try to read custom-report.json for details
179
+ const jsonPath = path_1.default.join(runPath, 'custom-report.json');
180
+ if (fs_1.default.existsSync(jsonPath)) {
181
+ try {
182
+ const data = JSON.parse(fs_1.default.readFileSync(jsonPath, 'utf-8'));
183
+ if (Array.isArray(data)) {
184
+ total = data.length;
185
+ passed = data.filter((t) => t.status === 'passed').length;
186
+ failed = data.filter((t) => t.status === 'failed' || t.status === 'timedOut').length;
187
+ // Sum duration or max? Let's use sum for now as a simple metric
188
+ duration = data.reduce((acc, curr) => acc + (curr.duration || 0), 0);
189
+ status = failed > 0 ? 'Failed' : (passed === total && total > 0 ? 'Passed' : 'Mixed');
190
+ }
191
+ else if (data.stats) {
192
+ // Legacy support if needed
193
+ duration = data.stats.duration;
194
+ total = data.stats.total;
195
+ passed = data.stats.passed;
196
+ failed = data.stats.failed;
197
+ status = failed > 0 ? 'Failed' : (passed === total && total > 0 ? 'Passed' : 'Mixed');
198
+ }
199
+ }
200
+ catch (e) { /* ignore */ }
201
+ }
202
+ // Extract timestamp from folder name test_YYYY-MM-DD_HHmmss
203
+ const timePart = run.replace('test_', '');
204
+ // Format for display: YYYY-MM-DD HH:mm:ss
205
+ const formattedTime = timePart.replace('_', ' ').replace(/(\d{4}-\d{2}-\d{2}) (\d{2})(\d{2})(\d{2})/, '$1 $2:$3:$4');
206
+ runs.push({
207
+ suite,
208
+ runFolder: run,
209
+ timestamp: formattedTime,
210
+ rawTimestamp: timePart,
211
+ path: runPath,
212
+ status,
213
+ duration,
214
+ passed,
215
+ failed,
216
+ total
217
+ });
218
+ }
219
+ }
220
+ // Sort by newest first
221
+ runs.sort((a, b) => b.rawTimestamp.localeCompare(a.rawTimestamp));
222
+ // Console Output
223
+ console.table(runs.map(r => ({
224
+ Suite: r.suite,
225
+ Timestamp: r.timestamp,
226
+ Status: r.status,
227
+ Passed: r.passed,
228
+ Failed: r.failed,
229
+ Duration: `${(r.duration / 1000).toFixed(2)}s`
230
+ })));
231
+ // Generate HTML Dashboard
232
+ generateHistoryHtml(runs, reportsDir);
233
+ }
234
+ function generateHistoryHtml(runs, reportsDir) {
235
+ const htmlPath = path_1.default.join(reportsDir, 'history.html');
236
+ // Group by Stats
237
+ const totalRuns = runs.length;
238
+ const passedRuns = runs.filter(r => r.status === 'Passed').length;
239
+ const failedRuns = runs.filter(r => r.status === 'Failed').length;
240
+ const rows = runs.map(r => {
241
+ const statusClass = r.status === 'Passed' ? 'passed' : (r.status === 'Failed' ? 'failed' : 'mixed');
242
+ // Relative path from reports/history.html to reports/suite/run/report.html
243
+ const reportLink = `${r.suite}/${r.runFolder}/report.html`;
244
+ return `
245
+ <tr>
246
+ <td>${r.timestamp}</td>
247
+ <td><span class="suite-badge">${r.suite}</span></td>
248
+ <td><span class="status-badge status-${statusClass}">${r.status}</span></td>
249
+ <td>${r.passed} / ${r.total}</td>
250
+ <td>${(r.duration / 1000).toFixed(2)}s</td>
251
+ <td><a href="${reportLink}" target="_blank" class="view-btn">View Report</a></td>
252
+ </tr>
253
+ `;
254
+ }).join('');
255
+ const html = `
256
+ <!DOCTYPE html>
257
+ <html lang="en">
258
+ <head>
259
+ <meta charset="UTF-8">
260
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
261
+ <title>Test Run History</title>
262
+ <style>
263
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; background: #f4f4f4; padding: 20px; color: #333; }
264
+ .container { max-width: 1000px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
265
+ h1 { margin-top: 0; border-bottom: 2px solid #eee; padding-bottom: 15px; }
266
+
267
+ .stats-grid { display: flex; gap: 20px; margin-bottom: 30px; }
268
+ .stat-card { flex: 1; background: #fafafa; padding: 15px; border-radius: 6px; text-align: center; border: 1px solid #eee; }
269
+ .stat-value { font-size: 2em; font-weight: bold; display: block; }
270
+ .stat-label { color: #666; font-size: 0.9em; text-transform: uppercase; letter-spacing: 1px; }
271
+ .stat-passed .stat-value { color: #4caf50; }
272
+ .stat-failed .stat-value { color: #f44336; }
273
+
274
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; }
275
+ th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eee; }
276
+ th { background: #f8f8f8; font-weight: 600; color: #555; }
277
+ tr:hover { background: #fcfcfc; }
278
+
279
+ .status-badge { padding: 4px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 600; }
280
+ .status-passed { background: #e8f5e9; color: #2e7d32; }
281
+ .status-failed { background: #ffebee; color: #c62828; }
282
+ .status-mixed { background: #fff3e0; color: #ef6c00; }
283
+
284
+ .suite-badge { background: #e3f2fd; color: #1565c0; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
285
+
286
+ .view-btn { text-decoration: none; background: #333; color: white; padding: 6px 12px; border-radius: 4px; font-size: 0.9em; transition: background 0.2s; }
287
+ .view-btn:hover { background: #555; }
288
+ </style>
289
+ </head>
290
+ <body>
291
+ <div class="container">
292
+ <h1>📜 Test Run History</h1>
293
+
294
+ <div class="stats-grid">
295
+ <div class="stat-card">
296
+ <span class="stat-value">${totalRuns}</span>
297
+ <span class="stat-label">Total Runs</span>
298
+ </div>
299
+ <div class="stat-card stat-passed">
300
+ <span class="stat-value">${passedRuns}</span>
301
+ <span class="stat-label">Passed</span>
302
+ </div>
303
+ <div class="stat-card stat-failed">
304
+ <span class="stat-value">${failedRuns}</span>
305
+ <span class="stat-label">Failed</span>
306
+ </div>
307
+ </div>
308
+
309
+ <table>
310
+ <thead>
311
+ <tr>
312
+ <th>Timestamp</th>
313
+ <th>Suite</th>
314
+ <th>Status</th>
315
+ <th>Pass / Total</th>
316
+ <th>Duration</th>
317
+ <th>Action</th>
318
+ </tr>
319
+ </thead>
320
+ <tbody>
321
+ ${rows}
322
+ </tbody>
323
+ </table>
324
+ </div>
325
+ </body>
326
+ </html>
327
+ `;
328
+ fs_1.default.writeFileSync(htmlPath, html);
329
+ console.log(`\nHistory Dashboard generated: ${htmlPath}`);
330
+ // Open in browser
331
+ try {
332
+ const startCmd = process.platform === 'win32' ? 'start' : (process.platform === 'darwin' ? 'open' : 'xdg-open');
333
+ (0, child_process_1.execSync)(`${startCmd} "${htmlPath}"`);
334
+ }
335
+ catch (e) {
336
+ console.log('Could not auto-open browser. Please open the file manually.');
337
+ }
338
+ }
@@ -1,10 +1,5 @@
1
1
  import { Page, TestInfo } from '@playwright/test';
2
2
  export declare const KeywordRegistry: Record<string, Function>;
3
- /**
4
- * Decorator to register a function as a Keyword and wrap it in a Playwright Step
5
- * Supports both Stage 3 (Standard) and Stage 2 (Legacy/Experimental) decorators.
6
- * @param name Custom name for the keyword (optional)
7
- */
8
3
  export declare function Keyword(name?: string): (targetOrValue: any, contextOrKey: any, descriptor?: PropertyDescriptor) => any;
9
4
  /**
10
5
  * Context holder to share Page and TestInfo across keywords
@@ -33,6 +33,16 @@ function formatArg(arg) {
33
33
  * Supports both Stage 3 (Standard) and Stage 2 (Legacy/Experimental) decorators.
34
34
  * @param name Custom name for the keyword (optional)
35
35
  */
36
+ // Helper to check for options object
37
+ function extractDescription(args) {
38
+ if (args.length > 0) {
39
+ const lastArg = args[args.length - 1];
40
+ if (lastArg && typeof lastArg === 'object' && 'description' in lastArg && typeof lastArg.description === 'string') {
41
+ return lastArg.description;
42
+ }
43
+ }
44
+ return undefined;
45
+ }
36
46
  function Keyword(name) {
37
47
  return function (targetOrValue, contextOrKey, descriptor) {
38
48
  // Stage 3: (value, context) where context is an object
@@ -43,8 +53,14 @@ function Keyword(name) {
43
53
  // Registry
44
54
  exports.KeywordRegistry[keywordName] = originalMethod;
45
55
  return async function (...args) {
46
- const argString = args.map(formatArg).join(', ');
47
- const stepName = argString ? `${keywordName} (${argString})` : keywordName;
56
+ const customDescription = extractDescription(args);
57
+ // Filter out the options object from display args if it was used for description
58
+ const displayArgs = customDescription ? args.slice(0, -1) : args;
59
+ const argString = displayArgs.map(formatArg).join(', ');
60
+ let stepName = argString ? `${keywordName} (${argString})` : keywordName;
61
+ if (customDescription) {
62
+ stepName = customDescription;
63
+ }
48
64
  return await test_1.test.step(stepName, async () => {
49
65
  return await originalMethod.apply(this, args);
50
66
  });
@@ -60,8 +76,14 @@ function Keyword(name) {
60
76
  const originalMethod = legacyDescriptor.value;
61
77
  const keywordName = name || contextOrKey;
62
78
  legacyDescriptor.value = async function (...args) {
63
- const argString = args.map(formatArg).join(', ');
64
- const stepName = argString ? `${keywordName} (${argString})` : keywordName;
79
+ const customDescription = extractDescription(args);
80
+ // Filter out the options object from display args if it was used for description
81
+ const displayArgs = customDescription ? args.slice(0, -1) : args;
82
+ const argString = displayArgs.map(formatArg).join(', ');
83
+ let stepName = argString ? `${keywordName} (${argString})` : keywordName;
84
+ if (customDescription) {
85
+ stepName = customDescription;
86
+ }
65
87
  return await test_1.test.step(stepName, async () => {
66
88
  return await originalMethod.apply(this, args);
67
89
  });
@@ -6,66 +6,91 @@ export declare class Web {
6
6
  * Navigates to the specified URL.
7
7
  * @param url - The URL to navigate to.
8
8
  */
9
- static navigateToUrl(url: string): Promise<void>;
9
+ static navigateToUrl(url: string, options?: {
10
+ description?: string;
11
+ }): Promise<void>;
10
12
  /**
11
13
  * Verifies that the current page URL matches the expected URL.
12
14
  * @param url - The expected URL.
13
15
  * @param timeout - The maximum time to wait in milliseconds (default: 5000).
14
16
  */
15
- static verifyUrl(url: string, timeout?: number): Promise<void>;
17
+ static verifyUrl(url: string, timeout?: number, options?: {
18
+ description?: string;
19
+ }): Promise<void>;
16
20
  /**
17
21
  * Clicks on the specified element.
18
22
  * @param to - The TestObject representing the element.
23
+ * @param options - Optional parameters (description).
19
24
  */
20
- static click(to: TestObject): Promise<void>;
25
+ static click(to: TestObject, options?: {
26
+ description?: string;
27
+ }): Promise<void>;
21
28
  /**
22
29
  * Sets the text of an input element.
23
30
  * @param to - The TestObject representing the input element.
24
31
  * @param text - The text to set.
25
32
  */
26
- static setText(to: TestObject, text: string): Promise<void>;
33
+ static setText(to: TestObject, text: string, options?: {
34
+ description?: string;
35
+ }): Promise<void>;
27
36
  /**
28
37
  * Searches for the specified text using heuristic selectors.
29
38
  * @param text - The text to search for.
30
39
  */
31
- static search(text: string): Promise<void>;
40
+ static search(text: string, options?: {
41
+ description?: string;
42
+ }): Promise<void>;
32
43
  /**
33
44
  * Verifies that the specified element is present (visible) on the page.
34
45
  * @param to - The TestObject representing the element.
35
46
  * @param timeout - The maximum time to wait in milliseconds (default: 5000).
36
47
  */
37
- static verifyElementPresent(to: TestObject, timeout?: number): Promise<void>;
48
+ static verifyElementPresent(to: TestObject, timeout?: number, options?: {
49
+ description?: string;
50
+ }): Promise<void>;
38
51
  /**
39
52
  * Gets the visible text of the specified element.
40
53
  * @param to - The TestObject representing the element.
41
54
  * @returns The text content of the element.
42
55
  */
43
- static getText(to: TestObject): Promise<string>;
56
+ static getText(to: TestObject, options?: {
57
+ description?: string;
58
+ }): Promise<string>;
44
59
  /**
45
60
  * Closes the current browser context/page.
46
61
  */
47
- static closeBrowser(): Promise<void>;
62
+ static closeBrowser(options?: {
63
+ description?: string;
64
+ }): Promise<void>;
48
65
  /**
49
66
  * Double clicks on the specified element.
50
67
  * @param to - The TestObject representing the element.
51
68
  */
52
- static doubleClick(to: TestObject): Promise<void>;
69
+ static doubleClick(to: TestObject, options?: {
70
+ description?: string;
71
+ }): Promise<void>;
53
72
  /**
54
73
  * Right clicks (context click) on the specified element.
55
74
  * @param to - The TestObject representing the element.
56
75
  */
57
- static rightClick(to: TestObject): Promise<void>;
76
+ static rightClick(to: TestObject, options?: {
77
+ description?: string;
78
+ }): Promise<void>;
58
79
  /**
59
80
  * Hovers over the specified element.
60
81
  * @param to - The TestObject representing the element.
61
82
  */
62
- static mouseOver(to: TestObject): Promise<void>;
83
+ static mouseOver(to: TestObject, options?: {
84
+ description?: string;
85
+ }): Promise<void>;
63
86
  /**
64
87
  * Drags the source element and drops it onto the target element.
65
88
  * @param source - The TestObject representing the source element.
66
89
  * @param target - The TestObject representing the target element.
67
90
  */
68
- static dragAndDrop(source: TestObject, target: TestObject): Promise<void>;
91
+ static dragAndDrop(source: TestObject, target: TestObject, options?: {
92
+ description?: string;
93
+ }): Promise<void>;
69
94
  /**
70
95
  * Checks (selects) the specified checkbox or radio button.
71
96
  * @param to - The TestObject representing the element.
@@ -28,7 +28,7 @@ class Web {
28
28
  * Navigates to the specified URL.
29
29
  * @param url - The URL to navigate to.
30
30
  */
31
- static async navigateToUrl(url) {
31
+ static async navigateToUrl(url, options) {
32
32
  await this.page.goto(url);
33
33
  }
34
34
  /**
@@ -36,14 +36,15 @@ class Web {
36
36
  * @param url - The expected URL.
37
37
  * @param timeout - The maximum time to wait in milliseconds (default: 5000).
38
38
  */
39
- static async verifyUrl(url, timeout = 5000) {
39
+ static async verifyUrl(url, timeout = 5000, options) {
40
40
  await (0, test_1.expect)(this.page).toHaveURL(url, { timeout });
41
41
  }
42
42
  /**
43
43
  * Clicks on the specified element.
44
44
  * @param to - The TestObject representing the element.
45
+ * @param options - Optional parameters (description).
45
46
  */
46
- static async click(to) {
47
+ static async click(to, options) {
47
48
  await this.getLocator(to).click();
48
49
  }
49
50
  /**
@@ -51,14 +52,14 @@ class Web {
51
52
  * @param to - The TestObject representing the input element.
52
53
  * @param text - The text to set.
53
54
  */
54
- static async setText(to, text) {
55
+ static async setText(to, text, options) {
55
56
  await this.getLocator(to).fill(text);
56
57
  }
57
58
  /**
58
59
  * Searches for the specified text using heuristic selectors.
59
60
  * @param text - The text to search for.
60
61
  */
61
- static async search(text) {
62
+ static async search(text, options) {
62
63
  // Common search input selectors
63
64
  const selectors = [
64
65
  'input[name="q"]',
@@ -78,7 +79,7 @@ class Web {
78
79
  * @param to - The TestObject representing the element.
79
80
  * @param timeout - The maximum time to wait in milliseconds (default: 5000).
80
81
  */
81
- static async verifyElementPresent(to, timeout = 5000) {
82
+ static async verifyElementPresent(to, timeout = 5000, options) {
82
83
  await (0, test_1.expect)(this.getLocator(to)).toBeVisible({ timeout });
83
84
  }
84
85
  /**
@@ -86,13 +87,13 @@ class Web {
86
87
  * @param to - The TestObject representing the element.
87
88
  * @returns The text content of the element.
88
89
  */
89
- static async getText(to) {
90
+ static async getText(to, options) {
90
91
  return await this.getLocator(to).innerText();
91
92
  }
92
93
  /**
93
94
  * Closes the current browser context/page.
94
95
  */
95
- static async closeBrowser() {
96
+ static async closeBrowser(options) {
96
97
  await this.page.close();
97
98
  }
98
99
  // --- Interaction Keywords ---
@@ -100,21 +101,21 @@ class Web {
100
101
  * Double clicks on the specified element.
101
102
  * @param to - The TestObject representing the element.
102
103
  */
103
- static async doubleClick(to) {
104
+ static async doubleClick(to, options) {
104
105
  await this.getLocator(to).dblclick();
105
106
  }
106
107
  /**
107
108
  * Right clicks (context click) on the specified element.
108
109
  * @param to - The TestObject representing the element.
109
110
  */
110
- static async rightClick(to) {
111
+ static async rightClick(to, options) {
111
112
  await this.getLocator(to).click({ button: 'right' });
112
113
  }
113
114
  /**
114
115
  * Hovers over the specified element.
115
116
  * @param to - The TestObject representing the element.
116
117
  */
117
- static async mouseOver(to) {
118
+ static async mouseOver(to, options) {
118
119
  await this.getLocator(to).hover();
119
120
  }
120
121
  /**
@@ -122,7 +123,7 @@ class Web {
122
123
  * @param source - The TestObject representing the source element.
123
124
  * @param target - The TestObject representing the target element.
124
125
  */
125
- static async dragAndDrop(source, target) {
126
+ static async dragAndDrop(source, target, options) {
126
127
  await this.getLocator(source).dragTo(this.getLocator(target));
127
128
  }
128
129
  /**
@@ -391,73 +392,73 @@ exports.Web = Web;
391
392
  __decorate([
392
393
  (0, Keyword_1.Keyword)("Navigate To Url"),
393
394
  __metadata("design:type", Function),
394
- __metadata("design:paramtypes", [String]),
395
+ __metadata("design:paramtypes", [String, Object]),
395
396
  __metadata("design:returntype", Promise)
396
397
  ], Web, "navigateToUrl", null);
397
398
  __decorate([
398
399
  (0, Keyword_1.Keyword)("Verify Url"),
399
400
  __metadata("design:type", Function),
400
- __metadata("design:paramtypes", [String, Number]),
401
+ __metadata("design:paramtypes", [String, Number, Object]),
401
402
  __metadata("design:returntype", Promise)
402
403
  ], Web, "verifyUrl", null);
403
404
  __decorate([
404
405
  (0, Keyword_1.Keyword)("Click"),
405
406
  __metadata("design:type", Function),
406
- __metadata("design:paramtypes", [ObjectRepository_1.TestObject]),
407
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, Object]),
407
408
  __metadata("design:returntype", Promise)
408
409
  ], Web, "click", null);
409
410
  __decorate([
410
411
  (0, Keyword_1.Keyword)("Set Text"),
411
412
  __metadata("design:type", Function),
412
- __metadata("design:paramtypes", [ObjectRepository_1.TestObject, String]),
413
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, String, Object]),
413
414
  __metadata("design:returntype", Promise)
414
415
  ], Web, "setText", null);
415
416
  __decorate([
416
417
  (0, Keyword_1.Keyword)("Search"),
417
418
  __metadata("design:type", Function),
418
- __metadata("design:paramtypes", [String]),
419
+ __metadata("design:paramtypes", [String, Object]),
419
420
  __metadata("design:returntype", Promise)
420
421
  ], Web, "search", null);
421
422
  __decorate([
422
423
  (0, Keyword_1.Keyword)("Verify Element Present"),
423
424
  __metadata("design:type", Function),
424
- __metadata("design:paramtypes", [ObjectRepository_1.TestObject, Number]),
425
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, Number, Object]),
425
426
  __metadata("design:returntype", Promise)
426
427
  ], Web, "verifyElementPresent", null);
427
428
  __decorate([
428
429
  (0, Keyword_1.Keyword)("Get Text"),
429
430
  __metadata("design:type", Function),
430
- __metadata("design:paramtypes", [ObjectRepository_1.TestObject]),
431
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, Object]),
431
432
  __metadata("design:returntype", Promise)
432
433
  ], Web, "getText", null);
433
434
  __decorate([
434
435
  (0, Keyword_1.Keyword)("Close Browser"),
435
436
  __metadata("design:type", Function),
436
- __metadata("design:paramtypes", []),
437
+ __metadata("design:paramtypes", [Object]),
437
438
  __metadata("design:returntype", Promise)
438
439
  ], Web, "closeBrowser", null);
439
440
  __decorate([
440
441
  (0, Keyword_1.Keyword)("Double Click"),
441
442
  __metadata("design:type", Function),
442
- __metadata("design:paramtypes", [ObjectRepository_1.TestObject]),
443
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, Object]),
443
444
  __metadata("design:returntype", Promise)
444
445
  ], Web, "doubleClick", null);
445
446
  __decorate([
446
447
  (0, Keyword_1.Keyword)("Right Click"),
447
448
  __metadata("design:type", Function),
448
- __metadata("design:paramtypes", [ObjectRepository_1.TestObject]),
449
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, Object]),
449
450
  __metadata("design:returntype", Promise)
450
451
  ], Web, "rightClick", null);
451
452
  __decorate([
452
453
  (0, Keyword_1.Keyword)("Mouse Over"),
453
454
  __metadata("design:type", Function),
454
- __metadata("design:paramtypes", [ObjectRepository_1.TestObject]),
455
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, Object]),
455
456
  __metadata("design:returntype", Promise)
456
457
  ], Web, "mouseOver", null);
457
458
  __decorate([
458
459
  (0, Keyword_1.Keyword)("Drag And Drop"),
459
460
  __metadata("design:type", Function),
460
- __metadata("design:paramtypes", [ObjectRepository_1.TestObject, ObjectRepository_1.TestObject]),
461
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, ObjectRepository_1.TestObject, Object]),
461
462
  __metadata("design:returntype", Promise)
462
463
  ], Web, "dragAndDrop", null);
463
464
  __decorate([
@@ -2,6 +2,7 @@ import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@
2
2
  declare class HtmlReporter implements Reporter {
3
3
  private suiteStartTime;
4
4
  private reportPath;
5
+ private envInfo;
5
6
  private fileCache;
6
7
  onBegin(config: FullConfig, suite: Suite): void;
7
8
  onEnd(result: FullResult): Promise<void>;
@@ -35,11 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const fs = __importStar(require("fs"));
37
37
  const path = __importStar(require("path"));
38
+ const os = __importStar(require("os"));
38
39
  const ReporterUtils_1 = require("./ReporterUtils");
39
40
  class HtmlReporter {
40
41
  constructor() {
41
42
  this.suiteStartTime = 0;
42
43
  this.reportPath = '';
44
+ this.envInfo = { user: 'Unknown', os: 'Unknown', ip: 'Unknown' };
43
45
  // Cache for file contents to avoid repeated reads
44
46
  this.fileCache = new Map();
45
47
  // onExit removed as generation is now in onEnd
@@ -49,6 +51,31 @@ class HtmlReporter {
49
51
  this.suiteStartTime = Date.now();
50
52
  const folder = (0, ReporterUtils_1.getReportFolder)(suite);
51
53
  this.reportPath = path.join(folder, 'report.html');
54
+ // Collect Env Info
55
+ try {
56
+ const userInfo = os.userInfo();
57
+ this.envInfo.user = userInfo.username;
58
+ }
59
+ catch (e) { /* ignore */ }
60
+ try {
61
+ this.envInfo.os = `${os.type()} ${os.release()} (${os.platform()})`;
62
+ }
63
+ catch (e) { /* ignore */ }
64
+ try {
65
+ const nets = os.networkInterfaces();
66
+ for (const name of Object.keys(nets)) {
67
+ for (const net of nets[name]) {
68
+ // Skip internal and non-IPv4 addresses
69
+ if (net.family === 'IPv4' && !net.internal) {
70
+ this.envInfo.ip = net.address;
71
+ break;
72
+ }
73
+ }
74
+ if (this.envInfo.ip !== 'Unknown')
75
+ break;
76
+ }
77
+ }
78
+ catch (e) { /* ignore */ }
52
79
  }
53
80
  async onEnd(result) {
54
81
  if (!this.reportPath)
@@ -542,6 +569,15 @@ class HtmlReporter {
542
569
  </div>
543
570
  </div>
544
571
 
572
+ <div style="margin-top:15px; font-size:0.8em; color:#666; text-align:center" id="reportTimestamp"></div>
573
+
574
+ <!-- Environment Info -->
575
+ <div style="margin-top:15px; padding:15px; background-color:#2a2d2e; border-top:1px solid var(--border-color); font-size:0.85em; color:#bbb">
576
+ <div style="margin-bottom:5px"><strong style="color:#888">User:</strong> <span id="envUser">...</span></div>
577
+ <div style="margin-bottom:5px"><strong style="color:#888">OS:</strong> <span id="envOs">...</span></div>
578
+ <div><strong style="color:#888">IP:</strong> <span id="envIp">...</span></div>
579
+ </div>
580
+
545
581
  <div class="filter-group">
546
582
  <button class="filter-btn active" onclick="setFilter('all')">All</button>
547
583
  <button class="filter-btn" onclick="setFilter('failed')">Failures</button>
@@ -563,9 +599,21 @@ class HtmlReporter {
563
599
 
564
600
  <script>
565
601
  const data = ${resultsData};
602
+ const startTime = ${this.suiteStartTime};
603
+ const envInfo = ${JSON.stringify(this.envInfo)};
566
604
  let currentId = null;
567
605
  let currentFilter = 'all';
568
606
 
607
+ // Set local timestamp
608
+ document.getElementById('reportTimestamp').textContent = new Date(startTime).toLocaleString();
609
+
610
+ // Set Env Info
611
+ if (envInfo) {
612
+ document.getElementById('envUser').textContent = envInfo.user;
613
+ document.getElementById('envOs').textContent = envInfo.os;
614
+ document.getElementById('envIp').textContent = envInfo.ip;
615
+ }
616
+
569
617
  function setFilter(filter) {
570
618
  currentFilter = filter;
571
619
  // Update buttons
@@ -39,15 +39,26 @@ exports.resetReportFolderCache = resetReportFolderCache;
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  let cachedReportFolder = null;
42
+ // Helper to get local YYYY-MM-DD_HHmmss timestamp
43
+ function getLocalTimestamp() {
44
+ const now = new Date();
45
+ const year = now.getFullYear();
46
+ const month = String(now.getMonth() + 1).padStart(2, '0');
47
+ const day = String(now.getDate()).padStart(2, '0');
48
+ const hours = String(now.getHours()).padStart(2, '0');
49
+ const minutes = String(now.getMinutes()).padStart(2, '0');
50
+ const seconds = String(now.getSeconds()).padStart(2, '0');
51
+ return `${year}-${month}-${day}_${hours}${minutes}${seconds}`;
52
+ }
42
53
  function generateReportPath(baseDir = 'reports', prefix = 'test') {
43
- const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '').split('.')[0];
54
+ const timestamp = getLocalTimestamp();
44
55
  return path.join(process.cwd(), baseDir, `${prefix}_${timestamp}`);
45
56
  }
46
57
  function getReportFolder(suite) {
47
58
  if (cachedReportFolder) {
48
59
  return cachedReportFolder;
49
60
  }
50
- const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '').split('.')[0];
61
+ const timestamp = getLocalTimestamp();
51
62
  // Find the first test file suite
52
63
  let suiteName = 'TestRun';
53
64
  if (suite.suites.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flash-ai-team/flash-test-framework",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "A powerful keyword-driven automation framework built on top of Playwright and TypeScript.",
5
5
  "keywords": [
6
6
  "playwright",