testlens-playwright-reporter 0.1.0 → 0.1.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.
- package/index.d.ts +0 -4
- package/index.js +94 -143
- package/index.ts +57 -101
- package/lib/index.js +50 -81
- package/package.json +3 -3
- package/README.md +0 -238
package/index.d.ts
CHANGED
|
@@ -11,8 +11,6 @@ export interface TestLensReporterConfig {
|
|
|
11
11
|
enableGitInfo?: boolean;
|
|
12
12
|
/** Enable artifact processing */
|
|
13
13
|
enableArtifacts?: boolean;
|
|
14
|
-
/** Enable S3 upload for artifacts */
|
|
15
|
-
enableS3Upload?: boolean;
|
|
16
14
|
/** Batch size for API requests */
|
|
17
15
|
batchSize?: number;
|
|
18
16
|
/** Flush interval in milliseconds */
|
|
@@ -38,8 +36,6 @@ export interface TestLensReporterOptions {
|
|
|
38
36
|
enableGitInfo?: boolean;
|
|
39
37
|
/** Enable artifact processing */
|
|
40
38
|
enableArtifacts?: boolean;
|
|
41
|
-
/** Enable S3 upload for artifacts */
|
|
42
|
-
enableS3Upload?: boolean;
|
|
43
39
|
/** Batch size for API requests */
|
|
44
40
|
batchSize?: number;
|
|
45
41
|
/** Flush interval in milliseconds */
|
package/index.js
CHANGED
|
@@ -2,36 +2,24 @@ const { randomUUID } = require('crypto');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const https = require('https');
|
|
5
6
|
const axios = require('axios');
|
|
6
|
-
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
7
|
-
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
|
8
7
|
const mime = require('mime');
|
|
9
|
-
|
|
10
|
-
// Load environment variables from .env file
|
|
11
|
-
// Embedded configuration from .env file (generated during build)
|
|
12
|
-
const EMBEDDED_CONFIG = {
|
|
13
|
-
"AWS_ACCESS_KEY_ID": "AKIA5QK76YZKWA2P5V4E",
|
|
14
|
-
"AWS_SECRET_ACCESS_KEY": "eX8lbMPMKBqTxmiNZO5dpLH5x6Do+cqnzhqJOOxJ",
|
|
15
|
-
"AWS_REGION": "eu-north-1",
|
|
16
|
-
"S3_BUCKET_NAME": "testlenss3",
|
|
17
|
-
"S3_ACL": "private",
|
|
18
|
-
"TEST_API_ENDPOINT": "https://testlens.qa-path.com/api/v1/webhook/playwright"
|
|
19
|
-
};
|
|
20
|
-
|
|
8
|
+
const FormData = require('form-data');
|
|
21
9
|
class TestLensReporter {
|
|
22
10
|
constructor(options = {}) {
|
|
23
11
|
this.config = {
|
|
24
|
-
apiEndpoint:
|
|
12
|
+
apiEndpoint: options.apiEndpoint || '',
|
|
25
13
|
apiKey: options.apiKey, // API key must come from config file
|
|
26
14
|
enableRealTimeStream: options.enableRealTimeStream !== false,
|
|
27
15
|
enableGitInfo: options.enableGitInfo !== false,
|
|
28
16
|
enableArtifacts: options.enableArtifacts !== false,
|
|
29
|
-
enableS3Upload: options.enableS3Upload !== false,
|
|
30
17
|
batchSize: options.batchSize || 10,
|
|
31
18
|
flushInterval: options.flushInterval || 5000,
|
|
32
19
|
retryAttempts: options.retryAttempts || 3,
|
|
33
20
|
timeout: options.timeout || 30000,
|
|
34
|
-
|
|
21
|
+
rejectUnauthorized: options.rejectUnauthorized !== false, // Default to true for security
|
|
22
|
+
ignoreSslErrors: options.ignoreSslErrors === true // Explicit opt-in to ignore SSL errors
|
|
35
23
|
};
|
|
36
24
|
|
|
37
25
|
if (!this.config.apiEndpoint) {
|
|
@@ -42,38 +30,25 @@ class TestLensReporter {
|
|
|
42
30
|
throw new Error('API_KEY is required for TestLensReporter. Pass it as apiKey option in your playwright config.');
|
|
43
31
|
}
|
|
44
32
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (process.env.AWS_ENDPOINT) {
|
|
64
|
-
s3Config.endpoint = process.env.AWS_ENDPOINT;
|
|
65
|
-
s3Config.forcePathStyle = true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
this.s3Client = new S3Client(s3Config);
|
|
69
|
-
console.log('✅ S3 client initialized for artifact uploads');
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error('❌ Failed to initialize S3 client:', error.message);
|
|
72
|
-
this.s3Enabled = false;
|
|
73
|
-
}
|
|
33
|
+
// Determine SSL validation behavior
|
|
34
|
+
let rejectUnauthorized = true; // Default to secure
|
|
35
|
+
|
|
36
|
+
// Check various ways SSL validation can be disabled (in order of precedence)
|
|
37
|
+
if (this.config.ignoreSslErrors) {
|
|
38
|
+
// Explicit configuration option
|
|
39
|
+
rejectUnauthorized = false;
|
|
40
|
+
console.log('⚠️ SSL certificate validation disabled via ignoreSslErrors option');
|
|
41
|
+
} else if (this.config.rejectUnauthorized === false) {
|
|
42
|
+
// Explicit configuration option
|
|
43
|
+
rejectUnauthorized = false;
|
|
44
|
+
console.log('⚠️ SSL certificate validation disabled via rejectUnauthorized option');
|
|
45
|
+
} else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
|
|
46
|
+
// Environment variable override
|
|
47
|
+
rejectUnauthorized = false;
|
|
48
|
+
console.log('⚠️ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
74
49
|
}
|
|
75
50
|
|
|
76
|
-
// Set up axios instance with retry logic
|
|
51
|
+
// Set up axios instance with retry logic and enhanced SSL handling
|
|
77
52
|
this.axiosInstance = axios.create({
|
|
78
53
|
baseURL: this.config.apiEndpoint,
|
|
79
54
|
timeout: this.config.timeout,
|
|
@@ -81,6 +56,13 @@ class TestLensReporter {
|
|
|
81
56
|
'Content-Type': 'application/json',
|
|
82
57
|
...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
|
|
83
58
|
},
|
|
59
|
+
// Enhanced SSL handling
|
|
60
|
+
httpsAgent: new https.Agent({
|
|
61
|
+
rejectUnauthorized: rejectUnauthorized,
|
|
62
|
+
// Additional SSL options for better compatibility
|
|
63
|
+
secureProtocol: 'TLSv1_2_method',
|
|
64
|
+
ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384'
|
|
65
|
+
})
|
|
84
66
|
});
|
|
85
67
|
|
|
86
68
|
// Add retry interceptor
|
|
@@ -374,27 +356,20 @@ class TestLensReporter {
|
|
|
374
356
|
if (attachment.path) {
|
|
375
357
|
try {
|
|
376
358
|
console.log(`📤 Processing ${attachment.name} asynchronously...`);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
382
|
-
console.log(`✅ S3 upload completed for ${attachment.name}`);
|
|
383
|
-
}
|
|
384
|
-
|
|
359
|
+
|
|
360
|
+
// Upload to S3 first
|
|
361
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
362
|
+
|
|
385
363
|
const artifactData = {
|
|
386
364
|
testId,
|
|
387
365
|
type: this.getArtifactType(attachment.name),
|
|
388
366
|
path: attachment.path,
|
|
389
367
|
name: attachment.name,
|
|
390
368
|
contentType: attachment.contentType,
|
|
391
|
-
fileSize:
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
presignedUrl: s3Data.presignedUrl,
|
|
396
|
-
storageType: 's3'
|
|
397
|
-
})
|
|
369
|
+
fileSize: this.getFileSize(attachment.path),
|
|
370
|
+
storageType: 's3',
|
|
371
|
+
s3Key: s3Data.key,
|
|
372
|
+
s3Url: s3Data.url
|
|
398
373
|
};
|
|
399
374
|
|
|
400
375
|
// Send artifact data to API
|
|
@@ -405,7 +380,7 @@ class TestLensReporter {
|
|
|
405
380
|
artifact: artifactData
|
|
406
381
|
});
|
|
407
382
|
|
|
408
|
-
console.log(`📎 Processed artifact: ${attachment.name}
|
|
383
|
+
console.log(`📎 Processed artifact: ${attachment.name} (uploaded to S3)`);
|
|
409
384
|
} catch (error) {
|
|
410
385
|
console.error(`❌ Failed to process ${attachment.name}:`, error.message);
|
|
411
386
|
}
|
|
@@ -584,86 +559,6 @@ class TestLensReporter {
|
|
|
584
559
|
return `${test.location.file}:${test.location.line}:${cleanTitle}`;
|
|
585
560
|
}
|
|
586
561
|
|
|
587
|
-
async uploadArtifactToS3(filePath, testId, fileName) {
|
|
588
|
-
console.log(`🔄 Starting S3 upload for ${fileName}...`);
|
|
589
|
-
try {
|
|
590
|
-
const fileContent = fs.readFileSync(filePath);
|
|
591
|
-
const fileSize = fileContent.length;
|
|
592
|
-
console.log(`📏 File size: ${fileSize} bytes`);
|
|
593
|
-
|
|
594
|
-
// Get content type based on file extension
|
|
595
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
596
|
-
const contentType = this.getContentType(ext);
|
|
597
|
-
console.log(`📄 Content type: ${contentType}`);
|
|
598
|
-
|
|
599
|
-
// Generate S3 key
|
|
600
|
-
const s3Key = this.generateS3Key(this.runId, testId, fileName);
|
|
601
|
-
console.log(`🔑 S3 key: ${s3Key}`);
|
|
602
|
-
|
|
603
|
-
const uploadParams = {
|
|
604
|
-
Bucket: EMBEDDED_CONFIG.S3_BUCKET_NAME,
|
|
605
|
-
Key: s3Key,
|
|
606
|
-
Body: fileContent,
|
|
607
|
-
ContentType: contentType,
|
|
608
|
-
ACL: EMBEDDED_CONFIG.S3_ACL || 'private',
|
|
609
|
-
ServerSideEncryption: process.env.S3_SSE || 'AES256'
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
console.log(`☁️ Uploading to S3 bucket: ${EMBEDDED_CONFIG.S3_BUCKET_NAME}`);
|
|
613
|
-
await this.s3Client.send(new PutObjectCommand(uploadParams));
|
|
614
|
-
console.log(`✅ S3 upload completed for ${fileName}`);
|
|
615
|
-
|
|
616
|
-
// Generate presigned URL for frontend access (expires in 7 days)
|
|
617
|
-
console.log(`🔗 Generating presigned URL for ${fileName}...`);
|
|
618
|
-
const getCommand = new GetObjectCommand({
|
|
619
|
-
Bucket: EMBEDDED_CONFIG.S3_BUCKET_NAME,
|
|
620
|
-
Key: s3Key
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
const presignedUrl = await getSignedUrl(this.s3Client, getCommand, {
|
|
624
|
-
expiresIn: 604800 // 7 days
|
|
625
|
-
});
|
|
626
|
-
console.log(`✅ Presigned URL generated for ${fileName}`);
|
|
627
|
-
|
|
628
|
-
const s3Url = `https://${EMBEDDED_CONFIG.S3_BUCKET_NAME}.s3.${EMBEDDED_CONFIG.AWS_REGION || 'us-east-1'}.amazonaws.com/${s3Key}`;
|
|
629
|
-
|
|
630
|
-
return {
|
|
631
|
-
key: s3Key,
|
|
632
|
-
url: s3Url,
|
|
633
|
-
presignedUrl: presignedUrl,
|
|
634
|
-
fileSize,
|
|
635
|
-
contentType
|
|
636
|
-
};
|
|
637
|
-
} catch (error) {
|
|
638
|
-
console.error(`❌ Failed to upload ${fileName} to S3:`, error.message);
|
|
639
|
-
console.error(`❌ Error details:`, error);
|
|
640
|
-
throw error;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
generateS3Key(runId, testId, fileName) {
|
|
645
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
646
|
-
const safeTestId = this.sanitizeForS3(testId);
|
|
647
|
-
const safeFileName = this.sanitizeForS3(fileName);
|
|
648
|
-
const ext = path.extname(fileName);
|
|
649
|
-
const baseName = path.basename(fileName, ext);
|
|
650
|
-
|
|
651
|
-
return `test-artifacts/${date}/${runId}/${safeTestId}/${safeFileName}${ext}`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
sanitizeForS3(value) {
|
|
655
|
-
return value
|
|
656
|
-
.replace(/\\/g, '/') // Convert Windows backslashes to forward slashes first
|
|
657
|
-
.replace(/[\/:*?"<>|]/g, '-')
|
|
658
|
-
.replace(/[\x00-\x1f\x7f]/g, '-')
|
|
659
|
-
.replace(/[^ -~]/g, '-')
|
|
660
|
-
.replace(/\s+/g, '-')
|
|
661
|
-
.replace(/[_]/g, '-')
|
|
662
|
-
.replace(/-+/g, '-')
|
|
663
|
-
.replace(/^-|-$/g, '')
|
|
664
|
-
.replace(/\/{2,}/g, '/'); // Remove multiple consecutive slashes
|
|
665
|
-
}
|
|
666
|
-
|
|
667
562
|
getContentType(ext) {
|
|
668
563
|
const contentTypes = {
|
|
669
564
|
'.mp4': 'video/mp4',
|
|
@@ -691,6 +586,62 @@ class TestLensReporter {
|
|
|
691
586
|
return 0;
|
|
692
587
|
}
|
|
693
588
|
}
|
|
589
|
+
|
|
590
|
+
async uploadArtifactToS3(filePath, testId, fileName) {
|
|
591
|
+
try {
|
|
592
|
+
// Use the public S3 upload API endpoint
|
|
593
|
+
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
594
|
+
const uploadUrl = `${baseUrl}/api/v1/artifacts/public/upload`;
|
|
595
|
+
|
|
596
|
+
// Prepare form data for multipart upload
|
|
597
|
+
const form = new FormData();
|
|
598
|
+
|
|
599
|
+
// Add required fields
|
|
600
|
+
form.append('apiKey', this.config.apiKey);
|
|
601
|
+
form.append('testRunId', this.runId);
|
|
602
|
+
form.append('testId', testId);
|
|
603
|
+
form.append('artifactType', this.getArtifactType(fileName));
|
|
604
|
+
form.append('file', fs.createReadStream(filePath), {
|
|
605
|
+
filename: fileName,
|
|
606
|
+
contentType: this.getContentType(fileName)
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
console.log(`📤 Uploading ${fileName} to TestLens S3 via API...`);
|
|
610
|
+
|
|
611
|
+
// Make the upload request
|
|
612
|
+
const response = await this.axiosInstance.post(uploadUrl, form, {
|
|
613
|
+
headers: {
|
|
614
|
+
...form.getHeaders(),
|
|
615
|
+
// Override content-type to let form-data set it
|
|
616
|
+
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
|
|
617
|
+
},
|
|
618
|
+
timeout: 60000 // 60 second timeout for uploads
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
if (response.status === 201 && response.data.success) {
|
|
622
|
+
const artifact = response.data.artifact;
|
|
623
|
+
console.log(`✅ S3 upload completed for ${fileName}`);
|
|
624
|
+
return {
|
|
625
|
+
key: artifact.s3Key,
|
|
626
|
+
url: artifact.s3Url,
|
|
627
|
+
presignedUrl: artifact.presignedUrl,
|
|
628
|
+
fileSize: artifact.fileSize,
|
|
629
|
+
contentType: artifact.contentType
|
|
630
|
+
};
|
|
631
|
+
} else {
|
|
632
|
+
throw new Error(`Upload failed: ${response.data.error || 'Unknown error'}`);
|
|
633
|
+
}
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.error(`❌ Failed to upload ${fileName} to TestLens S3 API:`, error.message);
|
|
636
|
+
throw error; // Re-throw to prevent fallback to local storage
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
getContentType(fileName) {
|
|
641
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
642
|
+
const mimeType = mime.getType(ext) || 'application/octet-stream';
|
|
643
|
+
return mimeType;
|
|
644
|
+
}
|
|
694
645
|
}
|
|
695
646
|
|
|
696
647
|
module.exports = TestLensReporter;
|
package/index.ts
CHANGED
|
@@ -6,20 +6,7 @@ import * as https from 'https';
|
|
|
6
6
|
import axios, { AxiosInstance } from 'axios';
|
|
7
7
|
import { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
|
-
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
10
|
-
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
11
9
|
import * as mime from 'mime';
|
|
12
|
-
|
|
13
|
-
// Embedded configuration from .env file (generated during build)
|
|
14
|
-
const EMBEDDED_CONFIG = {
|
|
15
|
-
"AWS_ACCESS_KEY_ID": "AKIA5QK76YZKWA2P5V4E",
|
|
16
|
-
"AWS_SECRET_ACCESS_KEY": "eX8lbMPMKBqTxmiNZO5dpLH5x6Do+cqnzhqJOOxJ",
|
|
17
|
-
"AWS_REGION": "eu-north-1",
|
|
18
|
-
"S3_BUCKET_NAME": "testlenss3",
|
|
19
|
-
"S3_ACL": "private",
|
|
20
|
-
"TEST_API_ENDPOINT": "https://testlens.qa-path.com/api/v1/webhook/playwright"
|
|
21
|
-
};
|
|
22
|
-
|
|
23
10
|
export interface TestLensReporterConfig {
|
|
24
11
|
/** TestLens API endpoint URL */
|
|
25
12
|
apiEndpoint?: string;
|
|
@@ -31,8 +18,6 @@ export interface TestLensReporterConfig {
|
|
|
31
18
|
enableGitInfo?: boolean;
|
|
32
19
|
/** Enable artifact processing */
|
|
33
20
|
enableArtifacts?: boolean;
|
|
34
|
-
/** Enable S3 upload for artifacts */
|
|
35
|
-
enableS3Upload?: boolean;
|
|
36
21
|
/** Batch size for API requests */
|
|
37
22
|
batchSize?: number;
|
|
38
23
|
/** Flush interval in milliseconds */
|
|
@@ -58,8 +43,6 @@ export interface TestLensReporterOptions {
|
|
|
58
43
|
enableGitInfo?: boolean;
|
|
59
44
|
/** Enable artifact processing */
|
|
60
45
|
enableArtifacts?: boolean;
|
|
61
|
-
/** Enable S3 upload for artifacts */
|
|
62
|
-
enableS3Upload?: boolean;
|
|
63
46
|
/** Batch size for API requests */
|
|
64
47
|
batchSize?: number;
|
|
65
48
|
/** Flush interval in milliseconds */
|
|
@@ -147,8 +130,6 @@ export interface SpecData {
|
|
|
147
130
|
export class TestLensReporter implements Reporter {
|
|
148
131
|
private config: Required<TestLensReporterConfig>;
|
|
149
132
|
private axiosInstance: AxiosInstance;
|
|
150
|
-
private s3Client: S3Client | null;
|
|
151
|
-
private s3Enabled: boolean;
|
|
152
133
|
private runId: string;
|
|
153
134
|
private runMetadata: RunMetadata;
|
|
154
135
|
private specMap: Map<string, SpecData>;
|
|
@@ -156,12 +137,11 @@ export class TestLensReporter implements Reporter {
|
|
|
156
137
|
|
|
157
138
|
constructor(options: TestLensReporterOptions) {
|
|
158
139
|
this.config = {
|
|
159
|
-
apiEndpoint:
|
|
140
|
+
apiEndpoint: options.apiEndpoint || '',
|
|
160
141
|
apiKey: options.apiKey, // API key must come from config file
|
|
161
142
|
enableRealTimeStream: options.enableRealTimeStream !== false,
|
|
162
143
|
enableGitInfo: options.enableGitInfo !== false,
|
|
163
144
|
enableArtifacts: options.enableArtifacts !== false,
|
|
164
|
-
enableS3Upload: options.enableS3Upload !== false,
|
|
165
145
|
batchSize: options.batchSize || 10,
|
|
166
146
|
flushInterval: options.flushInterval || 5000,
|
|
167
147
|
retryAttempts: options.retryAttempts || 3,
|
|
@@ -235,32 +215,6 @@ export class TestLensReporter implements Reporter {
|
|
|
235
215
|
}
|
|
236
216
|
);
|
|
237
217
|
|
|
238
|
-
// Initialize S3 client if S3 upload is enabled
|
|
239
|
-
this.s3Client = null;
|
|
240
|
-
this.s3Enabled = this.config.enableS3Upload && !!EMBEDDED_CONFIG.S3_BUCKET_NAME;
|
|
241
|
-
|
|
242
|
-
if (this.s3Enabled) {
|
|
243
|
-
try {
|
|
244
|
-
const s3Config: any = {
|
|
245
|
-
region: EMBEDDED_CONFIG.AWS_REGION || 'us-east-1',
|
|
246
|
-
maxAttempts: 3
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
if (EMBEDDED_CONFIG.AWS_ACCESS_KEY_ID && EMBEDDED_CONFIG.AWS_SECRET_ACCESS_KEY) {
|
|
250
|
-
s3Config.credentials = {
|
|
251
|
-
accessKeyId: EMBEDDED_CONFIG.AWS_ACCESS_KEY_ID,
|
|
252
|
-
secretAccessKey: EMBEDDED_CONFIG.AWS_SECRET_ACCESS_KEY
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
this.s3Client = new S3Client(s3Config);
|
|
257
|
-
console.log('✅ S3 client initialized for artifact uploads');
|
|
258
|
-
} catch (error) {
|
|
259
|
-
console.error('❌ Failed to initialize S3 client:', (error as Error).message);
|
|
260
|
-
this.s3Enabled = false;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
218
|
this.runId = randomUUID();
|
|
265
219
|
this.runMetadata = this.initializeRunMetadata();
|
|
266
220
|
this.specMap = new Map<string, SpecData>();
|
|
@@ -460,7 +414,7 @@ export class TestLensReporter implements Reporter {
|
|
|
460
414
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
461
415
|
const failedTests = Array.from(this.testMap.values()).filter(t => t.status === 'failed').length;
|
|
462
416
|
const skippedTests = Array.from(this.testMap.values()).filter(t => t.status === 'skipped').length;
|
|
463
|
-
const timedOutTests = Array.from(this.testMap.values()).filter(t => t.
|
|
417
|
+
const timedOutTests = Array.from(this.testMap.values()).filter(t => t.originalStatus === 'timedOut').length;
|
|
464
418
|
|
|
465
419
|
// Normalize run status - if there are timeouts, treat run as failed
|
|
466
420
|
const hasTimeouts = timedOutTests > 0;
|
|
@@ -509,12 +463,8 @@ export class TestLensReporter implements Reporter {
|
|
|
509
463
|
for (const attachment of attachments) {
|
|
510
464
|
if (attachment.path) {
|
|
511
465
|
try {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
// Upload to S3 if enabled
|
|
515
|
-
if (this.s3Enabled && this.s3Client) {
|
|
516
|
-
s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
517
|
-
}
|
|
466
|
+
// Upload to S3 first
|
|
467
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
518
468
|
|
|
519
469
|
const artifactData = {
|
|
520
470
|
testId,
|
|
@@ -522,13 +472,10 @@ export class TestLensReporter implements Reporter {
|
|
|
522
472
|
path: attachment.path,
|
|
523
473
|
name: attachment.name,
|
|
524
474
|
contentType: attachment.contentType,
|
|
525
|
-
fileSize:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
presignedUrl: s3Data.presignedUrl,
|
|
530
|
-
storageType: 's3'
|
|
531
|
-
})
|
|
475
|
+
fileSize: this.getFileSize(attachment.path),
|
|
476
|
+
storageType: 's3',
|
|
477
|
+
s3Key: s3Data.key,
|
|
478
|
+
s3Url: s3Data.url
|
|
532
479
|
};
|
|
533
480
|
|
|
534
481
|
// Send artifact data to API
|
|
@@ -539,7 +486,7 @@ export class TestLensReporter implements Reporter {
|
|
|
539
486
|
artifact: artifactData
|
|
540
487
|
});
|
|
541
488
|
|
|
542
|
-
console.log(`📎 Processed artifact: ${attachment.name}
|
|
489
|
+
console.log(`📎 Processed artifact: ${attachment.name} (uploaded to S3)`);
|
|
543
490
|
} catch (error) {
|
|
544
491
|
console.error(`❌ Failed to process artifact ${attachment.name}:`, (error as Error).message);
|
|
545
492
|
}
|
|
@@ -714,50 +661,59 @@ export class TestLensReporter implements Reporter {
|
|
|
714
661
|
return `${test.location.file}:${test.location.line}:${cleanTitle}`;
|
|
715
662
|
}
|
|
716
663
|
|
|
664
|
+
|
|
717
665
|
private async uploadArtifactToS3(filePath: string, testId: string, fileName: string): Promise<{ key: string; url: string; presignedUrl: string; fileSize: number; contentType: string }> {
|
|
718
666
|
try {
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
await this.s3Client!.send(new PutObjectCommand(uploadParams));
|
|
738
|
-
|
|
739
|
-
// Generate presigned URL for frontend access (expires in 7 days)
|
|
740
|
-
const getCommand = new GetObjectCommand({
|
|
741
|
-
Bucket: EMBEDDED_CONFIG.S3_BUCKET_NAME,
|
|
742
|
-
Key: s3Key
|
|
667
|
+
// Use the new public S3 upload API endpoint instead of direct AWS SDK calls
|
|
668
|
+
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
669
|
+
const uploadUrl = `${baseUrl}/api/v1/artifacts/public/upload`;
|
|
670
|
+
|
|
671
|
+
// Prepare form data for multipart upload
|
|
672
|
+
const FormData = require('form-data');
|
|
673
|
+
const form = new FormData();
|
|
674
|
+
|
|
675
|
+
// Add required fields
|
|
676
|
+
form.append('apiKey', this.config.apiKey);
|
|
677
|
+
form.append('testRunId', this.runId);
|
|
678
|
+
form.append('testId', testId);
|
|
679
|
+
form.append('artifactType', this.getArtifactType(fileName));
|
|
680
|
+
form.append('file', fs.createReadStream(filePath), {
|
|
681
|
+
filename: fileName,
|
|
682
|
+
contentType: this.getContentType(path.extname(filePath).toLowerCase())
|
|
743
683
|
});
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
684
|
+
|
|
685
|
+
console.log(`📤 Uploading ${fileName} to TestLens S3 via API...`);
|
|
686
|
+
|
|
687
|
+
// Make the upload request
|
|
688
|
+
const response = await this.axiosInstance.post(uploadUrl, form, {
|
|
689
|
+
headers: {
|
|
690
|
+
...form.getHeaders(),
|
|
691
|
+
// Override content-type to let form-data set it
|
|
692
|
+
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
|
|
693
|
+
},
|
|
694
|
+
timeout: 60000 // 60 second timeout for uploads
|
|
747
695
|
});
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
696
|
+
|
|
697
|
+
if (response.status === 201 && response.data.success) {
|
|
698
|
+
const artifact = response.data.artifact;
|
|
699
|
+
console.log(`✅ S3 upload completed for ${fileName}`);
|
|
700
|
+
|
|
701
|
+
// Extract S3 key from URL if not provided
|
|
702
|
+
const s3Key = artifact.s3Key || artifact.s3Url.split('/').slice(-1)[0];
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
key: s3Key,
|
|
706
|
+
url: artifact.s3Url,
|
|
707
|
+
presignedUrl: artifact.presignedUrl,
|
|
708
|
+
fileSize: artifact.fileSize,
|
|
709
|
+
contentType: artifact.contentType
|
|
710
|
+
};
|
|
711
|
+
} else {
|
|
712
|
+
throw new Error(`Upload failed: ${response.data.error || 'Unknown error'}`);
|
|
713
|
+
}
|
|
758
714
|
} catch (error) {
|
|
759
|
-
console.error(`❌ Failed to upload ${fileName} to S3:`, (error as Error).message);
|
|
760
|
-
throw error;
|
|
715
|
+
console.error(`❌ Failed to upload ${fileName} to TestLens S3 API:`, (error as Error).message);
|
|
716
|
+
throw error; // Re-throw to prevent fallback to local storage
|
|
761
717
|
}
|
|
762
718
|
}
|
|
763
719
|
|
package/lib/index.js
CHANGED
|
@@ -44,26 +44,14 @@ const fs = __importStar(require("fs"));
|
|
|
44
44
|
const https = __importStar(require("https"));
|
|
45
45
|
const axios_1 = __importDefault(require("axios"));
|
|
46
46
|
const child_process_1 = require("child_process");
|
|
47
|
-
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
48
|
-
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
|
|
49
|
-
// Embedded configuration from .env file (generated during build)
|
|
50
|
-
const EMBEDDED_CONFIG = {
|
|
51
|
-
"AWS_ACCESS_KEY_ID": "AKIA5QK76YZKWA2P5V4E",
|
|
52
|
-
"AWS_SECRET_ACCESS_KEY": "eX8lbMPMKBqTxmiNZO5dpLH5x6Do+cqnzhqJOOxJ",
|
|
53
|
-
"AWS_REGION": "eu-north-1",
|
|
54
|
-
"S3_BUCKET_NAME": "testlenss3",
|
|
55
|
-
"S3_ACL": "private",
|
|
56
|
-
"TEST_API_ENDPOINT": "https://testlens.qa-path.com/api/v1/webhook/playwright"
|
|
57
|
-
};
|
|
58
47
|
class TestLensReporter {
|
|
59
48
|
constructor(options) {
|
|
60
49
|
this.config = {
|
|
61
|
-
apiEndpoint:
|
|
50
|
+
apiEndpoint: options.apiEndpoint || '',
|
|
62
51
|
apiKey: options.apiKey, // API key must come from config file
|
|
63
52
|
enableRealTimeStream: options.enableRealTimeStream !== false,
|
|
64
53
|
enableGitInfo: options.enableGitInfo !== false,
|
|
65
54
|
enableArtifacts: options.enableArtifacts !== false,
|
|
66
|
-
enableS3Upload: options.enableS3Upload !== false,
|
|
67
55
|
batchSize: options.batchSize || 10,
|
|
68
56
|
flushInterval: options.flushInterval || 5000,
|
|
69
57
|
retryAttempts: options.retryAttempts || 3,
|
|
@@ -126,29 +114,6 @@ class TestLensReporter {
|
|
|
126
114
|
}
|
|
127
115
|
return Promise.reject(error);
|
|
128
116
|
});
|
|
129
|
-
// Initialize S3 client if S3 upload is enabled
|
|
130
|
-
this.s3Client = null;
|
|
131
|
-
this.s3Enabled = this.config.enableS3Upload && !!EMBEDDED_CONFIG.S3_BUCKET_NAME;
|
|
132
|
-
if (this.s3Enabled) {
|
|
133
|
-
try {
|
|
134
|
-
const s3Config = {
|
|
135
|
-
region: EMBEDDED_CONFIG.AWS_REGION || 'us-east-1',
|
|
136
|
-
maxAttempts: 3
|
|
137
|
-
};
|
|
138
|
-
if (EMBEDDED_CONFIG.AWS_ACCESS_KEY_ID && EMBEDDED_CONFIG.AWS_SECRET_ACCESS_KEY) {
|
|
139
|
-
s3Config.credentials = {
|
|
140
|
-
accessKeyId: EMBEDDED_CONFIG.AWS_ACCESS_KEY_ID,
|
|
141
|
-
secretAccessKey: EMBEDDED_CONFIG.AWS_SECRET_ACCESS_KEY
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
this.s3Client = new client_s3_1.S3Client(s3Config);
|
|
145
|
-
console.log('✅ S3 client initialized for artifact uploads');
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
console.error('❌ Failed to initialize S3 client:', error.message);
|
|
149
|
-
this.s3Enabled = false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
117
|
this.runId = (0, crypto_1.randomUUID)();
|
|
153
118
|
this.runMetadata = this.initializeRunMetadata();
|
|
154
119
|
this.specMap = new Map();
|
|
@@ -325,7 +290,7 @@ class TestLensReporter {
|
|
|
325
290
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
326
291
|
const failedTests = Array.from(this.testMap.values()).filter(t => t.status === 'failed').length;
|
|
327
292
|
const skippedTests = Array.from(this.testMap.values()).filter(t => t.status === 'skipped').length;
|
|
328
|
-
const timedOutTests = Array.from(this.testMap.values()).filter(t => t.
|
|
293
|
+
const timedOutTests = Array.from(this.testMap.values()).filter(t => t.originalStatus === 'timedOut').length;
|
|
329
294
|
// Normalize run status - if there are timeouts, treat run as failed
|
|
330
295
|
const hasTimeouts = timedOutTests > 0;
|
|
331
296
|
const normalizedRunStatus = this.normalizeRunStatus(result.status, hasTimeouts);
|
|
@@ -368,24 +333,18 @@ class TestLensReporter {
|
|
|
368
333
|
for (const attachment of attachments) {
|
|
369
334
|
if (attachment.path) {
|
|
370
335
|
try {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (this.s3Enabled && this.s3Client) {
|
|
374
|
-
s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
375
|
-
}
|
|
336
|
+
// Upload to S3 first
|
|
337
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
376
338
|
const artifactData = {
|
|
377
339
|
testId,
|
|
378
340
|
type: this.getArtifactType(attachment.name),
|
|
379
341
|
path: attachment.path,
|
|
380
342
|
name: attachment.name,
|
|
381
343
|
contentType: attachment.contentType,
|
|
382
|
-
fileSize:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
presignedUrl: s3Data.presignedUrl,
|
|
387
|
-
storageType: 's3'
|
|
388
|
-
})
|
|
344
|
+
fileSize: this.getFileSize(attachment.path),
|
|
345
|
+
storageType: 's3',
|
|
346
|
+
s3Key: s3Data.key,
|
|
347
|
+
s3Url: s3Data.url
|
|
389
348
|
};
|
|
390
349
|
// Send artifact data to API
|
|
391
350
|
await this.sendToApi({
|
|
@@ -394,7 +353,7 @@ class TestLensReporter {
|
|
|
394
353
|
timestamp: new Date().toISOString(),
|
|
395
354
|
artifact: artifactData
|
|
396
355
|
});
|
|
397
|
-
console.log(`📎 Processed artifact: ${attachment.name}
|
|
356
|
+
console.log(`📎 Processed artifact: ${attachment.name} (uploaded to S3)`);
|
|
398
357
|
}
|
|
399
358
|
catch (error) {
|
|
400
359
|
console.error(`❌ Failed to process artifact ${attachment.name}:`, error.message);
|
|
@@ -555,41 +514,51 @@ class TestLensReporter {
|
|
|
555
514
|
}
|
|
556
515
|
async uploadArtifactToS3(filePath, testId, fileName) {
|
|
557
516
|
try {
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
await this.s3Client.send(new client_s3_1.PutObjectCommand(uploadParams));
|
|
573
|
-
// Generate presigned URL for frontend access (expires in 7 days)
|
|
574
|
-
const getCommand = new client_s3_1.GetObjectCommand({
|
|
575
|
-
Bucket: EMBEDDED_CONFIG.S3_BUCKET_NAME,
|
|
576
|
-
Key: s3Key
|
|
517
|
+
// Use the new public S3 upload API endpoint instead of direct AWS SDK calls
|
|
518
|
+
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
519
|
+
const uploadUrl = `${baseUrl}/api/v1/artifacts/public/upload`;
|
|
520
|
+
// Prepare form data for multipart upload
|
|
521
|
+
const FormData = require('form-data');
|
|
522
|
+
const form = new FormData();
|
|
523
|
+
// Add required fields
|
|
524
|
+
form.append('apiKey', this.config.apiKey);
|
|
525
|
+
form.append('testRunId', this.runId);
|
|
526
|
+
form.append('testId', testId);
|
|
527
|
+
form.append('artifactType', this.getArtifactType(fileName));
|
|
528
|
+
form.append('file', fs.createReadStream(filePath), {
|
|
529
|
+
filename: fileName,
|
|
530
|
+
contentType: this.getContentType(path.extname(filePath).toLowerCase())
|
|
577
531
|
});
|
|
578
|
-
|
|
579
|
-
|
|
532
|
+
console.log(`📤 Uploading ${fileName} to TestLens S3 via API...`);
|
|
533
|
+
// Make the upload request
|
|
534
|
+
const response = await this.axiosInstance.post(uploadUrl, form, {
|
|
535
|
+
headers: {
|
|
536
|
+
...form.getHeaders(),
|
|
537
|
+
// Override content-type to let form-data set it
|
|
538
|
+
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
|
|
539
|
+
},
|
|
540
|
+
timeout: 60000 // 60 second timeout for uploads
|
|
580
541
|
});
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
542
|
+
if (response.status === 201 && response.data.success) {
|
|
543
|
+
const artifact = response.data.artifact;
|
|
544
|
+
console.log(`✅ S3 upload completed for ${fileName}`);
|
|
545
|
+
// Extract S3 key from URL if not provided
|
|
546
|
+
const s3Key = artifact.s3Key || artifact.s3Url.split('/').slice(-1)[0];
|
|
547
|
+
return {
|
|
548
|
+
key: s3Key,
|
|
549
|
+
url: artifact.s3Url,
|
|
550
|
+
presignedUrl: artifact.presignedUrl,
|
|
551
|
+
fileSize: artifact.fileSize,
|
|
552
|
+
contentType: artifact.contentType
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
throw new Error(`Upload failed: ${response.data.error || 'Unknown error'}`);
|
|
557
|
+
}
|
|
589
558
|
}
|
|
590
559
|
catch (error) {
|
|
591
|
-
console.error(`❌ Failed to upload ${fileName} to S3:`, error.message);
|
|
592
|
-
throw error;
|
|
560
|
+
console.error(`❌ Failed to upload ${fileName} to TestLens S3 API:`, error.message);
|
|
561
|
+
throw error; // Re-throw to prevent fallback to local storage
|
|
593
562
|
}
|
|
594
563
|
}
|
|
595
564
|
getContentType(ext) {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testlens-playwright-reporter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Universal Playwright reporter for TestLens - works with both TypeScript and JavaScript projects",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.js",
|
|
9
|
-
"index.d.ts",
|
|
10
|
-
"index.ts",
|
|
9
|
+
"index.d.ts",
|
|
10
|
+
"index.ts",
|
|
11
11
|
"lib/",
|
|
12
12
|
"README.md",
|
|
13
13
|
"CHANGELOG.md"
|
package/README.md
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
````markdown
|
|
2
|
-
# TestLens Playwright Reporter
|
|
3
|
-
|
|
4
|
-
A modern, feature-rich Playwright reporter that works with both TypeScript and JavaScript projects, integrating seamlessly with your TestLens dashboard.
|
|
5
|
-
|
|
6
|
-
## 🚀 Features
|
|
7
|
-
|
|
8
|
-
- ✅ **Universal Compatibility** - Works with both TypeScript and JavaScript Playwright projects
|
|
9
|
-
- 🔄 **Real-time Streaming** - Live test results streaming to TestLens dashboard
|
|
10
|
-
- 📊 **Comprehensive Reporting** - Test results, artifacts, and detailed metrics
|
|
11
|
-
- 🌿 **Git Integration** - Automatic collection of Git information (branch, commit, author, etc.)
|
|
12
|
-
- 📝 **Code Block Extraction** - Intelligent extraction of test code for better debugging
|
|
13
|
-
- 🔄 **Retry Logic** - Robust error handling and retry mechanisms
|
|
14
|
-
- 🎯 **Artifact Support** - Screenshots, videos, traces, and custom attachments with S3 storage
|
|
15
|
-
- ☁️ **S3 Integration** - Automatic upload of artifacts to Amazon S3 for persistent storage
|
|
16
|
-
|
|
17
|
-
## 📦 Installation
|
|
18
|
-
|
|
19
|
-
### Option 1: npm install (Recommended)
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
# Install the TestLens Playwright Reporter
|
|
23
|
-
npm install @testlens/playwright-reporter --save-dev
|
|
24
|
-
|
|
25
|
-
# Install the TestLens Playwright Reporter uisng the file locally
|
|
26
|
-
npm install --save-dev ./testlens-playwright-reporter-x.y.z.tgz
|
|
27
|
-
|
|
28
|
-
# Or using yarn
|
|
29
|
-
yarn add @testlens/playwright-reporter --dev
|
|
30
|
-
|
|
31
|
-
# Or using pnpm
|
|
32
|
-
pnpm add -D @testlens/playwright-reporter
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Option 2: Use from centralized location
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
# Clone or copy the testlens-reporter to a central location
|
|
39
|
-
git clone https://github.com/alternative-path/testlens-reporter.git D:\Experiment\AP\testlens-reporter
|
|
40
|
-
|
|
41
|
-
# Install dependencies
|
|
42
|
-
cd D:\Experiment\AP\testlens-reporter
|
|
43
|
-
npm install
|
|
44
|
-
|
|
45
|
-
# Reference from your Playwright projects using absolute path or environment variable
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## ⚙️ Configuration
|
|
49
|
-
|
|
50
|
-
### 1. Environment Variables
|
|
51
|
-
|
|
52
|
-
No environment file is required! All configuration is embedded in the package. However, you can still override settings if needed by creating a `.env` file:
|
|
53
|
-
|
|
54
|
-
```env
|
|
55
|
-
# Optional: TestLens API endpoint (defaults to embedded configuration)
|
|
56
|
-
TEST_API_ENDPOINT=http://localhost:3001/api/v1/webhook/playwright
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Note**:
|
|
60
|
-
- S3 configuration is pre-configured and embedded in the package. No additional S3 setup is required.
|
|
61
|
-
- API key must be provided directly in your Playwright configuration file, not in environment variables.
|
|
62
|
-
|
|
63
|
-
### 2. Playwright Configuration
|
|
64
|
-
|
|
65
|
-
#### For JavaScript projects (`playwright.config.js`):
|
|
66
|
-
|
|
67
|
-
```javascript
|
|
68
|
-
// @ts-check
|
|
69
|
-
const { defineConfig } = require('@playwright/test');
|
|
70
|
-
|
|
71
|
-
module.exports = defineConfig({
|
|
72
|
-
// ... your existing config
|
|
73
|
-
|
|
74
|
-
reporter: [
|
|
75
|
-
['list'], // Keep the default console reporter
|
|
76
|
-
['@testlens/playwright-reporter', {
|
|
77
|
-
// API key is required - provide it directly in config
|
|
78
|
-
apiKey: 'your-api-key-here', // Required: Your TestLens API key
|
|
79
|
-
// Optional overrides (these have embedded defaults)
|
|
80
|
-
apiEndpoint: 'http://localhost:3001/api/v1/webhook/playwright',
|
|
81
|
-
enableRealTimeStream: true,
|
|
82
|
-
enableGitInfo: true,
|
|
83
|
-
enableArtifacts: true,
|
|
84
|
-
enableS3Upload: true, // S3 configuration is embedded
|
|
85
|
-
retryAttempts: 3,
|
|
86
|
-
timeout: 30000
|
|
87
|
-
}]
|
|
88
|
-
],
|
|
89
|
-
|
|
90
|
-
// ... rest of your config
|
|
91
|
-
});
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
#### For TypeScript projects (`playwright.config.ts`):
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
import { defineConfig } from '@playwright/test';
|
|
98
|
-
|
|
99
|
-
export default defineConfig({
|
|
100
|
-
// ... your existing config
|
|
101
|
-
|
|
102
|
-
reporter: [
|
|
103
|
-
['list'], // Keep the default console reporter
|
|
104
|
-
['@testlens/playwright-reporter', {
|
|
105
|
-
// API key is required - provide it directly in config
|
|
106
|
-
apiKey: 'your-api-key-here', // Required: Your TestLens API key
|
|
107
|
-
// Optional overrides (these have embedded defaults)
|
|
108
|
-
apiEndpoint: 'http://localhost:3001/api/v1/webhook/playwright',
|
|
109
|
-
enableRealTimeStream: true,
|
|
110
|
-
enableGitInfo: true,
|
|
111
|
-
enableArtifacts: true,
|
|
112
|
-
enableS3Upload: true, // S3 configuration is embedded
|
|
113
|
-
retryAttempts: 3,
|
|
114
|
-
timeout: 30000
|
|
115
|
-
}]
|
|
116
|
-
],
|
|
117
|
-
|
|
118
|
-
// ... rest of your config
|
|
119
|
-
});
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## 🔧 Configuration Options
|
|
123
|
-
|
|
124
|
-
| Option | Type | Default | Description |
|
|
125
|
-
|--------|------|---------|-------------|
|
|
126
|
-
| `apiEndpoint` | string | `process.env.TEST_API_ENDPOINT` | **Required.** TestLens API endpoint URL |
|
|
127
|
-
| `apiKey` | string | *none* | **Required.** API key for authentication - must be provided in config |
|
|
128
|
-
| `enableRealTimeStream` | boolean | `true` | Enable real-time streaming of test events |
|
|
129
|
-
| `enableGitInfo` | boolean | `true` | Enable Git information collection |
|
|
130
|
-
| `enableArtifacts` | boolean | `true` | Enable artifact processing (screenshots, videos, etc.) |
|
|
131
|
-
| `enableS3Upload` | boolean | `true` | Enable S3 upload for artifacts (S3 configuration is embedded in the package) |
|
|
132
|
-
| `batchSize` | number | `10` | Batch size for API requests |
|
|
133
|
-
| `flushInterval` | number | `5000` | Flush interval in milliseconds |
|
|
134
|
-
| `retryAttempts` | number | `3` | Number of retry attempts for failed API calls |
|
|
135
|
-
| `timeout` | number | `30000` | Request timeout in milliseconds |
|
|
136
|
-
|
|
137
|
-
## 🚦 Usage
|
|
138
|
-
|
|
139
|
-
Once configured, simply run your Playwright tests as usual:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
# Run all tests
|
|
143
|
-
npx playwright test
|
|
144
|
-
|
|
145
|
-
# Run specific test file
|
|
146
|
-
npx playwright test tests/login.spec.ts
|
|
147
|
-
|
|
148
|
-
# Run tests in headed mode
|
|
149
|
-
npx playwright test --headed
|
|
150
|
-
|
|
151
|
-
# Run tests with specific reporter only
|
|
152
|
-
npx playwright test --reporter=./testlens-reporter
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## 📊 What Gets Reported
|
|
156
|
-
|
|
157
|
-
### Test Execution Data
|
|
158
|
-
- ✅ Test results (passed/failed/skipped)
|
|
159
|
-
- ⏱️ Test duration and timing
|
|
160
|
-
- 🔄 Retry attempts and status
|
|
161
|
-
- 📝 Error messages and stack traces
|
|
162
|
-
- 🏷️ Test annotations and tags
|
|
163
|
-
|
|
164
|
-
### System Information
|
|
165
|
-
- 🌿 Git branch, commit, author, and message
|
|
166
|
-
- 💻 OS and Node.js version
|
|
167
|
-
- 🎭 Playwright version
|
|
168
|
-
- 🌍 Environment information
|
|
169
|
-
|
|
170
|
-
### Artifacts
|
|
171
|
-
- 📸 Screenshots
|
|
172
|
-
- 🎥 Videos
|
|
173
|
-
- 🔍 Traces
|
|
174
|
-
- 📎 Custom attachments
|
|
175
|
-
- ☁️ **S3 Storage** - Automatic upload to Amazon S3 with presigned URLs for access
|
|
176
|
-
|
|
177
|
-
### Code Blocks
|
|
178
|
-
- 🧪 Extracted test code blocks
|
|
179
|
-
- 📝 Test structure and context
|
|
180
|
-
- 🔍 Searchable test content
|
|
181
|
-
|
|
182
|
-
## 🐛 Troubleshooting
|
|
183
|
-
|
|
184
|
-
### Common Issues
|
|
185
|
-
|
|
186
|
-
#### 1. "TEST_API_ENDPOINT is required" error
|
|
187
|
-
Make sure you have set the `TEST_API_ENDPOINT` environment variable or passed it as a configuration option.
|
|
188
|
-
|
|
189
|
-
#### 2. Connection errors
|
|
190
|
-
- Verify that your TestLens backend is running and accessible
|
|
191
|
-
- Check firewall settings and network connectivity
|
|
192
|
-
- Ensure the API endpoint URL is correct
|
|
193
|
-
|
|
194
|
-
#### 3. Git information not collected
|
|
195
|
-
- Ensure you're running tests from within a Git repository
|
|
196
|
-
- Check that Git is installed and accessible from the command line
|
|
197
|
-
- Git information collection can be disabled with `enableGitInfo: false`
|
|
198
|
-
|
|
199
|
-
### Debug Mode
|
|
200
|
-
|
|
201
|
-
Enable debug logging by setting environment variable:
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
DEBUG=testlens:* npx playwright test
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
## 🤝 Integration with TestLens Dashboard
|
|
208
|
-
|
|
209
|
-
This reporter integrates with your TestLens dashboard to provide:
|
|
210
|
-
|
|
211
|
-
- 📊 **Real-time Test Results** - See tests as they run
|
|
212
|
-
- 📈 **Historical Trends** - Track test performance over time
|
|
213
|
-
- 🔍 **Code Block Visualization** - View actual test code in the dashboard
|
|
214
|
-
- 🌿 **Git Integration** - Link test results to commits and branches
|
|
215
|
-
- 📱 **Artifact Gallery** - Browse screenshots, videos, and traces
|
|
216
|
-
|
|
217
|
-
## 📄 License
|
|
218
|
-
|
|
219
|
-
MIT License - see LICENSE file for details.
|
|
220
|
-
|
|
221
|
-
## 🤝 Contributing
|
|
222
|
-
|
|
223
|
-
1. Fork the repository
|
|
224
|
-
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
225
|
-
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
226
|
-
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
227
|
-
5. Open a Pull Request
|
|
228
|
-
|
|
229
|
-
## 📞 Support
|
|
230
|
-
|
|
231
|
-
For issues and questions:
|
|
232
|
-
- 🐛 [Report bugs](https://github.com/alternative-path/testlens/issues)
|
|
233
|
-
- 💬 [Discussions](https://github.com/alternative-path/testlens/discussions)
|
|
234
|
-
- 📧 [Email support](mailto:support@testlens.io)
|
|
235
|
-
|
|
236
|
-
````
|
|
237
|
-
node build-embed-env.js
|
|
238
|
-
npm pack
|