@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 +109 -46
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +151 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/reporting/HtmlReporter.d.ts +1 -0
- package/dist/reporting/HtmlReporter.js +313 -77
- package/dist/reporting/ReporterUtils.d.ts +1 -0
- package/dist/reporting/ReporterUtils.js +5 -0
- package/package.json +4 -1
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
|
-
##
|
|
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
|
-
|
|
17
|
+
Then you can use:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
flash-test init <project-name>
|
|
21
|
+
flash-test --version
|
|
22
|
+
```
|
|
12
23
|
|
|
13
|
-
1. **Initialize
|
|
24
|
+
1. **Initialize a new project**:
|
|
25
|
+
Make a new folder and run:
|
|
14
26
|
```bash
|
|
15
|
-
|
|
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. **
|
|
19
|
-
|
|
20
|
-
|
|
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 (`
|
|
63
|
+
### 1. Basic Web Automation (`Web`)
|
|
33
64
|
|
|
34
|
-
Use `
|
|
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 '@
|
|
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
|
-
|
|
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
|
|
75
|
+
await Web.navigateToUrl('https://example.com/login');
|
|
49
76
|
|
|
50
|
-
const usernameInput =
|
|
51
|
-
const passwordInput =
|
|
52
|
-
const loginButton =
|
|
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
|
|
55
|
-
await
|
|
56
|
-
await
|
|
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
|
|
86
|
+
await Web.search("Specific Item");
|
|
60
87
|
});
|
|
61
88
|
});
|
|
62
89
|
```
|
|
63
90
|
|
|
64
|
-
### 2. AI & Manual Steps (`
|
|
91
|
+
### 2. AI & Manual Steps (`AIWeb`)
|
|
65
92
|
|
|
66
|
-
Use `
|
|
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 '@
|
|
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
|
|
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
|
-
###
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* `
|
|
103
|
-
* `
|
|
104
|
-
* `
|
|
105
|
-
* `
|
|
106
|
-
* `
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* `
|
|
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,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; } });
|
|
@@ -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, '&')
|
|
104
|
+
.replace(/</g, '<')
|
|
105
|
+
.replace(/>/g, '>');
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
`;
|
|
@@ -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.
|
|
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
|
},
|