testlens-playwright-reporter 0.1.0 → 0.1.2
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 +69 -120
- package/lib/index.js +62 -99
- package/package.json +5 -4
- 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
|
@@ -4,22 +4,10 @@ import * as path from 'path';
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as https from 'https';
|
|
6
6
|
import axios, { AxiosInstance } from 'axios';
|
|
7
|
-
import { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
|
|
7
|
+
import type { 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
|
-
|
|
10
|
+
import FormData from 'form-data';
|
|
23
11
|
export interface TestLensReporterConfig {
|
|
24
12
|
/** TestLens API endpoint URL */
|
|
25
13
|
apiEndpoint?: string;
|
|
@@ -31,8 +19,6 @@ export interface TestLensReporterConfig {
|
|
|
31
19
|
enableGitInfo?: boolean;
|
|
32
20
|
/** Enable artifact processing */
|
|
33
21
|
enableArtifacts?: boolean;
|
|
34
|
-
/** Enable S3 upload for artifacts */
|
|
35
|
-
enableS3Upload?: boolean;
|
|
36
22
|
/** Batch size for API requests */
|
|
37
23
|
batchSize?: number;
|
|
38
24
|
/** Flush interval in milliseconds */
|
|
@@ -58,8 +44,6 @@ export interface TestLensReporterOptions {
|
|
|
58
44
|
enableGitInfo?: boolean;
|
|
59
45
|
/** Enable artifact processing */
|
|
60
46
|
enableArtifacts?: boolean;
|
|
61
|
-
/** Enable S3 upload for artifacts */
|
|
62
|
-
enableS3Upload?: boolean;
|
|
63
47
|
/** Batch size for API requests */
|
|
64
48
|
batchSize?: number;
|
|
65
49
|
/** Flush interval in milliseconds */
|
|
@@ -147,8 +131,6 @@ export interface SpecData {
|
|
|
147
131
|
export class TestLensReporter implements Reporter {
|
|
148
132
|
private config: Required<TestLensReporterConfig>;
|
|
149
133
|
private axiosInstance: AxiosInstance;
|
|
150
|
-
private s3Client: S3Client | null;
|
|
151
|
-
private s3Enabled: boolean;
|
|
152
134
|
private runId: string;
|
|
153
135
|
private runMetadata: RunMetadata;
|
|
154
136
|
private specMap: Map<string, SpecData>;
|
|
@@ -156,12 +138,11 @@ export class TestLensReporter implements Reporter {
|
|
|
156
138
|
|
|
157
139
|
constructor(options: TestLensReporterOptions) {
|
|
158
140
|
this.config = {
|
|
159
|
-
apiEndpoint:
|
|
141
|
+
apiEndpoint: options.apiEndpoint || '',
|
|
160
142
|
apiKey: options.apiKey, // API key must come from config file
|
|
161
143
|
enableRealTimeStream: options.enableRealTimeStream !== false,
|
|
162
144
|
enableGitInfo: options.enableGitInfo !== false,
|
|
163
145
|
enableArtifacts: options.enableArtifacts !== false,
|
|
164
|
-
enableS3Upload: options.enableS3Upload !== false,
|
|
165
146
|
batchSize: options.batchSize || 10,
|
|
166
147
|
flushInterval: options.flushInterval || 5000,
|
|
167
148
|
retryAttempts: options.retryAttempts || 3,
|
|
@@ -235,32 +216,6 @@ export class TestLensReporter implements Reporter {
|
|
|
235
216
|
}
|
|
236
217
|
);
|
|
237
218
|
|
|
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
219
|
this.runId = randomUUID();
|
|
265
220
|
this.runMetadata = this.initializeRunMetadata();
|
|
266
221
|
this.specMap = new Map<string, SpecData>();
|
|
@@ -460,7 +415,7 @@ export class TestLensReporter implements Reporter {
|
|
|
460
415
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
461
416
|
const failedTests = Array.from(this.testMap.values()).filter(t => t.status === 'failed').length;
|
|
462
417
|
const skippedTests = Array.from(this.testMap.values()).filter(t => t.status === 'skipped').length;
|
|
463
|
-
const timedOutTests = Array.from(this.testMap.values()).filter(t => t.
|
|
418
|
+
const timedOutTests = Array.from(this.testMap.values()).filter(t => t.originalStatus === 'timedOut').length;
|
|
464
419
|
|
|
465
420
|
// Normalize run status - if there are timeouts, treat run as failed
|
|
466
421
|
const hasTimeouts = timedOutTests > 0;
|
|
@@ -488,15 +443,20 @@ export class TestLensReporter implements Reporter {
|
|
|
488
443
|
|
|
489
444
|
private async sendToApi(payload: any): Promise<void> {
|
|
490
445
|
try {
|
|
446
|
+
console.log(`📤 Sending ${payload.type} event to ${this.config.apiEndpoint}`);
|
|
491
447
|
const response = await this.axiosInstance.post('', payload);
|
|
492
448
|
if (this.config.enableRealTimeStream) {
|
|
493
|
-
console.log(`✅ Sent ${payload.type} event to TestLens`);
|
|
449
|
+
console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
494
450
|
}
|
|
495
451
|
} catch (error: any) {
|
|
496
452
|
console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
|
|
497
453
|
message: error?.message || 'Unknown error',
|
|
498
454
|
status: error?.response?.status,
|
|
499
|
-
|
|
455
|
+
statusText: error?.response?.statusText,
|
|
456
|
+
data: error?.response?.data,
|
|
457
|
+
code: error?.code,
|
|
458
|
+
url: error?.config?.url,
|
|
459
|
+
method: error?.config?.method
|
|
500
460
|
});
|
|
501
461
|
|
|
502
462
|
// Don't throw error to avoid breaking test execution
|
|
@@ -509,12 +469,8 @@ export class TestLensReporter implements Reporter {
|
|
|
509
469
|
for (const attachment of attachments) {
|
|
510
470
|
if (attachment.path) {
|
|
511
471
|
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
|
-
}
|
|
472
|
+
// Upload to S3 first
|
|
473
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
518
474
|
|
|
519
475
|
const artifactData = {
|
|
520
476
|
testId,
|
|
@@ -522,13 +478,10 @@ export class TestLensReporter implements Reporter {
|
|
|
522
478
|
path: attachment.path,
|
|
523
479
|
name: attachment.name,
|
|
524
480
|
contentType: attachment.contentType,
|
|
525
|
-
fileSize:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
presignedUrl: s3Data.presignedUrl,
|
|
530
|
-
storageType: 's3'
|
|
531
|
-
})
|
|
481
|
+
fileSize: this.getFileSize(attachment.path),
|
|
482
|
+
storageType: 's3',
|
|
483
|
+
s3Key: s3Data.key,
|
|
484
|
+
s3Url: s3Data.url
|
|
532
485
|
};
|
|
533
486
|
|
|
534
487
|
// Send artifact data to API
|
|
@@ -539,7 +492,7 @@ export class TestLensReporter implements Reporter {
|
|
|
539
492
|
artifact: artifactData
|
|
540
493
|
});
|
|
541
494
|
|
|
542
|
-
console.log(`📎 Processed artifact: ${attachment.name}
|
|
495
|
+
console.log(`📎 Processed artifact: ${attachment.name} (uploaded to S3)`);
|
|
543
496
|
} catch (error) {
|
|
544
497
|
console.error(`❌ Failed to process artifact ${attachment.name}:`, (error as Error).message);
|
|
545
498
|
}
|
|
@@ -714,69 +667,65 @@ export class TestLensReporter implements Reporter {
|
|
|
714
667
|
return `${test.location.file}:${test.location.line}:${cleanTitle}`;
|
|
715
668
|
}
|
|
716
669
|
|
|
670
|
+
|
|
717
671
|
private async uploadArtifactToS3(filePath: string, testId: string, fileName: string): Promise<{ key: string; url: string; presignedUrl: string; fileSize: number; contentType: string }> {
|
|
718
672
|
try {
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
ACL: (EMBEDDED_CONFIG.S3_ACL as 'private' | 'public-read' | undefined) || 'private',
|
|
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
|
|
673
|
+
// Use the new public S3 upload API endpoint instead of direct AWS SDK calls
|
|
674
|
+
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
675
|
+
const uploadUrl = `${baseUrl}/api/v1/artifacts/public/upload`;
|
|
676
|
+
|
|
677
|
+
// Prepare form data for multipart upload
|
|
678
|
+
const form = new FormData();
|
|
679
|
+
|
|
680
|
+
// Add required fields
|
|
681
|
+
form.append('apiKey', this.config.apiKey);
|
|
682
|
+
form.append('testRunId', this.runId);
|
|
683
|
+
form.append('testId', testId);
|
|
684
|
+
form.append('artifactType', this.getArtifactType(fileName));
|
|
685
|
+
form.append('file', fs.createReadStream(filePath), {
|
|
686
|
+
filename: fileName,
|
|
687
|
+
contentType: this.getContentType(fileName)
|
|
743
688
|
});
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
689
|
+
|
|
690
|
+
console.log(`📤 Uploading ${fileName} to TestLens S3 via API...`);
|
|
691
|
+
|
|
692
|
+
// Make the upload request
|
|
693
|
+
const response = await this.axiosInstance.post(uploadUrl, form, {
|
|
694
|
+
headers: {
|
|
695
|
+
...form.getHeaders(),
|
|
696
|
+
// Override content-type to let form-data set it
|
|
697
|
+
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
|
|
698
|
+
},
|
|
699
|
+
timeout: 60000 // 60 second timeout for uploads
|
|
747
700
|
});
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
701
|
+
|
|
702
|
+
if (response.status === 201 && response.data.success) {
|
|
703
|
+
const artifact = response.data.artifact;
|
|
704
|
+
console.log(`✅ S3 upload completed for ${fileName}`);
|
|
705
|
+
|
|
706
|
+
// Extract S3 key from URL if not provided
|
|
707
|
+
const s3Key = artifact.s3Key || artifact.s3Url.split('/').slice(-1)[0];
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
key: s3Key,
|
|
711
|
+
url: artifact.s3Url,
|
|
712
|
+
presignedUrl: artifact.presignedUrl,
|
|
713
|
+
fileSize: artifact.fileSize,
|
|
714
|
+
contentType: artifact.contentType
|
|
715
|
+
};
|
|
716
|
+
} else {
|
|
717
|
+
throw new Error(`Upload failed: ${response.data.error || 'Unknown error'}`);
|
|
718
|
+
}
|
|
758
719
|
} catch (error) {
|
|
759
|
-
console.error(`❌ Failed to upload ${fileName} to S3:`, (error as Error).message);
|
|
760
|
-
throw error;
|
|
720
|
+
console.error(`❌ Failed to upload ${fileName} to TestLens S3 API:`, (error as Error).message);
|
|
721
|
+
throw error; // Re-throw to prevent fallback to local storage
|
|
761
722
|
}
|
|
762
723
|
}
|
|
763
724
|
|
|
764
|
-
private getContentType(
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
'.png': 'image/png',
|
|
769
|
-
'.jpg': 'image/jpeg',
|
|
770
|
-
'.jpeg': 'image/jpeg',
|
|
771
|
-
'.gif': 'image/gif',
|
|
772
|
-
'.json': 'application/json',
|
|
773
|
-
'.txt': 'text/plain',
|
|
774
|
-
'.html': 'text/html',
|
|
775
|
-
'.xml': 'application/xml',
|
|
776
|
-
'.zip': 'application/zip',
|
|
777
|
-
'.pdf': 'application/pdf'
|
|
778
|
-
};
|
|
779
|
-
return contentTypes[ext] || 'application/octet-stream';
|
|
725
|
+
private getContentType(fileName: string): string {
|
|
726
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
727
|
+
const mimeType = mime.default.getType(ext) || 'application/octet-stream';
|
|
728
|
+
return mimeType;
|
|
780
729
|
}
|
|
781
730
|
|
|
782
731
|
private generateS3Key(runId: string, testId: string, fileName: string): string {
|
package/lib/index.js
CHANGED
|
@@ -44,26 +44,16 @@ 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
|
|
48
|
-
const
|
|
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
|
-
};
|
|
47
|
+
const mime = __importStar(require("mime"));
|
|
48
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
58
49
|
class TestLensReporter {
|
|
59
50
|
constructor(options) {
|
|
60
51
|
this.config = {
|
|
61
|
-
apiEndpoint:
|
|
52
|
+
apiEndpoint: options.apiEndpoint || '',
|
|
62
53
|
apiKey: options.apiKey, // API key must come from config file
|
|
63
54
|
enableRealTimeStream: options.enableRealTimeStream !== false,
|
|
64
55
|
enableGitInfo: options.enableGitInfo !== false,
|
|
65
56
|
enableArtifacts: options.enableArtifacts !== false,
|
|
66
|
-
enableS3Upload: options.enableS3Upload !== false,
|
|
67
57
|
batchSize: options.batchSize || 10,
|
|
68
58
|
flushInterval: options.flushInterval || 5000,
|
|
69
59
|
retryAttempts: options.retryAttempts || 3,
|
|
@@ -126,29 +116,6 @@ class TestLensReporter {
|
|
|
126
116
|
}
|
|
127
117
|
return Promise.reject(error);
|
|
128
118
|
});
|
|
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
119
|
this.runId = (0, crypto_1.randomUUID)();
|
|
153
120
|
this.runMetadata = this.initializeRunMetadata();
|
|
154
121
|
this.specMap = new Map();
|
|
@@ -325,7 +292,7 @@ class TestLensReporter {
|
|
|
325
292
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
326
293
|
const failedTests = Array.from(this.testMap.values()).filter(t => t.status === 'failed').length;
|
|
327
294
|
const skippedTests = Array.from(this.testMap.values()).filter(t => t.status === 'skipped').length;
|
|
328
|
-
const timedOutTests = Array.from(this.testMap.values()).filter(t => t.
|
|
295
|
+
const timedOutTests = Array.from(this.testMap.values()).filter(t => t.originalStatus === 'timedOut').length;
|
|
329
296
|
// Normalize run status - if there are timeouts, treat run as failed
|
|
330
297
|
const hasTimeouts = timedOutTests > 0;
|
|
331
298
|
const normalizedRunStatus = this.normalizeRunStatus(result.status, hasTimeouts);
|
|
@@ -349,16 +316,21 @@ class TestLensReporter {
|
|
|
349
316
|
}
|
|
350
317
|
async sendToApi(payload) {
|
|
351
318
|
try {
|
|
319
|
+
console.log(`📤 Sending ${payload.type} event to ${this.config.apiEndpoint}`);
|
|
352
320
|
const response = await this.axiosInstance.post('', payload);
|
|
353
321
|
if (this.config.enableRealTimeStream) {
|
|
354
|
-
console.log(`✅ Sent ${payload.type} event to TestLens`);
|
|
322
|
+
console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
355
323
|
}
|
|
356
324
|
}
|
|
357
325
|
catch (error) {
|
|
358
326
|
console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
|
|
359
327
|
message: error?.message || 'Unknown error',
|
|
360
328
|
status: error?.response?.status,
|
|
361
|
-
|
|
329
|
+
statusText: error?.response?.statusText,
|
|
330
|
+
data: error?.response?.data,
|
|
331
|
+
code: error?.code,
|
|
332
|
+
url: error?.config?.url,
|
|
333
|
+
method: error?.config?.method
|
|
362
334
|
});
|
|
363
335
|
// Don't throw error to avoid breaking test execution
|
|
364
336
|
}
|
|
@@ -368,24 +340,18 @@ class TestLensReporter {
|
|
|
368
340
|
for (const attachment of attachments) {
|
|
369
341
|
if (attachment.path) {
|
|
370
342
|
try {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (this.s3Enabled && this.s3Client) {
|
|
374
|
-
s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
375
|
-
}
|
|
343
|
+
// Upload to S3 first
|
|
344
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
376
345
|
const artifactData = {
|
|
377
346
|
testId,
|
|
378
347
|
type: this.getArtifactType(attachment.name),
|
|
379
348
|
path: attachment.path,
|
|
380
349
|
name: attachment.name,
|
|
381
350
|
contentType: attachment.contentType,
|
|
382
|
-
fileSize:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
presignedUrl: s3Data.presignedUrl,
|
|
387
|
-
storageType: 's3'
|
|
388
|
-
})
|
|
351
|
+
fileSize: this.getFileSize(attachment.path),
|
|
352
|
+
storageType: 's3',
|
|
353
|
+
s3Key: s3Data.key,
|
|
354
|
+
s3Url: s3Data.url
|
|
389
355
|
};
|
|
390
356
|
// Send artifact data to API
|
|
391
357
|
await this.sendToApi({
|
|
@@ -394,7 +360,7 @@ class TestLensReporter {
|
|
|
394
360
|
timestamp: new Date().toISOString(),
|
|
395
361
|
artifact: artifactData
|
|
396
362
|
});
|
|
397
|
-
console.log(`📎 Processed artifact: ${attachment.name}
|
|
363
|
+
console.log(`📎 Processed artifact: ${attachment.name} (uploaded to S3)`);
|
|
398
364
|
}
|
|
399
365
|
catch (error) {
|
|
400
366
|
console.error(`❌ Failed to process artifact ${attachment.name}:`, error.message);
|
|
@@ -555,59 +521,56 @@ class TestLensReporter {
|
|
|
555
521
|
}
|
|
556
522
|
async uploadArtifactToS3(filePath, testId, fileName) {
|
|
557
523
|
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
|
|
524
|
+
// Use the new public S3 upload API endpoint instead of direct AWS SDK calls
|
|
525
|
+
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
526
|
+
const uploadUrl = `${baseUrl}/api/v1/artifacts/public/upload`;
|
|
527
|
+
// Prepare form data for multipart upload
|
|
528
|
+
const form = new form_data_1.default();
|
|
529
|
+
// Add required fields
|
|
530
|
+
form.append('apiKey', this.config.apiKey);
|
|
531
|
+
form.append('testRunId', this.runId);
|
|
532
|
+
form.append('testId', testId);
|
|
533
|
+
form.append('artifactType', this.getArtifactType(fileName));
|
|
534
|
+
form.append('file', fs.createReadStream(filePath), {
|
|
535
|
+
filename: fileName,
|
|
536
|
+
contentType: this.getContentType(fileName)
|
|
577
537
|
});
|
|
578
|
-
|
|
579
|
-
|
|
538
|
+
console.log(`📤 Uploading ${fileName} to TestLens S3 via API...`);
|
|
539
|
+
// Make the upload request
|
|
540
|
+
const response = await this.axiosInstance.post(uploadUrl, form, {
|
|
541
|
+
headers: {
|
|
542
|
+
...form.getHeaders(),
|
|
543
|
+
// Override content-type to let form-data set it
|
|
544
|
+
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
|
|
545
|
+
},
|
|
546
|
+
timeout: 60000 // 60 second timeout for uploads
|
|
580
547
|
});
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
548
|
+
if (response.status === 201 && response.data.success) {
|
|
549
|
+
const artifact = response.data.artifact;
|
|
550
|
+
console.log(`✅ S3 upload completed for ${fileName}`);
|
|
551
|
+
// Extract S3 key from URL if not provided
|
|
552
|
+
const s3Key = artifact.s3Key || artifact.s3Url.split('/').slice(-1)[0];
|
|
553
|
+
return {
|
|
554
|
+
key: s3Key,
|
|
555
|
+
url: artifact.s3Url,
|
|
556
|
+
presignedUrl: artifact.presignedUrl,
|
|
557
|
+
fileSize: artifact.fileSize,
|
|
558
|
+
contentType: artifact.contentType
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
throw new Error(`Upload failed: ${response.data.error || 'Unknown error'}`);
|
|
563
|
+
}
|
|
589
564
|
}
|
|
590
565
|
catch (error) {
|
|
591
|
-
console.error(`❌ Failed to upload ${fileName} to S3:`, error.message);
|
|
592
|
-
throw error;
|
|
566
|
+
console.error(`❌ Failed to upload ${fileName} to TestLens S3 API:`, error.message);
|
|
567
|
+
throw error; // Re-throw to prevent fallback to local storage
|
|
593
568
|
}
|
|
594
569
|
}
|
|
595
|
-
getContentType(
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
'.png': 'image/png',
|
|
600
|
-
'.jpg': 'image/jpeg',
|
|
601
|
-
'.jpeg': 'image/jpeg',
|
|
602
|
-
'.gif': 'image/gif',
|
|
603
|
-
'.json': 'application/json',
|
|
604
|
-
'.txt': 'text/plain',
|
|
605
|
-
'.html': 'text/html',
|
|
606
|
-
'.xml': 'application/xml',
|
|
607
|
-
'.zip': 'application/zip',
|
|
608
|
-
'.pdf': 'application/pdf'
|
|
609
|
-
};
|
|
610
|
-
return contentTypes[ext] || 'application/octet-stream';
|
|
570
|
+
getContentType(fileName) {
|
|
571
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
572
|
+
const mimeType = mime.default.getType(ext) || 'application/octet-stream';
|
|
573
|
+
return mimeType;
|
|
611
574
|
}
|
|
612
575
|
generateS3Key(runId, testId, fileName) {
|
|
613
576
|
const date = new Date().toISOString().slice(0, 10);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testlens-playwright-reporter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"author": {
|
|
37
37
|
"name": "TestLens Team",
|
|
38
38
|
"email": "support@testlens.io",
|
|
39
|
-
"url": "https://testlens.
|
|
39
|
+
"url": "https://testlens.qa-path.com"
|
|
40
40
|
},
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"peerDependencies": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@aws-sdk/s3-request-presigner": "^3.624.0",
|
|
48
48
|
"axios": "^1.11.0",
|
|
49
49
|
"dotenv": "^16.4.5",
|
|
50
|
+
"form-data": "^4.0.1",
|
|
50
51
|
"mime": "^4.0.4"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
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
|