@flash-ai-team/flash-test-framework 0.0.6 → 0.0.9

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 CHANGED
@@ -5,7 +5,7 @@ A powerful, keyword-driven automation framework built on top of Playwright and T
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install flash-test-framework
8
+ npm install @flash-ai-team/flash-test-framework
9
9
  ```
10
10
 
11
11
  ## Setup
@@ -17,7 +17,7 @@ npm install flash-test-framework
17
17
 
18
18
  2. **Configuration**:
19
19
  * **Email Reporting**: Create `email.config.json` in your project root if you want email reports.
20
- * **AI Features**: Create `ai.config.json` with your OpenAI API key to use `AIWebUI`.
20
+ * **AI Features**: Create `ai.config.json` with your OpenAI API key to use `AIWeb`.
21
21
  ```json
22
22
  {
23
23
  "enabled": true,
@@ -29,56 +29,47 @@ npm install flash-test-framework
29
29
 
30
30
  ## Usage
31
31
 
32
- ### 1. Basic Web Automation (`web`)
32
+ ### 1. Basic Web Automation (`Web`)
33
33
 
34
- Use `web` for standard, deterministic interactions using selectors.
34
+ Use `Web` for standard, deterministic interactions using selectors.
35
+ Import `test` from the framework to automatically initialize the context.
35
36
 
36
37
  ```typescript
37
- import { test } from '@playwright/test';
38
- import { web, findTestObject, KeywordContext } from 'flash-test-framework';
38
+ import { test, Web, el, KeywordContext } from '@flash-ai-team/flash-test-framework';
39
39
 
40
40
  test.describe('My Test Suite', () => {
41
- test.beforeEach(async ({ page }, testInfo) => {
42
- // Initialize Context
43
- KeywordContext.page = page;
44
- KeywordContext.testInfo = testInfo;
45
- });
41
+ // Context is automatically initialized by the 'test' fixture
46
42
 
47
43
  test('Login Test', async () => {
48
- await web.navigateToUrl('https://example.com/login');
44
+ await Web.navigateToUrl('https://example.com/login');
49
45
 
50
- const usernameInput = findTestObject('#username', 'Username Field');
51
- const passwordInput = findTestObject('#password', 'Password Field');
52
- const loginButton = findTestObject('button[type="submit"]', 'Login Button');
46
+ const usernameInput = el('#username', 'Username Field');
47
+ const passwordInput = el('#password', 'Password Field');
48
+ const loginButton = el('button[type="submit"]', 'Login Button');
53
49
 
54
- await web.setText(usernameInput, 'myuser');
55
- await web.setText(passwordInput, 'mypassword');
56
- await web.click(loginButton);
50
+ await Web.setText(usernameInput, 'myuser');
51
+ await Web.setText(passwordInput, 'mypassword');
52
+ await Web.click(loginButton);
57
53
 
58
54
  // precise search using heuristics
59
- await web.search("Specific Item");
55
+ await Web.search("Specific Item");
60
56
  });
61
57
  });
62
58
  ```
63
59
 
64
- ### 2. AI & Manual Steps (`aiWeb`)
60
+ ### 2. AI & Manual Steps (`AIWeb`)
65
61
 
66
- Use `aiWeb` to write tests in plain English or to click elements that are hard to target (like map markers) using images.
62
+ Use `AIWeb` to write tests in plain English or to click elements that are hard to target (like map markers) using images.
67
63
 
68
64
  **Example: `AIManualSteps.spec.ts`**
69
65
 
70
66
  ```typescript
71
- import { test } from '@playwright/test';
72
- import { aiWeb, KeywordContext } from 'flash-test-framework';
67
+ import { test, AIWeb } from '@flash-ai-team/flash-test-framework';
73
68
 
74
69
  test.describe('AI Scenario', () => {
75
- test.beforeEach(async ({ page }, testInfo) => {
76
- KeywordContext.page = page;
77
- KeywordContext.testInfo = testInfo;
78
- });
79
70
 
80
71
  test('Google Maps Flow', async () => {
81
- await aiWeb.executeManualSteps(`
72
+ await AIWeb.executeManualSteps(`
82
73
  1. Navigate to "https://www.google.com/maps"
83
74
  2. Click on "Accept all"
84
75
  3. Search "KFC"
@@ -96,15 +87,56 @@ test.describe('AI Scenario', () => {
96
87
 
97
88
  ## Keywords
98
89
 
99
- ### web
100
- * `navigateToUrl(url)`
101
- * `click(testObject)`
102
- * `setText(testObject, text)`
103
- * `search(text)` - Smart search input targeting.
104
- * `clickImage(imagePath)` - Visual testing.
105
- * `pressKey(key)` - Keyboard interactions.
106
- * `verifyElementPresent(testObject)`
107
- * ...and more.
108
-
109
- ### aiWeb
110
- * `executeManualSteps(steps: string)` - Parses multi-line natural language steps.
90
+ ### Web
91
+
92
+ #### Navigation
93
+ * `navigateToUrl(url)`: Navigate to the specified URL.
94
+ * `refresh()`: Refresh the current page.
95
+ * `back()`: Navigate back.
96
+ * `forward()`: Navigate forward.
97
+ * `closeBrowser()`: Close the browser instance.
98
+ * `verifyUrl(url, timeout?)`: Verify the current URL matches the expected URL.
99
+
100
+ #### Interaction
101
+ * `click(testObject)`: Click on a test object.
102
+ * `doubleClick(testObject)`: Double-click on a test object.
103
+ * `rightClick(testObject)`: Right-click on a test object.
104
+ * `setText(testObject, text)`: Clear and set text in an input field.
105
+ * `sendKeys(testObject, key)`: Send specific keys to an element.
106
+ * `pressKey(key)`: Press a specific key globally (e.g., 'Enter').
107
+ * `check(testObject)`: Check a checkbox or radio button.
108
+ * `uncheck(testObject)`: Uncheck a checkbox.
109
+ * `selectOptionByValue(testObject, value)`: Select a dropdown option by value.
110
+ * `selectOptionByLabel(testObject, label)`: Select a dropdown option by label.
111
+ * `mouseOver(testObject)`: Hover over an element.
112
+ * `dragAndDrop(source, target)`: Drag one element and drop it onto another.
113
+ * `uploadFile(testObject, absolutePath)`: Upload a file to an input element.
114
+ * `scrollToElement(testObject)`: Scroll the page to make the element visible.
115
+ * `search(text)`: Heuristically find a search bar and enter text.
116
+ * `clickImage(imagePath)`: Click an element by matching an image template.
117
+
118
+ #### Verification
119
+ * `verifyElementPresent(testObject, timeout?)`: Assert an element exists.
120
+ * `verifyElementNotPresent(testObject, timeout?)`: Assert an element does not exist.
121
+ * `verifyElementText(testObject, expectedText)`: Assert element text matches.
122
+ * `verifyElementAttributeValue(testObject, attribute, value)`: Assert element attribute matches.
123
+ * `verifyElementChecked(testObject, checked?)`: Assert element checked state.
124
+ * `verifyTextPresent(text)`: Assert text exists on the page.
125
+
126
+ #### Synchonization
127
+ * `waitForElementVisible(testObject, timeout?)`: Wait for element to be visible.
128
+ * `waitForElementNotVisible(testObject, timeout?)`: Wait for element to disappear.
129
+ * `waitForElementClickable(testObject, timeout?)`: Wait for element to be clickable.
130
+ * `waitForAngularLoad()`: Wait for Angular stability (if applicable).
131
+ * `delay(seconds)`: Hard wait (use sparingly).
132
+
133
+ #### Utilities
134
+ * `getText(testObject)`: Get text content of an element.
135
+ * `getAttribute(testObject, attribute)`: Get attribute value of an element.
136
+ * `takeScreenshot(filename?)`: Capture a full-page screenshot.
137
+ * `maximizeWindow()`: Maximize the browser window.
138
+ * `setWindowSize(width, height)`: Set specific window dimensions.
139
+
140
+ ### AIWeb
141
+
142
+ * `executeManualSteps(steps: string)`: Execute a multi-step test case described in natural language.
@@ -3,14 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.expect = exports.test = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const Keyword_1 = require("./Keyword");
6
- const ReporterUtils_1 = require("../reporting/ReporterUtils");
7
6
  exports.test = test_1.test.extend({
8
7
  contextSetup: [async ({ page }, use, testInfo) => {
9
8
  // Initialize the Keyword Context before every test
10
9
  Keyword_1.KeywordContext.page = page;
11
10
  Keyword_1.KeywordContext.testInfo = testInfo;
12
- // Ensure report folder exists automatically
13
- (0, ReporterUtils_1.ensureReportFolder)(testInfo);
14
11
  await use();
15
12
  }, { auto: true }],
16
13
  });
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { Keyword, KeywordContext } from './core/Keyword';
4
4
  export { TestObject, el } from './core/ObjectRepository';
5
5
  export { TestCase } from './core/TestDecorators';
6
6
  export { test, expect } from './core/TestBase';
7
+ export { generateReportPath } from './reporting/ReporterUtils';
7
8
  export { default as CustomReporter } from './reporting/CustomReporter';
8
9
  export { default as HtmlReporter } from './reporting/HtmlReporter';
9
10
  export { default as EmailReporter } from './reporting/EmailReporter';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.EmailReporter = exports.HtmlReporter = exports.CustomReporter = exports.expect = exports.test = exports.TestCase = exports.el = exports.TestObject = exports.KeywordContext = exports.Keyword = exports.AIWeb = exports.Web = void 0;
6
+ exports.EmailReporter = exports.HtmlReporter = exports.CustomReporter = exports.generateReportPath = exports.expect = exports.test = exports.TestCase = exports.el = exports.TestObject = exports.KeywordContext = exports.Keyword = exports.AIWeb = exports.Web = void 0;
7
7
  // Core
8
8
  var WebUI_1 = require("./keywords/WebUI");
9
9
  Object.defineProperty(exports, "Web", { enumerable: true, get: function () { return WebUI_1.Web; } });
@@ -20,6 +20,8 @@ Object.defineProperty(exports, "TestCase", { enumerable: true, get: function ()
20
20
  var TestBase_1 = require("./core/TestBase");
21
21
  Object.defineProperty(exports, "test", { enumerable: true, get: function () { return TestBase_1.test; } });
22
22
  Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return TestBase_1.expect; } });
23
+ var ReporterUtils_1 = require("./reporting/ReporterUtils");
24
+ Object.defineProperty(exports, "generateReportPath", { enumerable: true, get: function () { return ReporterUtils_1.generateReportPath; } });
23
25
  // Reporters
24
26
  var CustomReporter_1 = require("./reporting/CustomReporter");
25
27
  Object.defineProperty(exports, "CustomReporter", { enumerable: true, get: function () { return __importDefault(CustomReporter_1).default; } });
@@ -6,6 +6,7 @@ declare class HtmlReporter implements Reporter {
6
6
  onEnd(result: FullResult): Promise<void>;
7
7
  private testResults;
8
8
  onTestEnd(test: TestCase, result: TestResult): void;
9
+ private ansiToHtml;
9
10
  private generateHtml;
10
11
  }
11
12
  export default HtmlReporter;
@@ -96,92 +96,328 @@ class HtmlReporter {
96
96
  const screenshotRelativePath = copyAttachment('screenshot', a => (a.name === 'screenshot' || a.contentType.startsWith('image/')) && !!a.path);
97
97
  this.testResults.push({ test, result, suiteName, videoPath: videoRelativePath, screenshotPath: screenshotRelativePath });
98
98
  }
99
+ ansiToHtml(text) {
100
+ if (!text)
101
+ return '';
102
+ let html = text
103
+ .replace(/&/g, '&amp;')
104
+ .replace(/</g, '&lt;')
105
+ .replace(/>/g, '&gt;');
106
+ // Simple mapping for common Playwright ANSI codes
107
+ // Styles
108
+ html = html.replace(/\u001b\[1m/g, '<span style="font-weight:bold">')
109
+ .replace(/\u001b\[2m/g, '<span style="opacity:0.6">')
110
+ .replace(/\u001b\[3m/g, '<span style="font-style:italic">')
111
+ .replace(/\u001b\[4m/g, '<span style="text-decoration:underline">')
112
+ .replace(/\u001b\[22m/g, '</span>') // Reset bold/dim
113
+ .replace(/\u001b\[23m/g, '</span>') // Reset italic
114
+ .replace(/\u001b\[24m/g, '</span>'); // Reset underline
115
+ // Colors (Foreground)
116
+ html = html.replace(/\u001b\[30m/g, '<span style="color:#555">')
117
+ .replace(/\u001b\[31m/g, '<span style="color:#ff6b6b">') // Red
118
+ .replace(/\u001b\[32m/g, '<span style="color:#69db7c">') // Green
119
+ .replace(/\u001b\[33m/g, '<span style="color:#ffd43b">') // Yellow
120
+ .replace(/\u001b\[34m/g, '<span style="color:#4dabf7">') // Blue
121
+ .replace(/\u001b\[35m/g, '<span style="color:#da77f2">') // Magenta
122
+ .replace(/\u001b\[36m/g, '<span style="color:#9775fa">') // Cyan
123
+ .replace(/\u001b\[37m/g, '<span style="color:#ccc">') // White
124
+ .replace(/\u001b\[39m/g, '</span>'); // Default color
125
+ // Reset all
126
+ html = html.replace(/\u001b\[0m/g, '</span></span></span></span>');
127
+ return html;
128
+ }
99
129
  generateHtml() {
100
- const rows = this.testResults.map((item, index) => {
101
- const statusColor = item.result.status === 'passed' ? '#dff0d8' : item.result.status === 'failed' ? '#f2dede' : '#fcf8e3';
102
- const statusText = item.result.status.toUpperCase();
103
- // Steps processing
104
- const stepsHtml = item.result.steps.map(step => {
105
- return `<div class="step">
106
- <span class="step-title">${step.title}</span>
107
- <span class="step-duration">${step.duration}ms</span>
108
- ${step.error ? `<div class="error">${step.error.message}</div>` : ''}
109
- </div>`;
110
- }).join('');
111
- const screenshotHtml = item.screenshotPath ? `
112
- <div class="screenshot-container" style="margin-top: 15px;">
113
- <h3>Screenshot</h3>
114
- <img src="${item.screenshotPath}" alt="Failure Screenshot" style="max-width: 100%; border: 1px solid #ddd;">
115
- </div>
116
- ` : '';
117
- const videoHtml = item.videoPath ? `
118
- <div class="video-container" style="margin-top: 15px;">
119
- <h3>Video Recording</h3>
120
- <video width="640" height="480" controls>
121
- <source src="${item.videoPath}" type="video/webm">
122
- Your browser does not support the video tag.
123
- </video>
124
- </div>
125
- ` : '';
126
- return `
127
- <tr class="test-row" onclick="toggleDetails(${index})" style="background-color: ${statusColor}">
128
- <td>${item.suiteName}</td>
129
- <td>${item.test.title}</td>
130
- <td>${statusText}</td>
131
- <td>${item.result.duration}ms</td>
132
- </tr>
133
- <tr id="details-${index}" style="display:none">
134
- <td colspan="4">
135
- <div class="details-container">
136
- ${stepsHtml}
137
- ${screenshotHtml}
138
- ${videoHtml}
139
- </div>
140
- </td>
141
- </tr>
142
- `;
143
- }).join('');
130
+ const totalTests = this.testResults.length;
131
+ const passedTests = this.testResults.filter(r => r.result.status === 'passed').length;
132
+ const failedTests = this.testResults.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length;
133
+ const skippedTests = totalTests - passedTests - failedTests;
134
+ const resultsData = JSON.stringify(this.testResults.map((item, index) => ({
135
+ id: index,
136
+ suite: item.suiteName,
137
+ title: item.test.title,
138
+ status: item.result.status,
139
+ duration: item.result.duration,
140
+ steps: item.result.steps.map(step => ({
141
+ title: step.title,
142
+ duration: step.duration,
143
+ error: step.error ? this.ansiToHtml(step.error.stack || step.error.message || '') : undefined
144
+ })),
145
+ error: item.result.error ? this.ansiToHtml(item.result.error.stack || item.result.error.message || '') : undefined,
146
+ screenshot: item.screenshotPath,
147
+ video: item.videoPath
148
+ })));
144
149
  return `
145
150
  <!DOCTYPE html>
146
- <html>
151
+ <html lang="en">
147
152
  <head>
148
- <title>Test Execution Report</title>
153
+ <meta charset="UTF-8">
154
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
155
+ <title>Test Result</title>
149
156
  <style>
150
- body { font-family: Arial, sans-serif; margin: 20px; }
151
- h1 { color: #333; }
152
- table { width: 100%; border-collapse: collapse; margin-top: 20px; }
153
- th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
154
- th { background-color: #f2f2f2; }
155
- .test-row { cursor: pointer; }
156
- .step { margin: 5px 0; padding: 5px; border-left: 3px solid #007bff; background: #f9f9f9; }
157
- .step-title { font-weight: bold; }
158
- .step-duration { float: right; color: #666; font-size: 0.9em; }
159
- .error { color: red; margin-top: 5px; white-space: pre-wrap; }
160
- .details-container { padding: 10px; background: #fff; border: 1px solid #ddd; }
157
+ :root {
158
+ --bg-color: #1e1e1e;
159
+ --sidebar-bg: #252526;
160
+ --text-color: #d4d4d4;
161
+ --border-color: #3e3e42;
162
+ --hover-color: #2a2d2e;
163
+ --success-color: #4caf50;
164
+ --fail-color: #f44336;
165
+ --skip-color: #ff9800;
166
+ --blue-accent: #007acc;
167
+ }
168
+ body {
169
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
170
+ margin: 0;
171
+ padding: 0;
172
+ background-color: var(--bg-color);
173
+ color: var(--text-color);
174
+ display: flex;
175
+ height: 100vh;
176
+ overflow: hidden;
177
+ }
178
+ /* Sidebar styles */
179
+ .sidebar {
180
+ width: 350px;
181
+ background-color: var(--sidebar-bg);
182
+ border-right: 1px solid var(--border-color);
183
+ display: flex;
184
+ flex-direction: column;
185
+ }
186
+ .sidebar-header {
187
+ padding: 15px;
188
+ border-bottom: 1px solid var(--border-color);
189
+ }
190
+ .stats-container {
191
+ display: flex;
192
+ gap: 10px;
193
+ margin-bottom: 10px;
194
+ font-size: 0.9em;
195
+ }
196
+ .stat-item {
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 5px;
200
+ }
201
+ .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
202
+ .passed-dot { background-color: var(--success-color); }
203
+ .failed-dot { background-color: var(--fail-color); }
204
+ .skipped-dot { background-color: var(--skip-color); }
205
+
206
+ .search-box {
207
+ width: 100%;
208
+ padding: 8px;
209
+ background-color: #3c3c3c;
210
+ border: 1px solid var(--border-color);
211
+ color: white;
212
+ border-radius: 4px;
213
+ box-sizing: border-box;
214
+ }
215
+
216
+ .test-list {
217
+ overflow-y: auto;
218
+ flex: 1;
219
+ }
220
+ .test-item {
221
+ padding: 10px 15px;
222
+ border-bottom: 1px solid var(--border-color);
223
+ cursor: pointer;
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 10px;
227
+ }
228
+ .test-item:hover, .test-item.active { background-color: var(--hover-color); }
229
+ .test-status-icon {
230
+ font-size: 1.2em;
231
+ width: 20px;
232
+ text-align: center;
233
+ }
234
+ .test-info { flex: 1; overflow: hidden; }
235
+ .test-title { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
236
+ .test-suite { font-size: 0.8em; color: #888; }
237
+ .test-duration { font-size: 0.8em; color: #888; }
238
+
239
+ /* Main Content */
240
+ .main-content {
241
+ flex: 1;
242
+ padding: 20px;
243
+ overflow-y: auto;
244
+ display: none; /* Hidden by default until selected */
245
+ }
246
+ .main-content.active { display: block; }
247
+
248
+ .detail-header {
249
+ border-bottom: 1px solid var(--border-color);
250
+ padding-bottom: 15px;
251
+ margin-bottom: 20px;
252
+ }
253
+ .detail-title { font-size: 1.5em; margin-bottom: 5px; }
254
+ .detail-meta { color: #888; font-size: 0.9em; display: flex; gap: 20px; }
255
+
256
+ .section-title {
257
+ font-size: 1.1em;
258
+ margin: 20px 0 10px 0;
259
+ color: #bbb;
260
+ border-bottom: 1px solid #333;
261
+ padding-bottom: 5px;
262
+ }
263
+
264
+ /* Steps */
265
+ .steps-container {
266
+ background-color: #252526;
267
+ border-radius: 5px;
268
+ padding: 10px;
269
+ }
270
+ .step-row {
271
+ padding: 8px;
272
+ border-left: 2px solid #555;
273
+ margin-bottom: 5px;
274
+ background-color: #2d2d2d;
275
+ }
276
+ .step-row.passed { border-left-color: var(--success-color); }
277
+ .step-row.failed { border-left-color: var(--fail-color); }
278
+ .step-header { display: flex; justify-content: space-between; }
279
+ .step-error {
280
+ margin-top: 5px;
281
+ padding: 10px;
282
+ background-color: #0d1117;
283
+ color: #c9d1d9;
284
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
285
+ font-size: 0.85em;
286
+ border: 1px solid #30363d;
287
+ border-radius: 6px;
288
+ white-space: pre-wrap;
289
+ overflow-x: auto;
290
+ line-height: 1.45;
291
+ }
292
+
293
+ /* Media */
294
+ .media-container {
295
+ display: flex;
296
+ flex-wrap: wrap;
297
+ gap: 20px;
298
+ }
299
+ .media-item {
300
+ background-color: #000;
301
+ padding: 5px;
302
+ border-radius: 4px;
303
+ border: 1px solid var(--border-color);
304
+ }
305
+ img, video { max-width: 100%; display: block; }
306
+
307
+ .empty-state {
308
+ display: flex;
309
+ justify-content: center;
310
+ align-items: center;
311
+ height: 100%;
312
+ color: #666;
313
+ font-size: 1.2em;
314
+ }
161
315
  </style>
316
+ </head>
317
+ <body>
318
+ <div class="sidebar">
319
+ <div class="sidebar-header">
320
+ <div class="stats-container">
321
+ <div class="stat-item"><span class="dot passed-dot"></span>Passed: ${passedTests}</div>
322
+ <div class="stat-item"><span class="dot failed-dot"></span>Failed: ${failedTests}</div>
323
+ <div class="stat-item"><span class="dot skipped-dot"></span>Skipped: ${skippedTests}</div>
324
+ </div>
325
+ <input type="text" class="search-box" placeholder="Filter tests..." id="searchInput" onkeyup="filterTests()">
326
+ </div>
327
+ <div class="test-list" id="testList"></div>
328
+ </div>
329
+
330
+ <div id="contentArea" class="main-content">
331
+ <div class="empty-state">Select a test view details</div>
332
+ </div>
333
+
162
334
  <script>
163
- function toggleDetails(id) {
164
- var row = document.getElementById('details-' + id);
165
- row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
335
+ const data = ${resultsData};
336
+ let currentId = null;
337
+
338
+ function renderList(items) {
339
+ const container = document.getElementById('testList');
340
+ container.innerHTML = items.map(item => {
341
+ const icon = item.status === 'passed' ? '✅' : item.status === 'failed' ? '❌' : '⚠️';
342
+ const activeClass = currentId === item.id ? 'active' : '';
343
+ return \`<div class="test-item \${activeClass}" onclick="selectTest(\${item.id})">
344
+ <div class="test-status-icon">\${icon}</div>
345
+ <div class="test-info">
346
+ <div class="test-title" title="\${item.title}">\${item.title}</div>
347
+ <div class="test-suite">\${item.suite}</div>
348
+ </div>
349
+ <div class="test-duration">\${item.duration}ms</div>
350
+ </div>\`;
351
+ }).join('');
166
352
  }
353
+
354
+ function selectTest(id) {
355
+ currentId = id;
356
+ renderList(data); // Re-render to update active state
357
+ const test = data.find(t => t.id === id);
358
+ if (!test) return;
359
+
360
+ const content = document.getElementById('contentArea');
361
+ const statusColor = test.status === 'passed' ? '#4caf50' : '#f44336';
362
+
363
+ const stepsHtml = test.steps.map(step => \`
364
+ <div class="step-row \${step.error ? 'failed' : 'passed'}">
365
+ <div class="step-header">
366
+ <span>\${step.title}</span>
367
+ <span style="color: #888">\${step.duration}ms</span>
368
+ </div>
369
+ \${step.error ? \`<div class="step-error">\${step.error}</div>\` : ''}
370
+ </div>
371
+ \`).join('');
372
+
373
+ const mediaHtml = \`
374
+ \${test.screenshot ? \`
375
+ <div class="media-item">
376
+ <div style="margin-bottom:5px; color:#aaa">Screenshot</div>
377
+ <img src="\${test.screenshot}" height="200" onclick="window.open(this.src)">
378
+ </div>\` : ''}
379
+ \${test.video ? \`
380
+ <div class="media-item">
381
+ <div style="margin-bottom:5px; color:#aaa">Video</div>
382
+ <video src="\${test.video}" height="200" controls></video>
383
+ </div>\` : ''}
384
+ \`;
385
+
386
+ content.innerHTML = \`
387
+ <div class="detail-header">
388
+ <div style="display:flex; align-items:center; gap:10px;">
389
+ <span style="font-size:1.5em; color:\${statusColor}">\${test.status.toUpperCase()}</span>
390
+ <div class="detail-title">\${test.title}</div>
391
+ </div>
392
+ <div class="detail-meta">
393
+ <span>Suite: \${test.suite}</span>
394
+ <span>Duration: \${test.duration}ms</span>
395
+ </div>
396
+ \${test.error ? \`<div class="step-error" style="margin-top:10px">\${test.error}</div>\` : ''}
397
+ </div>
398
+
399
+ <div class="section-title">Execution Steps</div>
400
+ <div class="steps-container">
401
+ \${stepsHtml}
402
+ </div>
403
+
404
+ \${test.screenshot || test.video ? \`<div class="section-title">Artifacts</div><div class="media-container">\${mediaHtml}</div>\` : ''}
405
+ \`;
406
+ content.classList.add('active');
407
+ }
408
+
409
+ function filterTests() {
410
+ const query = document.getElementById('searchInput').value.toLowerCase();
411
+ const filtered = data.filter(item =>
412
+ item.title.toLowerCase().includes(query) ||
413
+ item.suite.toLowerCase().includes(query)
414
+ );
415
+ renderList(filtered);
416
+ }
417
+
418
+ // Initial render
419
+ renderList(data);
167
420
  </script>
168
- </head>
169
- <body>
170
- <h1>Execution Report</h1>
171
- <p>Generated: ${new Date().toLocaleString()}</p>
172
- <table>
173
- <thead>
174
- <tr>
175
- <th>Test Suite</th>
176
- <th>Test Case</th>
177
- <th>Status</th>
178
- <th>Duration</th>
179
- </tr>
180
- </thead>
181
- <tbody>
182
- ${rows}
183
- </tbody>
184
- </table>
185
421
  </body>
186
422
  </html>
187
423
  `;
@@ -1,5 +1,4 @@
1
1
  import { Suite } from '@playwright/test/reporter';
2
+ export declare function generateReportPath(baseDir?: string, prefix?: string): string;
2
3
  export declare function getReportFolder(suite: Suite): string;
3
- import { TestInfo } from '@playwright/test';
4
- export declare function ensureReportFolder(testInfo: TestInfo): string;
5
4
  export declare function resetReportFolderCache(): void;
@@ -33,12 +33,16 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.generateReportPath = generateReportPath;
36
37
  exports.getReportFolder = getReportFolder;
37
- exports.ensureReportFolder = ensureReportFolder;
38
38
  exports.resetReportFolderCache = resetReportFolderCache;
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  let cachedReportFolder = null;
42
+ function generateReportPath(baseDir = 'reports', prefix = 'test') {
43
+ const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '').split('.')[0];
44
+ return path.join(process.cwd(), baseDir, `${prefix}_${timestamp}`);
45
+ }
42
46
  function getReportFolder(suite) {
43
47
  if (cachedReportFolder) {
44
48
  return cachedReportFolder;
@@ -65,42 +69,6 @@ function getReportFolder(suite) {
65
69
  console.log(`[ReporterUtils] Calculated report folder: ${cachedReportFolder}`);
66
70
  return cachedReportFolder;
67
71
  }
68
- // ... existing code ...
69
- function ensureReportFolder(testInfo) {
70
- if (cachedReportFolder) {
71
- return cachedReportFolder;
72
- }
73
- const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '').split('.')[0];
74
- // Extract suite name from test info title path (usually file name without ext)
75
- // testInfo.titlePath[0] is usually the file name relative to test dir, e.g. "suites/SauceDemoAI.spec.ts"
76
- let suiteName = 'TestRun';
77
- if (testInfo.titlePath.length > 0) {
78
- const fileName = path.basename(testInfo.file || '');
79
- if (fileName) {
80
- suiteName = fileName.split('.')[0];
81
- }
82
- }
83
- // Check if our reporters are already configured.
84
- // If they are, they will handle folder creation (in the main process), so we shouldn't create a duplicate one here (in the worker process)
85
- // which would likely differ in timestamp.
86
- const reporters = testInfo.config.reporter;
87
- const hasFlashReporter = reporters.some(r => {
88
- const name = Array.isArray(r) ? r[0] : r;
89
- if (typeof name === 'string') {
90
- return name.includes('CustomReporter') || name.includes('HtmlReporter') || name.includes('EmailReporter');
91
- }
92
- return false;
93
- });
94
- if (hasFlashReporter) {
95
- return '';
96
- }
97
- cachedReportFolder = path.join(process.cwd(), 'reports', suiteName, `test_${timestamp}`);
98
- if (!fs.existsSync(cachedReportFolder)) {
99
- fs.mkdirSync(cachedReportFolder, { recursive: true });
100
- console.log(`[ReporterUtils] AUTO-CREATED report folder: ${cachedReportFolder}`);
101
- }
102
- return cachedReportFolder;
103
- }
104
72
  function resetReportFolderCache() {
105
73
  cachedReportFolder = null;
106
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flash-ai-team/flash-test-framework",
3
- "version": "0.0.6",
3
+ "version": "0.0.9",
4
4
  "description": "A powerful keyword-driven automation framework built on top of Playwright and TypeScript.",
5
5
  "keywords": [
6
6
  "playwright",