@flash-ai-team/flash-test-framework 0.0.7 → 0.0.10

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
@@ -2,22 +2,53 @@
2
2
 
3
3
  A powerful, keyword-driven automation framework built on top of Playwright and TypeScript, featuring AI-powered capabilities.
4
4
 
5
- ## Installation
5
+ ## Quick Start
6
+
7
+ The easiest way to start a new project is using our CLI.
8
+
9
+ ### Global Installation
10
+
11
+ Alternatively, you can install the CLI globally to run `flash-test` commands directly.
6
12
 
7
13
  ```bash
8
- npm install flash-test-framework
14
+ npm install -g @flash-ai-team/flash-test-framework
9
15
  ```
10
16
 
11
- ## Setup
17
+ Then you can use:
18
+
19
+ ```bash
20
+ flash-test init <project-name>
21
+ flash-test --version
22
+ ```
12
23
 
13
- 1. **Initialize Playwright** (if not already done):
24
+ 1. **Initialize a new project**:
25
+ Make a new folder and run:
14
26
  ```bash
15
- npx playwright install
27
+ flash-test init <project-name>
16
28
  ```
29
+ This will create:
30
+ * `package.json` with dependencies.
31
+ * `playwright.config.ts` configured for the framework.
32
+ * `tsconfig.json`.
33
+ * `tests/cases/ExampleTests.ts` and `tests/suites/Example.spec.ts`.
17
34
 
18
- 2. **Configuration**:
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`.
35
+ 2. **Install dependencies**:
36
+ ```bash
37
+ npm install
38
+ ```
39
+
40
+ 4. **Run the test**:
41
+ ```bash
42
+ npx playwright test
43
+ ```
44
+
45
+ ## Manual Setup (Optional)
46
+
47
+ If you prefer to set up manually:
48
+ 1. `npm install @flash-ai-team/flash-test-framework`
49
+ 2. Create `playwright.config.ts` and `tsconfig.json`.
50
+ 3. **Email Reporting**: Create `email.config.json`.
51
+ 4. **AI Features**: Create `ai.config.json`:
21
52
  ```json
22
53
  {
23
54
  "enabled": true,
@@ -29,56 +60,47 @@ npm install flash-test-framework
29
60
 
30
61
  ## Usage
31
62
 
32
- ### 1. Basic Web Automation (`web`)
63
+ ### 1. Basic Web Automation (`Web`)
33
64
 
34
- Use `web` for standard, deterministic interactions using selectors.
65
+ Use `Web` for standard, deterministic interactions using selectors.
66
+ Import `test` from the framework to automatically initialize the context.
35
67
 
36
68
  ```typescript
37
- import { test } from '@playwright/test';
38
- import { web, findTestObject, KeywordContext } from 'flash-test-framework';
69
+ import { test, Web, el, KeywordContext } from '@flash-ai-team/flash-test-framework';
39
70
 
40
71
  test.describe('My Test Suite', () => {
41
- test.beforeEach(async ({ page }, testInfo) => {
42
- // Initialize Context
43
- KeywordContext.page = page;
44
- KeywordContext.testInfo = testInfo;
45
- });
72
+ // Context is automatically initialized by the 'test' fixture
46
73
 
47
74
  test('Login Test', async () => {
48
- await web.navigateToUrl('https://example.com/login');
75
+ await Web.navigateToUrl('https://example.com/login');
49
76
 
50
- const usernameInput = findTestObject('#username', 'Username Field');
51
- const passwordInput = findTestObject('#password', 'Password Field');
52
- const loginButton = findTestObject('button[type="submit"]', 'Login Button');
77
+ const usernameInput = el('#username', 'Username Field');
78
+ const passwordInput = el('#password', 'Password Field');
79
+ const loginButton = el('button[type="submit"]', 'Login Button');
53
80
 
54
- await web.setText(usernameInput, 'myuser');
55
- await web.setText(passwordInput, 'mypassword');
56
- await web.click(loginButton);
81
+ await Web.setText(usernameInput, 'myuser');
82
+ await Web.setText(passwordInput, 'mypassword');
83
+ await Web.click(loginButton);
57
84
 
58
85
  // precise search using heuristics
59
- await web.search("Specific Item");
86
+ await Web.search("Specific Item");
60
87
  });
61
88
  });
62
89
  ```
63
90
 
64
- ### 2. AI & Manual Steps (`aiWeb`)
91
+ ### 2. AI & Manual Steps (`AIWeb`)
65
92
 
66
- Use `aiWeb` to write tests in plain English or to click elements that are hard to target (like map markers) using images.
93
+ Use `AIWeb` to write tests in plain English or to click elements that are hard to target (like map markers) using images.
67
94
 
68
95
  **Example: `AIManualSteps.spec.ts`**
69
96
 
70
97
  ```typescript
71
- import { test } from '@playwright/test';
72
- import { aiWeb, KeywordContext } from 'flash-test-framework';
98
+ import { test, AIWeb } from '@flash-ai-team/flash-test-framework';
73
99
 
74
100
  test.describe('AI Scenario', () => {
75
- test.beforeEach(async ({ page }, testInfo) => {
76
- KeywordContext.page = page;
77
- KeywordContext.testInfo = testInfo;
78
- });
79
101
 
80
102
  test('Google Maps Flow', async () => {
81
- await aiWeb.executeManualSteps(`
103
+ await AIWeb.executeManualSteps(`
82
104
  1. Navigate to "https://www.google.com/maps"
83
105
  2. Click on "Accept all"
84
106
  3. Search "KFC"
@@ -96,15 +118,56 @@ test.describe('AI Scenario', () => {
96
118
 
97
119
  ## Keywords
98
120
 
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.
121
+ ### Web
122
+
123
+ #### Navigation
124
+ * `navigateToUrl(url)`: Navigate to the specified URL.
125
+ * `refresh()`: Refresh the current page.
126
+ * `back()`: Navigate back.
127
+ * `forward()`: Navigate forward.
128
+ * `closeBrowser()`: Close the browser instance.
129
+ * `verifyUrl(url, timeout?)`: Verify the current URL matches the expected URL.
130
+
131
+ #### Interaction
132
+ * `click(testObject)`: Click on a test object.
133
+ * `doubleClick(testObject)`: Double-click on a test object.
134
+ * `rightClick(testObject)`: Right-click on a test object.
135
+ * `setText(testObject, text)`: Clear and set text in an input field.
136
+ * `sendKeys(testObject, key)`: Send specific keys to an element.
137
+ * `pressKey(key)`: Press a specific key globally (e.g., 'Enter').
138
+ * `check(testObject)`: Check a checkbox or radio button.
139
+ * `uncheck(testObject)`: Uncheck a checkbox.
140
+ * `selectOptionByValue(testObject, value)`: Select a dropdown option by value.
141
+ * `selectOptionByLabel(testObject, label)`: Select a dropdown option by label.
142
+ * `mouseOver(testObject)`: Hover over an element.
143
+ * `dragAndDrop(source, target)`: Drag one element and drop it onto another.
144
+ * `uploadFile(testObject, absolutePath)`: Upload a file to an input element.
145
+ * `scrollToElement(testObject)`: Scroll the page to make the element visible.
146
+ * `search(text)`: Heuristically find a search bar and enter text.
147
+ * `clickImage(imagePath)`: Click an element by matching an image template.
148
+
149
+ #### Verification
150
+ * `verifyElementPresent(testObject, timeout?)`: Assert an element exists.
151
+ * `verifyElementNotPresent(testObject, timeout?)`: Assert an element does not exist.
152
+ * `verifyElementText(testObject, expectedText)`: Assert element text matches.
153
+ * `verifyElementAttributeValue(testObject, attribute, value)`: Assert element attribute matches.
154
+ * `verifyElementChecked(testObject, checked?)`: Assert element checked state.
155
+ * `verifyTextPresent(text)`: Assert text exists on the page.
156
+
157
+ #### Synchonization
158
+ * `waitForElementVisible(testObject, timeout?)`: Wait for element to be visible.
159
+ * `waitForElementNotVisible(testObject, timeout?)`: Wait for element to disappear.
160
+ * `waitForElementClickable(testObject, timeout?)`: Wait for element to be clickable.
161
+ * `waitForAngularLoad()`: Wait for Angular stability (if applicable).
162
+ * `delay(seconds)`: Hard wait (use sparingly).
163
+
164
+ #### Utilities
165
+ * `getText(testObject)`: Get text content of an element.
166
+ * `getAttribute(testObject, attribute)`: Get attribute value of an element.
167
+ * `takeScreenshot(filename?)`: Capture a full-page screenshot.
168
+ * `maximizeWindow()`: Maximize the browser window.
169
+ * `setWindowSize(width, height)`: Set specific window dimensions.
170
+
171
+ ### AIWeb
172
+
173
+ * `executeManualSteps(steps: string)`: Execute a multi-step test case described in natural language.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const args = process.argv.slice(2);
10
+ const command = args[0];
11
+ if (command === 'init') {
12
+ const projectName = args[1];
13
+ initProject(projectName);
14
+ }
15
+ else if (command === '--version' || command === '-v') {
16
+ printVersion();
17
+ }
18
+ else {
19
+ console.log('Usage: flash-test init [project-name]');
20
+ console.log(' flash-test -v / --version');
21
+ process.exit(1);
22
+ }
23
+ function printVersion() {
24
+ try {
25
+ const packageJsonPath = path_1.default.join(__dirname, '..', '..', 'package.json');
26
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
27
+ console.log(`v${packageJson.version}`);
28
+ }
29
+ catch (error) {
30
+ console.error('Error reading version:', error);
31
+ process.exit(1);
32
+ }
33
+ }
34
+ function initProject(projectName) {
35
+ console.log('Initializing Flash Test Project...');
36
+ let projectDir = process.cwd();
37
+ let name = "my-flash-test-project";
38
+ if (projectName) {
39
+ projectDir = path_1.default.join(process.cwd(), projectName);
40
+ name = projectName;
41
+ if (!fs_1.default.existsSync(projectDir)) {
42
+ fs_1.default.mkdirSync(projectDir);
43
+ console.log(`Created directory: ${projectName}`);
44
+ }
45
+ }
46
+ // 1. Create Files
47
+ createFile(path_1.default.join(projectDir, 'package.json'), JSON.stringify({
48
+ "name": name,
49
+ "version": "1.0.0",
50
+ "scripts": {
51
+ "test": "playwright test"
52
+ },
53
+ "devDependencies": {
54
+ "@flash-ai-team/flash-test-framework": "latest",
55
+ "@playwright/test": "^1.57.0",
56
+ "typescript": "^5.9.3"
57
+ }
58
+ }, null, 2));
59
+ createFile(path_1.default.join(projectDir, 'playwright.config.ts'), `
60
+ import { defineConfig, devices } from '@playwright/test';
61
+
62
+ export default defineConfig({
63
+ testDir: './tests',
64
+ fullyParallel: true,
65
+ forbidOnly: !!process.env.CI,
66
+ retries: process.env.CI ? 2 : 0,
67
+ workers: process.env.CI ? 1 : undefined,
68
+ reporter: [
69
+ ['html', { open: 'never' }],
70
+ ['list'], // Keep 'list' for console output
71
+ [require.resolve('@flash-ai-team/flash-test-framework/dist/reporting/CustomReporter.js')],
72
+ [require.resolve('@flash-ai-team/flash-test-framework/dist/reporting/HtmlReporter.js')]
73
+ ],
74
+ use: {
75
+ trace: 'on-first-retry',
76
+ },
77
+ projects: [
78
+ {
79
+ name: 'chrome',
80
+ use: { ...devices['Desktop Chrome'] },
81
+ },
82
+ ],
83
+ });
84
+ `);
85
+ createFile(path_1.default.join(projectDir, 'tsconfig.json'), JSON.stringify({
86
+ "compilerOptions": {
87
+ "target": "es2016",
88
+ "module": "commonjs",
89
+ "esModuleInterop": true,
90
+ "forceConsistentCasingInFileNames": true,
91
+ "strict": true,
92
+ "skipLibCheck": true
93
+ }
94
+ }, null, 2));
95
+ createFile(path_1.default.join(projectDir, '.gitignore'), `
96
+ node_modules/
97
+ test-results/
98
+ playwright-report/
99
+ dist/
100
+ `);
101
+ const testsDir = path_1.default.join(projectDir, 'tests');
102
+ const casesDir = path_1.default.join(testsDir, 'cases');
103
+ const suitesDir = path_1.default.join(testsDir, 'suites');
104
+ if (!fs_1.default.existsSync(testsDir))
105
+ fs_1.default.mkdirSync(testsDir);
106
+ if (!fs_1.default.existsSync(casesDir))
107
+ fs_1.default.mkdirSync(casesDir);
108
+ if (!fs_1.default.existsSync(suitesDir))
109
+ fs_1.default.mkdirSync(suitesDir);
110
+ createFile(path_1.default.join(casesDir, 'ExampleTests.ts'), `
111
+ import { Web, el, TestCase } from '@flash-ai-team/flash-test-framework';
112
+
113
+ export class ExampleTests {
114
+
115
+ @TestCase
116
+ static async BasicVerify() {
117
+ await Web.navigateToUrl('https://example.com');
118
+ await Web.verifyTextPresent('Example Domain');
119
+
120
+ const header = el('h1', 'Header');
121
+ await Web.verifyElementPresent(header);
122
+ }
123
+ }
124
+ `);
125
+ createFile(path_1.default.join(suitesDir, 'Example.spec.ts'), `
126
+ import { test } from '@flash-ai-team/flash-test-framework';
127
+ import { ExampleTests } from '../cases/ExampleTests';
128
+
129
+ test.describe('Example Suite', () => {
130
+ test('Basic Verification', async () => {
131
+ await ExampleTests.BasicVerify();
132
+ });
133
+ });
134
+ `);
135
+ console.log('Project initialized successfully!');
136
+ if (projectName) {
137
+ console.log(`\nNext steps:\n cd ${projectName}\n npm install`);
138
+ }
139
+ else {
140
+ console.log('Run "npm install" to install dependencies.');
141
+ }
142
+ }
143
+ function createFile(filePath, content) {
144
+ if (fs_1.default.existsSync(filePath)) {
145
+ console.log(`Skipping existing file: ${path_1.default.basename(filePath)}`);
146
+ }
147
+ else {
148
+ fs_1.default.writeFileSync(filePath, content.trim());
149
+ console.log(`Created: ${path_1.default.basename(filePath)}`);
150
+ }
151
+ }
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,3 +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
4
  export declare function resetReportFolderCache(): void;
@@ -33,11 +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
38
  exports.resetReportFolderCache = resetReportFolderCache;
38
39
  const fs = __importStar(require("fs"));
39
40
  const path = __importStar(require("path"));
40
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
+ }
41
46
  function getReportFolder(suite) {
42
47
  if (cachedReportFolder) {
43
48
  return cachedReportFolder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flash-ai-team/flash-test-framework",
3
- "version": "0.0.7",
3
+ "version": "0.0.10",
4
4
  "description": "A powerful keyword-driven automation framework built on top of Playwright and TypeScript.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -24,6 +24,9 @@
24
24
  "type": "commonjs",
25
25
  "main": "dist/index.js",
26
26
  "types": "dist/index.d.ts",
27
+ "bin": {
28
+ "flash-test": "./dist/cli/index.js"
29
+ },
27
30
  "directories": {
28
31
  "test": "tests"
29
32
  },