@flash-ai-team/flash-test-framework 0.0.1

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.
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const nodemailer_1 = __importDefault(require("nodemailer"));
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const archiver_1 = __importDefault(require("archiver"));
10
+ const ReporterUtils_1 = require("./ReporterUtils");
11
+ class EmailReporter {
12
+ constructor() {
13
+ this.suiteStartTime = 0;
14
+ }
15
+ onBegin(config, suite) {
16
+ this.suiteStartTime = Date.now();
17
+ this.rootSuite = suite;
18
+ console.log(`[EmailReporter] Starting test suite at ${new Date(this.suiteStartTime).toISOString()}`);
19
+ }
20
+ async onEnd(result) {
21
+ const duration = Date.now() - this.suiteStartTime;
22
+ console.log(`[EmailReporter] Finished suite in ${duration}ms`);
23
+ let config;
24
+ try {
25
+ const configPath = path_1.default.resolve(process.cwd(), 'email.config.json');
26
+ if (fs_1.default.existsSync(configPath)) {
27
+ config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
28
+ }
29
+ }
30
+ catch (e) {
31
+ console.error('[EmailReporter] Failed to load email.config.json', e);
32
+ }
33
+ if (!config || !config.enabled) {
34
+ console.log('[EmailReporter] Email reporting disabled in email.config.json or file not found.');
35
+ return;
36
+ }
37
+ const reportFolder = (0, ReporterUtils_1.getReportFolder)({ suites: [] }); // Use memoized folder
38
+ const zipPath = `${reportFolder}.zip`;
39
+ console.log(`[EmailReporter] Zipping report folder: ${reportFolder}`);
40
+ try {
41
+ await this.zipDirectory(reportFolder, zipPath);
42
+ console.log(`[EmailReporter] Zip created at: ${zipPath}`);
43
+ }
44
+ catch (error) {
45
+ console.error('[EmailReporter] Failed to zip report:', error);
46
+ }
47
+ const transport = nodemailer_1.default.createTransport({
48
+ host: config.host,
49
+ port: config.port,
50
+ secure: config.secure,
51
+ auth: config.auth,
52
+ });
53
+ // Generate Detailed Summary
54
+ let suiteSummaryRows = '';
55
+ if (this.rootSuite) {
56
+ for (const projectSuite of this.rootSuite.suites) {
57
+ const projectName = projectSuite.title;
58
+ // In Playwright, projectSuite.suites are usually the files (specs)
59
+ for (const fileSuite of projectSuite.suites) {
60
+ const suiteName = fileSuite.title;
61
+ const allTests = fileSuite.allTests();
62
+ const total = allTests.length;
63
+ let passed = 0;
64
+ let failed = 0;
65
+ let skipped = 0;
66
+ for (const test of allTests) {
67
+ const outcome = test.results[0]?.status; // Naive status check, ideally check all results or use outcome() logic
68
+ // Better: check test.outcome() if available or calculate
69
+ if (test.outcome() === 'expected')
70
+ passed++;
71
+ else if (test.outcome() === 'skipped')
72
+ skipped++;
73
+ else if (test.outcome() === 'unexpected')
74
+ failed++;
75
+ else if (test.outcome() === 'flaky')
76
+ passed++; // Treat flaky as passed in simple summary
77
+ }
78
+ // Only show if tests were run or if user wants to see everything
79
+ if (total > 0) {
80
+ const rowColor = failed > 0 ? '#ffe6e6' : '#e6ffe6';
81
+ suiteSummaryRows += `
82
+ <tr style="background-color: ${rowColor};">
83
+ <td>${projectName}</td>
84
+ <td>${path_1.default.basename(suiteName)}</td>
85
+ <td>${total}</td>
86
+ <td>${passed}</td>
87
+ <td style="color: ${failed > 0 ? 'red' : 'green'}; font-weight: bold;">${failed}</td>
88
+ <td>${skipped}</td>
89
+ </tr>
90
+ `;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ const status = result.status.toUpperCase();
96
+ const statusColor = status === 'PASSED' ? 'green' : 'red';
97
+ const subject = `Test Run ${status}: ${new Date().toLocaleString()}`;
98
+ const html = `
99
+ <h2>Test Execution Summary</h2>
100
+ <p><strong>Status:</strong> <span style="color: ${statusColor}; font-weight: bold;">${status}</span></p>
101
+ <p><strong>Duration:</strong> ${(duration / 1000).toFixed(2)}s</p>
102
+ <p><strong>Start Time:</strong> ${new Date(this.suiteStartTime).toLocaleString()}</p>
103
+
104
+ <h3>Suite Breakdown</h3>
105
+ <table border="1" cellpadding="8" cellspacing="0" style="border-collapse: collapse; font-family: Arial, sans-serif; width: 100%;">
106
+ <thead style="background-color: #f2f2f2;">
107
+ <tr>
108
+ <th style="text-align: left;">Browser</th>
109
+ <th style="text-align: left;">Suite (File)</th>
110
+ <th>Total</th>
111
+ <th>Passed</th>
112
+ <th>Failed</th>
113
+ <th>Skipped</th>
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ ${suiteSummaryRows}
118
+ </tbody>
119
+ </table>
120
+
121
+ <br/>
122
+ <p>Report attached.</p>
123
+ `;
124
+ const mailOptions = {
125
+ from: config.from,
126
+ to: config.to,
127
+ subject: subject,
128
+ html: html,
129
+ attachments: []
130
+ };
131
+ if (fs_1.default.existsSync(zipPath)) {
132
+ mailOptions.attachments.push({
133
+ filename: 'TestReport.zip',
134
+ path: zipPath
135
+ });
136
+ }
137
+ const htmlReportPath = path_1.default.join(reportFolder, 'report.html');
138
+ if (fs_1.default.existsSync(htmlReportPath)) {
139
+ mailOptions.attachments.push({
140
+ filename: 'report.html',
141
+ path: htmlReportPath
142
+ });
143
+ }
144
+ else {
145
+ console.warn('[EmailReporter] Warning: Report zip file not found, sending email without attachment.');
146
+ }
147
+ try {
148
+ console.log(`[EmailReporter] Sending email to ${config.to}...`);
149
+ const info = await transport.sendMail(mailOptions);
150
+ console.log(`[EmailReporter] Email sent: ${info.messageId}`);
151
+ }
152
+ catch (error) {
153
+ console.error('[EmailReporter] Error sending email:', error);
154
+ }
155
+ }
156
+ zipDirectory(sourceDir, outPath) {
157
+ const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
158
+ const stream = fs_1.default.createWriteStream(outPath);
159
+ return new Promise((resolve, reject) => {
160
+ archive
161
+ .directory(sourceDir, false)
162
+ .on('error', err => reject(err))
163
+ .pipe(stream);
164
+ stream.on('close', () => resolve());
165
+ archive.finalize();
166
+ });
167
+ }
168
+ }
169
+ exports.default = EmailReporter;
@@ -0,0 +1,11 @@
1
+ import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
+ declare class HtmlReporter implements Reporter {
3
+ private suiteStartTime;
4
+ private reportPath;
5
+ onBegin(config: FullConfig, suite: Suite): void;
6
+ onEnd(result: FullResult): Promise<void>;
7
+ private testResults;
8
+ onTestEnd(test: TestCase, result: TestResult): void;
9
+ private generateHtml;
10
+ }
11
+ export default HtmlReporter;
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fs = __importStar(require("fs"));
37
+ const path = __importStar(require("path"));
38
+ const ReporterUtils_1 = require("./ReporterUtils");
39
+ class HtmlReporter {
40
+ constructor() {
41
+ this.suiteStartTime = 0;
42
+ this.reportPath = '';
43
+ // onExit removed as generation is now in onEnd
44
+ this.testResults = [];
45
+ }
46
+ onBegin(config, suite) {
47
+ this.suiteStartTime = Date.now();
48
+ const folder = (0, ReporterUtils_1.getReportFolder)(suite);
49
+ this.reportPath = path.join(folder, 'report.html');
50
+ }
51
+ async onEnd(result) {
52
+ if (!this.reportPath)
53
+ return;
54
+ const html = this.generateHtml();
55
+ if (!fs.existsSync(path.dirname(this.reportPath))) {
56
+ fs.mkdirSync(path.dirname(this.reportPath), { recursive: true });
57
+ }
58
+ fs.writeFileSync(this.reportPath, html);
59
+ console.log(`HTML Report generated at: ${this.reportPath}`);
60
+ }
61
+ onTestEnd(test, result) {
62
+ let suite = test.parent;
63
+ let suiteName = '';
64
+ while (suite) {
65
+ if (suite.title && suite.title !== 'Root') {
66
+ suiteName = suite.title + (suiteName ? ' > ' + suiteName : '');
67
+ }
68
+ suite = suite.parent;
69
+ }
70
+ if (!suiteName) {
71
+ suiteName = path.basename(test.location.file);
72
+ }
73
+ const reportDir = this.reportPath ? path.dirname(this.reportPath) : '';
74
+ if (reportDir && !fs.existsSync(reportDir)) {
75
+ fs.mkdirSync(reportDir, { recursive: true });
76
+ }
77
+ // Helper to copy attachment
78
+ const copyAttachment = (name, matchFn) => {
79
+ const attachment = result.attachments.find(matchFn);
80
+ if (attachment && attachment.path && reportDir) {
81
+ const fileName = `${name}_${test.title.replace(/[^a-z0-9]/gi, '_')}_${Date.now()}${path.extname(attachment.path)}`;
82
+ const destPath = path.join(reportDir, fileName);
83
+ try {
84
+ fs.copyFileSync(attachment.path, destPath);
85
+ return fileName;
86
+ }
87
+ catch (e) {
88
+ console.error(`Failed to copy ${name} attachment`, e);
89
+ }
90
+ }
91
+ return undefined;
92
+ };
93
+ // Handle Video
94
+ const videoRelativePath = copyAttachment('video', a => a.name === 'video' && !!a.path);
95
+ // Handle Screenshot (usually named 'screenshot' or has image content type)
96
+ const screenshotRelativePath = copyAttachment('screenshot', a => (a.name === 'screenshot' || a.contentType.startsWith('image/')) && !!a.path);
97
+ this.testResults.push({ test, result, suiteName, videoPath: videoRelativePath, screenshotPath: screenshotRelativePath });
98
+ }
99
+ 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('');
144
+ return `
145
+ <!DOCTYPE html>
146
+ <html>
147
+ <head>
148
+ <title>Test Execution Report</title>
149
+ <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; }
161
+ </style>
162
+ <script>
163
+ function toggleDetails(id) {
164
+ var row = document.getElementById('details-' + id);
165
+ row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
166
+ }
167
+ </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
+ </body>
186
+ </html>
187
+ `;
188
+ }
189
+ }
190
+ exports.default = HtmlReporter;
@@ -0,0 +1,3 @@
1
+ import { Suite } from '@playwright/test/reporter';
2
+ export declare function getReportFolder(suite: Suite): string;
3
+ export declare function resetReportFolderCache(): void;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getReportFolder = getReportFolder;
37
+ exports.resetReportFolderCache = resetReportFolderCache;
38
+ const path = __importStar(require("path"));
39
+ let cachedReportFolder = null;
40
+ function getReportFolder(suite) {
41
+ if (cachedReportFolder) {
42
+ return cachedReportFolder;
43
+ }
44
+ const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '').split('.')[0];
45
+ // Find the first test file suite
46
+ let suiteName = 'TestRun';
47
+ if (suite.suites.length > 0) {
48
+ const projectSuite = suite.suites[0];
49
+ if (projectSuite.suites.length > 0) {
50
+ const fileSuite = projectSuite.suites[0];
51
+ // Extract basename without extension (e.g. "TestA.spec.ts" -> "TestA")
52
+ const filename = path.basename(fileSuite.location?.file || '');
53
+ if (filename) {
54
+ suiteName = filename.split('.')[0];
55
+ }
56
+ }
57
+ }
58
+ cachedReportFolder = path.join(process.cwd(), 'reports', suiteName, `test_${timestamp}`);
59
+ console.log(`[ReporterUtils] Calculated report folder: ${cachedReportFolder}`);
60
+ return cachedReportFolder;
61
+ }
62
+ function resetReportFolderCache() {
63
+ cachedReportFolder = null;
64
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@flash-ai-team/flash-test-framework",
3
+ "version": "0.0.1",
4
+ "description": "A powerful keyword-driven automation framework built on top of Playwright and TypeScript.",
5
+ "keywords": [
6
+ "playwright",
7
+ "automation",
8
+ "framework",
9
+ "keyword-driven",
10
+ "typescript",
11
+ "testing",
12
+ "flash-test"
13
+ ],
14
+ "homepage": "https://bitbucket.org/dluonganh/flash-ai-test#readme",
15
+ "bugs": {
16
+ "url": "https://bitbucket.org/dluonganh/flash-ai-test/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+ssh://git@bitbucket.org/dluonganh/flash-ai-test.git"
21
+ },
22
+ "license": "ISC",
23
+ "author": "Flash Test Team",
24
+ "type": "commonjs",
25
+ "main": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "directories": {
28
+ "test": "tests"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "prepublishOnly": "npm run build",
38
+ "test": "playwright test",
39
+ "test:ui": "playwright test --ui",
40
+ "test:chrome": "npm run test:chrome:headless",
41
+ "test:chrome:headless": "playwright test --project=chrome",
42
+ "test:chrome:ui": "playwright test --project=chrome --headed",
43
+ "test:firefox": "playwright test --project=firefox",
44
+ "test:webkit": "playwright test --project=webkit",
45
+ "test:suite:parallel": "playwright test tests/suites/parallel_suite.spec.ts",
46
+ "test:suite:sequential": "playwright test tests/suites/sequential_suite.spec.ts",
47
+ "test:all:parallel": "playwright test --workers=4",
48
+ "test:all:serial": "playwright test --workers=1"
49
+ },
50
+ "dependencies": {
51
+ "archiver": "^7.0.1",
52
+ "jimp": "^1.6.0",
53
+ "nodemailer": "^7.0.12",
54
+ "openai": "^6.15.0"
55
+ },
56
+ "devDependencies": {
57
+ "@playwright/test": "^1.57.0",
58
+ "@types/archiver": "^7.0.0",
59
+ "@types/node": "^25.0.3",
60
+ "@types/nodemailer": "^7.0.4",
61
+ "typescript": "^5.9.3"
62
+ }
63
+ }