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 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": "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",
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: EMBEDDED_CONFIG.TEST_API_ENDPOINT || options.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
- ...options
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
- // Initialize S3 client if S3 upload is enabled
46
- this.s3Client = null;
47
- this.s3Enabled = this.config.enableS3Upload && EMBEDDED_CONFIG.S3_BUCKET_NAME;
48
-
49
- if (this.s3Enabled) {
50
- try {
51
- const s3Config = {
52
- region: EMBEDDED_CONFIG.AWS_REGION || 'us-east-1',
53
- maxAttempts: 3
54
- };
55
-
56
- if (EMBEDDED_CONFIG.AWS_ACCESS_KEY_ID && EMBEDDED_CONFIG.AWS_SECRET_ACCESS_KEY) {
57
- s3Config.credentials = {
58
- accessKeyId: EMBEDDED_CONFIG.AWS_ACCESS_KEY_ID,
59
- secretAccessKey: EMBEDDED_CONFIG.AWS_SECRET_ACCESS_KEY
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
- let s3Data = null;
378
-
379
- // Upload to S3 if enabled
380
- if (this.s3Enabled && this.s3Client) {
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: s3Data ? s3Data.fileSize : this.getFileSize(attachment.path),
392
- ...(s3Data && {
393
- s3Key: s3Data.key,
394
- s3Url: s3Data.url,
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}${s3Data ? ' (uploaded to S3)' : ''}`);
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": "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",
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: EMBEDDED_CONFIG.TEST_API_ENDPOINT || options.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.status === 'timedOut').length;
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
- let s3Data: { key: string; url: string; presignedUrl: string; fileSize: number; contentType: string } | null = null;
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: s3Data ? s3Data.fileSize : this.getFileSize(attachment.path),
526
- ...(s3Data && {
527
- s3Key: s3Data.key,
528
- s3Url: s3Data.url,
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}${s3Data ? ' (uploaded to S3)' : ''}`);
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
- const fileContent = fs.readFileSync(filePath);
720
- const fileSize = fileContent.length;
721
-
722
- // Get content type based on file extension
723
- const ext = path.extname(filePath).toLowerCase();
724
- const contentType = this.getContentType(ext);
725
-
726
- // Generate S3 key
727
- const s3Key = this.generateS3Key(this.runId, testId, fileName);
728
-
729
- const uploadParams = {
730
- Bucket: EMBEDDED_CONFIG.S3_BUCKET_NAME,
731
- Key: s3Key,
732
- Body: fileContent,
733
- ContentType: contentType,
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
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
- const presignedUrl = await getSignedUrl(this.s3Client!, getCommand, {
746
- expiresIn: 604800 // 7 days
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
- const s3Url = `https://${EMBEDDED_CONFIG.S3_BUCKET_NAME}.s3.${EMBEDDED_CONFIG.AWS_REGION || 'us-east-1'}.amazonaws.com/${s3Key}`;
750
-
751
- return {
752
- key: s3Key,
753
- url: s3Url,
754
- presignedUrl: presignedUrl,
755
- fileSize,
756
- contentType
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": "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",
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: EMBEDDED_CONFIG.TEST_API_ENDPOINT || options.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.status === 'timedOut').length;
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
- let s3Data = null;
372
- // Upload to S3 if enabled
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: s3Data ? s3Data.fileSize : this.getFileSize(attachment.path),
383
- ...(s3Data && {
384
- s3Key: s3Data.key,
385
- s3Url: s3Data.url,
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}${s3Data ? ' (uploaded to S3)' : ''}`);
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
- const fileContent = fs.readFileSync(filePath);
559
- const fileSize = fileContent.length;
560
- // Get content type based on file extension
561
- const ext = path.extname(filePath).toLowerCase();
562
- const contentType = this.getContentType(ext);
563
- // Generate S3 key
564
- const s3Key = this.generateS3Key(this.runId, testId, fileName);
565
- const uploadParams = {
566
- Bucket: EMBEDDED_CONFIG.S3_BUCKET_NAME,
567
- Key: s3Key,
568
- Body: fileContent,
569
- ContentType: contentType,
570
- ACL: EMBEDDED_CONFIG.S3_ACL || 'private',
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
- const presignedUrl = await (0, s3_request_presigner_1.getSignedUrl)(this.s3Client, getCommand, {
579
- expiresIn: 604800 // 7 days
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
- const s3Url = `https://${EMBEDDED_CONFIG.S3_BUCKET_NAME}.s3.${EMBEDDED_CONFIG.AWS_REGION || 'us-east-1'}.amazonaws.com/${s3Key}`;
582
- return {
583
- key: s3Key,
584
- url: s3Url,
585
- presignedUrl: presignedUrl,
586
- fileSize,
587
- contentType
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.0",
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