@flash-ai-team/flash-test-framework 0.0.9 → 0.0.11
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 +39 -8
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +151 -0
- package/dist/keywords/WebUI.d.ts +6 -0
- package/dist/keywords/WebUI.js +14 -0
- package/dist/reporting/HtmlReporter.d.ts +4 -0
- package/dist/reporting/HtmlReporter.js +425 -120
- 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-ai-team/flash-test-framework
|
|
14
|
+
npm install -g @flash-ai-team/flash-test-framework
|
|
9
15
|
```
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
Then you can use:
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
```bash
|
|
20
|
+
flash-test init <project-name>
|
|
21
|
+
flash-test --version
|
|
22
|
+
```
|
|
23
|
+
|
|
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`.
|
|
34
|
+
|
|
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)
|
|
17
46
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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,
|
|
@@ -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/keywords/WebUI.d.ts
CHANGED
|
@@ -116,6 +116,12 @@ export declare class Web {
|
|
|
116
116
|
* @param timeout - The maximum time to wait in milliseconds (default: 5000).
|
|
117
117
|
*/
|
|
118
118
|
static verifyElementNotPresent(to: TestObject, timeout?: number): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Verifies that the specified text is present on the page.
|
|
121
|
+
* @param text - The text to search for.
|
|
122
|
+
* @param timeout - The maximum time to wait in milliseconds (default: 5000).
|
|
123
|
+
*/
|
|
124
|
+
static verifyTextPresent(text: string, timeout?: number): Promise<void>;
|
|
119
125
|
/**
|
|
120
126
|
* Verifies that the element's text matches the expected text.
|
|
121
127
|
* @param to - The TestObject representing the element.
|
package/dist/keywords/WebUI.js
CHANGED
|
@@ -253,6 +253,14 @@ class Web {
|
|
|
253
253
|
static async verifyElementNotPresent(to, timeout = 5000) {
|
|
254
254
|
await (0, test_1.expect)(this.getLocator(to)).toBeHidden({ timeout });
|
|
255
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Verifies that the specified text is present on the page.
|
|
258
|
+
* @param text - The text to search for.
|
|
259
|
+
* @param timeout - The maximum time to wait in milliseconds (default: 5000).
|
|
260
|
+
*/
|
|
261
|
+
static async verifyTextPresent(text, timeout = 5000) {
|
|
262
|
+
await (0, test_1.expect)(this.page.locator('body')).toContainText(text, { timeout });
|
|
263
|
+
}
|
|
256
264
|
/**
|
|
257
265
|
* Verifies that the element's text matches the expected text.
|
|
258
266
|
* @param to - The TestObject representing the element.
|
|
@@ -506,6 +514,12 @@ __decorate([
|
|
|
506
514
|
__metadata("design:paramtypes", [ObjectRepository_1.TestObject, Number]),
|
|
507
515
|
__metadata("design:returntype", Promise)
|
|
508
516
|
], Web, "verifyElementNotPresent", null);
|
|
517
|
+
__decorate([
|
|
518
|
+
(0, Keyword_1.Keyword)("Verify Text Present"),
|
|
519
|
+
__metadata("design:type", Function),
|
|
520
|
+
__metadata("design:paramtypes", [String, Number]),
|
|
521
|
+
__metadata("design:returntype", Promise)
|
|
522
|
+
], Web, "verifyTextPresent", null);
|
|
509
523
|
__decorate([
|
|
510
524
|
(0, Keyword_1.Keyword)("Verify Element Text"),
|
|
511
525
|
__metadata("design:type", Function),
|
|
@@ -2,11 +2,15 @@ import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@
|
|
|
2
2
|
declare class HtmlReporter implements Reporter {
|
|
3
3
|
private suiteStartTime;
|
|
4
4
|
private reportPath;
|
|
5
|
+
private fileCache;
|
|
5
6
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
6
7
|
onEnd(result: FullResult): Promise<void>;
|
|
7
8
|
private testResults;
|
|
8
9
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
9
10
|
private ansiToHtml;
|
|
11
|
+
private getFileContent;
|
|
12
|
+
private getSnippet;
|
|
13
|
+
private processSteps;
|
|
10
14
|
private generateHtml;
|
|
11
15
|
}
|
|
12
16
|
export default HtmlReporter;
|
|
@@ -40,6 +40,8 @@ class HtmlReporter {
|
|
|
40
40
|
constructor() {
|
|
41
41
|
this.suiteStartTime = 0;
|
|
42
42
|
this.reportPath = '';
|
|
43
|
+
// Cache for file contents to avoid repeated reads
|
|
44
|
+
this.fileCache = new Map();
|
|
43
45
|
// onExit removed as generation is now in onEnd
|
|
44
46
|
this.testResults = [];
|
|
45
47
|
}
|
|
@@ -126,22 +128,80 @@ class HtmlReporter {
|
|
|
126
128
|
html = html.replace(/\u001b\[0m/g, '</span></span></span></span>');
|
|
127
129
|
return html;
|
|
128
130
|
}
|
|
131
|
+
// New method to get file content with caching
|
|
132
|
+
getFileContent(filePath) {
|
|
133
|
+
if (this.fileCache.has(filePath)) {
|
|
134
|
+
return this.fileCache.get(filePath);
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
if (fs.existsSync(filePath)) {
|
|
138
|
+
const content = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
139
|
+
this.fileCache.set(filePath, content);
|
|
140
|
+
return content;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
// Ignore errors reading file
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// New method to extract code snippet
|
|
149
|
+
getSnippet(location) {
|
|
150
|
+
if (!location || !location.file)
|
|
151
|
+
return undefined;
|
|
152
|
+
const lines = this.getFileContent(location.file);
|
|
153
|
+
if (!lines)
|
|
154
|
+
return undefined;
|
|
155
|
+
const line = location.line - 1; // 0-indexed
|
|
156
|
+
if (line < 0 || line >= lines.length)
|
|
157
|
+
return undefined;
|
|
158
|
+
// Get 2 lines before and 2 lines after
|
|
159
|
+
const start = Math.max(0, line - 2);
|
|
160
|
+
const end = Math.min(lines.length, line + 3);
|
|
161
|
+
let snippet = '';
|
|
162
|
+
const maxLineNumWidth = end.toString().length;
|
|
163
|
+
for (let i = start; i < end; i++) {
|
|
164
|
+
const isErrorLine = i === line;
|
|
165
|
+
const lineNum = (i + 1).toString().padStart(maxLineNumWidth, ' ');
|
|
166
|
+
const prefix = isErrorLine ? '>' : ' ';
|
|
167
|
+
snippet += `${prefix} ${lineNum} | ${lines[i]}\n`;
|
|
168
|
+
}
|
|
169
|
+
return snippet;
|
|
170
|
+
}
|
|
171
|
+
processSteps(steps, depth = 0) {
|
|
172
|
+
return steps.map(step => {
|
|
173
|
+
const hasChildren = step.steps && step.steps.length > 0;
|
|
174
|
+
// Only try to get snippet for failed steps that have a location
|
|
175
|
+
let codeSnippet;
|
|
176
|
+
if (step.error && step.location) {
|
|
177
|
+
codeSnippet = this.getSnippet(step.location);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
title: step.title,
|
|
181
|
+
duration: step.duration,
|
|
182
|
+
error: step.error ? this.ansiToHtml(step.error.stack || step.error.message || '') : undefined,
|
|
183
|
+
codeSnippet: codeSnippet, // Add codeSnippet here
|
|
184
|
+
depth: depth,
|
|
185
|
+
steps: hasChildren ? this.processSteps(step.steps, depth + 1) : []
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
129
189
|
generateHtml() {
|
|
130
190
|
const totalTests = this.testResults.length;
|
|
131
191
|
const passedTests = this.testResults.filter(r => r.result.status === 'passed').length;
|
|
132
192
|
const failedTests = this.testResults.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length;
|
|
133
193
|
const skippedTests = totalTests - passedTests - failedTests;
|
|
194
|
+
// Calculate percentages for the pie chart
|
|
195
|
+
const passPct = totalTests ? (passedTests / totalTests) * 360 : 0;
|
|
196
|
+
const failPct = totalTests ? (failedTests / totalTests) * 360 : 0;
|
|
197
|
+
const skipPct = totalTests ? (skippedTests / totalTests) * 360 : 0;
|
|
134
198
|
const resultsData = JSON.stringify(this.testResults.map((item, index) => ({
|
|
135
199
|
id: index,
|
|
136
200
|
suite: item.suiteName,
|
|
137
201
|
title: item.test.title,
|
|
138
202
|
status: item.result.status,
|
|
139
203
|
duration: item.result.duration,
|
|
140
|
-
steps: item.result.steps
|
|
141
|
-
title: step.title,
|
|
142
|
-
duration: step.duration,
|
|
143
|
-
error: step.error ? this.ansiToHtml(step.error.stack || step.error.message || '') : undefined
|
|
144
|
-
})),
|
|
204
|
+
steps: this.processSteps(item.result.steps),
|
|
145
205
|
error: item.result.error ? this.ansiToHtml(item.result.error.stack || item.result.error.message || '') : undefined,
|
|
146
206
|
screenshot: item.screenshotPath,
|
|
147
207
|
video: item.videoPath
|
|
@@ -164,6 +224,7 @@ class HtmlReporter {
|
|
|
164
224
|
--fail-color: #f44336;
|
|
165
225
|
--skip-color: #ff9800;
|
|
166
226
|
--blue-accent: #007acc;
|
|
227
|
+
--code-bg: #0d1117;
|
|
167
228
|
}
|
|
168
229
|
body {
|
|
169
230
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -177,40 +238,99 @@ class HtmlReporter {
|
|
|
177
238
|
}
|
|
178
239
|
/* Sidebar styles */
|
|
179
240
|
.sidebar {
|
|
180
|
-
width:
|
|
241
|
+
width: 400px;
|
|
181
242
|
background-color: var(--sidebar-bg);
|
|
182
243
|
border-right: 1px solid var(--border-color);
|
|
183
244
|
display: flex;
|
|
184
245
|
flex-direction: column;
|
|
246
|
+
flex-shrink: 0;
|
|
185
247
|
}
|
|
186
248
|
.sidebar-header {
|
|
187
|
-
padding:
|
|
249
|
+
padding: 20px;
|
|
188
250
|
border-bottom: 1px solid var(--border-color);
|
|
251
|
+
display: flex;
|
|
252
|
+
flex-direction: column;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 15px;
|
|
189
255
|
}
|
|
190
|
-
|
|
256
|
+
/* Pie Chart */
|
|
257
|
+
.chart-container {
|
|
258
|
+
position: relative;
|
|
259
|
+
width: 120px;
|
|
260
|
+
height: 120px;
|
|
261
|
+
border-radius: 50%;
|
|
262
|
+
background: conic-gradient(
|
|
263
|
+
var(--success-color) 0deg ${passPct}deg,
|
|
264
|
+
var(--fail-color) ${passPct}deg ${passPct + failPct}deg,
|
|
265
|
+
var(--skip-color) ${passPct + failPct}deg 360deg
|
|
266
|
+
);
|
|
191
267
|
display: flex;
|
|
192
|
-
|
|
193
|
-
|
|
268
|
+
align-items: center;
|
|
269
|
+
justify-content: center;
|
|
270
|
+
}
|
|
271
|
+
.chart-inner {
|
|
272
|
+
width: 90px;
|
|
273
|
+
height: 90px;
|
|
274
|
+
background-color: var(--sidebar-bg);
|
|
275
|
+
border-radius: 50%;
|
|
276
|
+
display: flex;
|
|
277
|
+
flex-direction: column;
|
|
278
|
+
align-items: center;
|
|
279
|
+
justify-content: center;
|
|
280
|
+
}
|
|
281
|
+
.total-text { font-size: 1.5em; font-weight: bold; }
|
|
282
|
+
.total-label { font-size: 0.8em; color: #888; }
|
|
283
|
+
|
|
284
|
+
.stats-row {
|
|
285
|
+
display: flex;
|
|
286
|
+
gap: 15px;
|
|
194
287
|
font-size: 0.9em;
|
|
288
|
+
width: 100%;
|
|
289
|
+
justify-content: space-around;
|
|
195
290
|
}
|
|
196
291
|
.stat-item {
|
|
197
292
|
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
198
294
|
align-items: center;
|
|
199
|
-
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
padding: 5px 10px;
|
|
297
|
+
border-radius: 4px;
|
|
298
|
+
transition: background 0.2s;
|
|
200
299
|
}
|
|
201
|
-
.
|
|
202
|
-
.
|
|
203
|
-
.
|
|
204
|
-
.
|
|
300
|
+
.stat-item:hover { background-color: var(--hover-color); }
|
|
301
|
+
.stat-item.active { background-color: #333; box-shadow: 0 0 0 1px var(--border-color); }
|
|
302
|
+
.stat-value { font-weight: bold; font-size: 1.1em; }
|
|
303
|
+
.stat-label { font-size: 0.8em; color: #aaa; }
|
|
205
304
|
|
|
305
|
+
.filter-group {
|
|
306
|
+
display: flex;
|
|
307
|
+
gap: 5px;
|
|
308
|
+
width: 100%;
|
|
309
|
+
}
|
|
310
|
+
.filter-btn {
|
|
311
|
+
flex: 1;
|
|
312
|
+
background: #333;
|
|
313
|
+
border: 1px solid var(--border-color);
|
|
314
|
+
color: #ccc;
|
|
315
|
+
padding: 5px;
|
|
316
|
+
cursor: pointer;
|
|
317
|
+
font-size: 0.85em;
|
|
318
|
+
}
|
|
319
|
+
.filter-btn.active {
|
|
320
|
+
background-color: var(--blue-accent);
|
|
321
|
+
color: white;
|
|
322
|
+
border-color: var(--blue-accent);
|
|
323
|
+
}
|
|
324
|
+
|
|
206
325
|
.search-box {
|
|
207
326
|
width: 100%;
|
|
208
|
-
padding:
|
|
209
|
-
background-color: #
|
|
327
|
+
padding: 10px;
|
|
328
|
+
background-color: #1e1e1e;
|
|
210
329
|
border: 1px solid var(--border-color);
|
|
211
330
|
color: white;
|
|
212
331
|
border-radius: 4px;
|
|
213
332
|
box-sizing: border-box;
|
|
333
|
+
margin-top: 10px;
|
|
214
334
|
}
|
|
215
335
|
|
|
216
336
|
.test-list {
|
|
@@ -218,130 +338,270 @@ class HtmlReporter {
|
|
|
218
338
|
flex: 1;
|
|
219
339
|
}
|
|
220
340
|
.test-item {
|
|
221
|
-
padding:
|
|
341
|
+
padding: 12px 20px;
|
|
222
342
|
border-bottom: 1px solid var(--border-color);
|
|
223
343
|
cursor: pointer;
|
|
224
344
|
display: flex;
|
|
225
345
|
align-items: center;
|
|
226
|
-
gap:
|
|
346
|
+
gap: 12px;
|
|
347
|
+
transition: background 0.1s;
|
|
227
348
|
}
|
|
228
|
-
.test-item:hover, .test-item.active { background-color: var(--hover-color); }
|
|
229
|
-
.test-status-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
349
|
+
.test-item:hover, .test-item.active { background-color: var(--hover-color); border-left: 4px solid var(--blue-accent); padding-left: 16px; }
|
|
350
|
+
.test-status-indicator {
|
|
351
|
+
width: 10px;
|
|
352
|
+
height: 10px;
|
|
353
|
+
border-radius: 50%;
|
|
233
354
|
}
|
|
355
|
+
.status-passed { background-color: var(--success-color); }
|
|
356
|
+
.status-failed { background-color: var(--fail-color); }
|
|
357
|
+
.status-skipped { background-color: var(--skip-color); }
|
|
358
|
+
|
|
234
359
|
.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: #
|
|
360
|
+
.test-title { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.95em; }
|
|
361
|
+
.test-suite { font-size: 0.8em; color: #888; margin-top: 3px; }
|
|
362
|
+
.test-duration { font-size: 0.8em; color: #666; width: 50px; text-align: right; }
|
|
238
363
|
|
|
239
364
|
/* Main Content */
|
|
240
365
|
.main-content {
|
|
241
366
|
flex: 1;
|
|
242
|
-
padding:
|
|
243
|
-
overflow-y:
|
|
367
|
+
padding: 0;
|
|
368
|
+
overflow-y: hidden;
|
|
244
369
|
display: none; /* Hidden by default until selected */
|
|
370
|
+
flex-direction: column;
|
|
245
371
|
}
|
|
246
|
-
.main-content.active { display:
|
|
372
|
+
.main-content.active { display: flex; }
|
|
247
373
|
|
|
248
|
-
.detail-header {
|
|
374
|
+
.detail-header-wrapper {
|
|
375
|
+
padding: 20px 30px;
|
|
249
376
|
border-bottom: 1px solid var(--border-color);
|
|
250
|
-
|
|
251
|
-
|
|
377
|
+
background-color: #252526;
|
|
378
|
+
}
|
|
379
|
+
.detail-title { font-size: 1.6em; margin: 10px 0; font-weight: 300; }
|
|
380
|
+
.detail-badges { display: flex; gap: 10px; align-items: center; }
|
|
381
|
+
.badge {
|
|
382
|
+
padding: 3px 8px;
|
|
383
|
+
border-radius: 3px;
|
|
384
|
+
font-size: 0.8em;
|
|
385
|
+
font-weight: 600;
|
|
386
|
+
text-transform: uppercase;
|
|
252
387
|
}
|
|
253
|
-
.
|
|
254
|
-
.
|
|
388
|
+
.badge-passed { background-color: rgba(76, 175, 80, 0.2); color: var(--success-color); border: 1px solid var(--success-color); }
|
|
389
|
+
.badge-failed { background-color: rgba(244, 67, 54, 0.2); color: var(--fail-color); border: 1px solid var(--fail-color); }
|
|
390
|
+
.badge-skipped { background-color: rgba(255, 152, 0, 0.2); color: var(--skip-color); border: 1px solid var(--skip-color); }
|
|
255
391
|
|
|
256
|
-
.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
border-bottom: 1px solid #333;
|
|
261
|
-
padding-bottom: 5px;
|
|
392
|
+
.detail-scroll-area {
|
|
393
|
+
flex: 1;
|
|
394
|
+
padding: 30px;
|
|
395
|
+
overflow-y: auto;
|
|
262
396
|
}
|
|
263
397
|
|
|
264
|
-
|
|
265
|
-
.steps-container {
|
|
398
|
+
.section-card {
|
|
266
399
|
background-color: #252526;
|
|
267
|
-
border
|
|
268
|
-
|
|
400
|
+
border: 1px solid var(--border-color);
|
|
401
|
+
border-radius: 6px;
|
|
402
|
+
margin-bottom: 25px;
|
|
403
|
+
overflow: hidden;
|
|
269
404
|
}
|
|
270
|
-
.
|
|
271
|
-
padding:
|
|
272
|
-
border-left: 2px solid #555;
|
|
273
|
-
margin-bottom: 5px;
|
|
405
|
+
.section-header {
|
|
406
|
+
padding: 10px 15px;
|
|
274
407
|
background-color: #2d2d2d;
|
|
408
|
+
border-bottom: 1px solid var(--border-color);
|
|
409
|
+
font-weight: 600;
|
|
410
|
+
color: #bbb;
|
|
411
|
+
font-size: 0.9em;
|
|
412
|
+
text-transform: uppercase;
|
|
413
|
+
letter-spacing: 0.5px;
|
|
275
414
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
.step-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
font-family:
|
|
285
|
-
font-size: 0.
|
|
286
|
-
|
|
287
|
-
|
|
415
|
+
|
|
416
|
+
/* Steps */
|
|
417
|
+
.step-row {
|
|
418
|
+
padding: 8px 15px;
|
|
419
|
+
border-bottom: 1px solid #333;
|
|
420
|
+
display: flex;
|
|
421
|
+
gap: 10px;
|
|
422
|
+
align-items: flex-start;
|
|
423
|
+
font-family: Consolas, monospace;
|
|
424
|
+
font-size: 0.9em;
|
|
425
|
+
transition: background-color 0.2s;
|
|
426
|
+
}
|
|
427
|
+
.step-row:last-child { border-bottom: none; }
|
|
428
|
+
.step-row:hover { background-color: #2a2d2e; }
|
|
429
|
+
.step-status { margin-top: 3px; font-size: 1.2em; line-height: 1; }
|
|
430
|
+
.step-content { flex: 1; }
|
|
431
|
+
.step-meta { color: #666; font-size: 0.9em; margin-left: 10px; white-space: nowrap; }
|
|
432
|
+
|
|
433
|
+
/* Tree Toggle */
|
|
434
|
+
.toggle-icon {
|
|
435
|
+
display: inline-block;
|
|
436
|
+
width: 16px;
|
|
437
|
+
text-align: center;
|
|
438
|
+
cursor: pointer;
|
|
439
|
+
user-select: none;
|
|
440
|
+
font-size: 0.8em;
|
|
441
|
+
transition: transform 0.1s;
|
|
442
|
+
color: #ccc;
|
|
443
|
+
}
|
|
444
|
+
.step-row.collapsed + .children-container { display: none; }
|
|
445
|
+
.step-row.collapsed .toggle-icon { transform: rotate(-90deg); }
|
|
446
|
+
|
|
447
|
+
/* Error Box */
|
|
448
|
+
.error-box {
|
|
449
|
+
padding: 15px;
|
|
450
|
+
background-color: rgba(244, 67, 54, 0.1);
|
|
451
|
+
border-left: 4px solid var(--fail-color);
|
|
452
|
+
margin: 0;
|
|
453
|
+
}
|
|
454
|
+
.error-message {
|
|
455
|
+
color: #ff8a80;
|
|
456
|
+
font-size: 1.1em;
|
|
457
|
+
margin-bottom: 10px;
|
|
288
458
|
white-space: pre-wrap;
|
|
459
|
+
}
|
|
460
|
+
.stack-trace {
|
|
461
|
+
background-color: var(--code-bg);
|
|
462
|
+
padding: 15px;
|
|
463
|
+
border-radius: 4px;
|
|
464
|
+
font-family: Consolas, monospace;
|
|
465
|
+
color: #d4d4d4;
|
|
466
|
+
white-space: pre-wrap;
|
|
467
|
+
overflow-x: auto;
|
|
468
|
+
border: 1px solid #30363d;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* Code Snippet */
|
|
472
|
+
.code-snippet {
|
|
473
|
+
margin-top: 10px;
|
|
474
|
+
background-color: #000;
|
|
475
|
+
border-radius: 4px;
|
|
476
|
+
padding: 10px;
|
|
477
|
+
font-family: Consolas, monospace;
|
|
478
|
+
font-size: 0.9em;
|
|
479
|
+
color: #ccc;
|
|
480
|
+
white-space: pre;
|
|
289
481
|
overflow-x: auto;
|
|
290
|
-
|
|
482
|
+
border-left: 3px solid #ffcc00;
|
|
291
483
|
}
|
|
292
484
|
|
|
293
485
|
/* Media */
|
|
294
|
-
.media-
|
|
295
|
-
display:
|
|
296
|
-
|
|
486
|
+
.media-grid {
|
|
487
|
+
display: grid;
|
|
488
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
297
489
|
gap: 20px;
|
|
490
|
+
padding: 20px;
|
|
298
491
|
}
|
|
299
492
|
.media-item {
|
|
300
493
|
background-color: #000;
|
|
301
|
-
padding: 5px;
|
|
302
494
|
border-radius: 4px;
|
|
303
495
|
border: 1px solid var(--border-color);
|
|
496
|
+
overflow: hidden;
|
|
304
497
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
498
|
+
.media-header {
|
|
499
|
+
padding: 8px 12px;
|
|
500
|
+
background-color: #333;
|
|
501
|
+
color: #ccc;
|
|
502
|
+
font-size: 0.85em;
|
|
503
|
+
border-bottom: 1px solid #444;
|
|
504
|
+
}
|
|
505
|
+
img, video { width: 100%; height: auto; display: block; }
|
|
506
|
+
|
|
507
|
+
.copy-btn {
|
|
508
|
+
float: right;
|
|
509
|
+
background: transparent;
|
|
510
|
+
border: 1px solid #555;
|
|
511
|
+
color: #aaa;
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
padding: 2px 8px;
|
|
514
|
+
border-radius: 3px;
|
|
314
515
|
}
|
|
516
|
+
.copy-btn:hover { background: #333; color: white; }
|
|
517
|
+
|
|
315
518
|
</style>
|
|
316
519
|
</head>
|
|
317
520
|
<body>
|
|
318
521
|
<div class="sidebar">
|
|
319
522
|
<div class="sidebar-header">
|
|
320
|
-
<div class="
|
|
321
|
-
<div class="
|
|
322
|
-
|
|
323
|
-
|
|
523
|
+
<div class="chart-container">
|
|
524
|
+
<div class="chart-inner">
|
|
525
|
+
<div class="total-text">${totalTests}</div>
|
|
526
|
+
<div class="total-label">TESTS</div>
|
|
527
|
+
</div>
|
|
324
528
|
</div>
|
|
325
|
-
|
|
529
|
+
|
|
530
|
+
<div class="stats-row">
|
|
531
|
+
<div class="stat-item" onclick="setFilter('passed')" title="Show Passed">
|
|
532
|
+
<span class="stat-value" style="color:var(--success-color)">${passedTests}</span>
|
|
533
|
+
<span class="stat-label">Passed</span>
|
|
534
|
+
</div>
|
|
535
|
+
<div class="stat-item" onclick="setFilter('failed')" title="Show Failed">
|
|
536
|
+
<span class="stat-value" style="color:var(--fail-color)">${failedTests}</span>
|
|
537
|
+
<span class="stat-label">Failed</span>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="stat-item" onclick="setFilter('skipped')" title="Show Skipped">
|
|
540
|
+
<span class="stat-value" style="color:var(--skip-color)">${skippedTests}</span>
|
|
541
|
+
<span class="stat-label">Skipped</span>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<div class="filter-group">
|
|
546
|
+
<button class="filter-btn active" onclick="setFilter('all')">All</button>
|
|
547
|
+
<button class="filter-btn" onclick="setFilter('failed')">Failures</button>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<input type="text" class="search-box" placeholder="Search tests..." id="searchInput" onkeyup="filterTests()">
|
|
326
551
|
</div>
|
|
327
552
|
<div class="test-list" id="testList"></div>
|
|
328
553
|
</div>
|
|
329
554
|
|
|
330
555
|
<div id="contentArea" class="main-content">
|
|
331
|
-
<div class="empty-state">
|
|
556
|
+
<div class="empty-state">
|
|
557
|
+
<div style="text-align:center">
|
|
558
|
+
<div style="font-size:3em; margin-bottom:10px">🧪</div>
|
|
559
|
+
<div>Select a test to view execution details</div>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
332
562
|
</div>
|
|
333
563
|
|
|
334
564
|
<script>
|
|
335
565
|
const data = ${resultsData};
|
|
336
566
|
let currentId = null;
|
|
567
|
+
let currentFilter = 'all';
|
|
568
|
+
|
|
569
|
+
function setFilter(filter) {
|
|
570
|
+
currentFilter = filter;
|
|
571
|
+
// Update buttons
|
|
572
|
+
document.querySelectorAll('.stat-item').forEach(el => el.classList.remove('active'));
|
|
573
|
+
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
|
|
574
|
+
|
|
575
|
+
if (filter === 'all') document.querySelector('button[onclick="setFilter(\\'all\\')"]').classList.add('active');
|
|
576
|
+
else if (filter === 'failed') document.querySelector('button[onclick="setFilter(\\'failed\\')"]').classList.add('active');
|
|
577
|
+
|
|
578
|
+
filterTests();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function filterTests() {
|
|
582
|
+
const query = document.getElementById('searchInput').value.toLowerCase();
|
|
583
|
+
const filtered = data.filter(item => {
|
|
584
|
+
const matchesSearch = item.title.toLowerCase().includes(query) || item.suite.toLowerCase().includes(query);
|
|
585
|
+
const matchesFilter = currentFilter === 'all' ? true :
|
|
586
|
+
currentFilter === 'failed' ? (item.status === 'failed' || item.status === 'timedOut') :
|
|
587
|
+
item.status === currentFilter;
|
|
588
|
+
return matchesSearch && matchesFilter;
|
|
589
|
+
});
|
|
590
|
+
renderList(filtered);
|
|
591
|
+
}
|
|
337
592
|
|
|
338
593
|
function renderList(items) {
|
|
339
594
|
const container = document.getElementById('testList');
|
|
595
|
+
if (items.length === 0) {
|
|
596
|
+
container.innerHTML = '<div style="padding:20px; text-align:center; color:#666">No tests match current filter</div>';
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
340
599
|
container.innerHTML = items.map(item => {
|
|
341
|
-
const
|
|
600
|
+
const statusClass = item.status === 'passed' ? 'status-passed' :
|
|
601
|
+
(item.status === 'failed' || item.status === 'timedOut') ? 'status-failed' : 'status-skipped';
|
|
342
602
|
const activeClass = currentId === item.id ? 'active' : '';
|
|
343
603
|
return \`<div class="test-item \${activeClass}" onclick="selectTest(\${item.id})">
|
|
344
|
-
<div class="test-status-
|
|
604
|
+
<div class="test-status-indicator \${statusClass}"></div>
|
|
345
605
|
<div class="test-info">
|
|
346
606
|
<div class="test-title" title="\${item.title}">\${item.title}</div>
|
|
347
607
|
<div class="test-suite">\${item.suite}</div>
|
|
@@ -351,70 +611,115 @@ class HtmlReporter {
|
|
|
351
611
|
}).join('');
|
|
352
612
|
}
|
|
353
613
|
|
|
614
|
+
function toggleStep(element) {
|
|
615
|
+
const row = element.closest('.step-row');
|
|
616
|
+
row.classList.toggle('collapsed');
|
|
617
|
+
const icon = row.querySelector('.toggle-icon');
|
|
618
|
+
if (row.classList.contains('collapsed')) {
|
|
619
|
+
icon.textContent = '▶';
|
|
620
|
+
} else {
|
|
621
|
+
icon.textContent = '▼';
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function generateStepTreeHtml(step) {
|
|
626
|
+
const hasChildren = step.steps && step.steps.length > 0;
|
|
627
|
+
const isPassed = !step.error;
|
|
628
|
+
// Collapse by default if passed and has children
|
|
629
|
+
const isCollapsed = hasChildren && isPassed;
|
|
630
|
+
const collapsedClass = isCollapsed ? 'collapsed' : '';
|
|
631
|
+
const icon = hasChildren ? (isCollapsed ? '▶' : '▼') : '';
|
|
632
|
+
|
|
633
|
+
const statusIcon = step.error ? '❌' : '✓';
|
|
634
|
+
const style = step.error ? 'color:var(--fail-color)' : 'color:var(--success-color)';
|
|
635
|
+
|
|
636
|
+
let childHtml = '';
|
|
637
|
+
if (hasChildren) {
|
|
638
|
+
childHtml = \`<div class="children-container">\${step.steps.map(generateStepTreeHtml).join('')}</div>\`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return \`
|
|
642
|
+
<div class="step-wrapper">
|
|
643
|
+
<div class="step-row \${collapsedClass}" style="padding-left: \${15 + (step.depth * 20)}px" >
|
|
644
|
+
\${hasChildren ? \`<span class="toggle-icon" onclick="toggleStep(this)">\${icon}</span>\` : '<span style="width:16px;display:inline-block"></span>'}
|
|
645
|
+
<div class="step-status" style="\${style}">\${statusIcon}</div>
|
|
646
|
+
<div class="step-content">
|
|
647
|
+
<div>\${step.title}</div>
|
|
648
|
+
\${step.error ? \`<div style="margin-top:5px; color:#ff8a80; font-size:0.9em">\${step.error}</div>\` : ''}
|
|
649
|
+
\${step.codeSnippet ? \`<div class="code-snippet">\${step.codeSnippet}</div>\` : ''}
|
|
650
|
+
</div>
|
|
651
|
+
<div class="step-meta">\${step.duration}ms</div>
|
|
652
|
+
</div>
|
|
653
|
+
\${childHtml}
|
|
654
|
+
</div>
|
|
655
|
+
\`;
|
|
656
|
+
}
|
|
657
|
+
|
|
354
658
|
function selectTest(id) {
|
|
355
659
|
currentId = id;
|
|
356
|
-
|
|
660
|
+
filterTests(); // Re-render to update active state
|
|
357
661
|
const test = data.find(t => t.id === id);
|
|
358
662
|
if (!test) return;
|
|
359
663
|
|
|
360
664
|
const content = document.getElementById('contentArea');
|
|
361
|
-
const
|
|
665
|
+
const statusClass = test.status === 'passed' ? 'badge-passed' :
|
|
666
|
+
(test.status === 'failed' || test.status === 'timedOut') ? 'badge-failed' : 'badge-skipped';
|
|
362
667
|
|
|
363
|
-
|
|
364
|
-
|
|
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('');
|
|
668
|
+
// Steps (using tree renderer)
|
|
669
|
+
const stepsHtml = test.steps.map(generateStepTreeHtml).join('');
|
|
372
670
|
|
|
671
|
+
// Media
|
|
373
672
|
const mediaHtml = \`
|
|
374
673
|
\${test.screenshot ? \`
|
|
375
674
|
<div class="media-item">
|
|
376
|
-
<div style="
|
|
377
|
-
<img src="\${test.screenshot}"
|
|
675
|
+
<div class="media-header">Screenshot <a href="\${test.screenshot}" target="_blank" style="float:right; color:#4dabf7; text-decoration:none">Open New Tab</a></div>
|
|
676
|
+
<img src="\${test.screenshot}" onclick="window.open(this.src)">
|
|
378
677
|
</div>\` : ''}
|
|
379
678
|
\${test.video ? \`
|
|
380
679
|
<div class="media-item">
|
|
381
|
-
<div
|
|
382
|
-
<video src="\${test.video}"
|
|
680
|
+
<div class="media-header">Video</div>
|
|
681
|
+
<video src="\${test.video}" controls></video>
|
|
383
682
|
</div>\` : ''}
|
|
384
683
|
\`;
|
|
385
684
|
|
|
386
685
|
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>
|
|
686
|
+
<div class="detail-header-wrapper">
|
|
392
687
|
<div class="detail-meta">
|
|
393
|
-
<span
|
|
394
|
-
<span
|
|
688
|
+
<span class="badge \${statusClass}">\${test.status}</span>
|
|
689
|
+
<span>\${test.suite}</span>
|
|
690
|
+
<span style="margin-left:auto">Duration: \${test.duration}ms</span>
|
|
395
691
|
</div>
|
|
396
|
-
|
|
692
|
+
<div class="detail-title">\${test.title}</div>
|
|
397
693
|
</div>
|
|
694
|
+
|
|
695
|
+
<div class="detail-scroll-area">
|
|
696
|
+
\${test.error ? \`
|
|
697
|
+
<div class="section-card error-box">
|
|
698
|
+
<div style="display:flex; justify-content:space-between; margin-bottom:10px">
|
|
699
|
+
<strong style="color:var(--fail-color)">Error Details</strong>
|
|
700
|
+
</div>
|
|
701
|
+
<div class="stack-trace">\${test.error}</div>
|
|
702
|
+
</div>
|
|
703
|
+
\` : ''}
|
|
398
704
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
705
|
+
<div class="section-card">
|
|
706
|
+
<div class="section-header">Execution Steps</div>
|
|
707
|
+
<div class="steps-tree-container">
|
|
708
|
+
\${stepsHtml}
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
403
711
|
|
|
404
|
-
|
|
712
|
+
\${test.screenshot || test.video ? \`
|
|
713
|
+
<div class="section-card">
|
|
714
|
+
<div class="section-header">Artifacts</div>
|
|
715
|
+
<div class="media-grid">\${mediaHtml}</div>
|
|
716
|
+
</div>
|
|
717
|
+
\` : ''}
|
|
718
|
+
</div>
|
|
405
719
|
\`;
|
|
406
720
|
content.classList.add('active');
|
|
407
721
|
}
|
|
408
722
|
|
|
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
723
|
// Initial render
|
|
419
724
|
renderList(data);
|
|
420
725
|
</script>
|
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.11",
|
|
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
|
},
|