testlens-playwright-reporter 0.1.2 → 0.1.5

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.
Files changed (4) hide show
  1. package/index.js +89 -32
  2. package/index.ts +94 -35
  3. package/lib/index.js +84 -32
  4. package/package.json +1 -1
package/index.js CHANGED
@@ -56,12 +56,12 @@ class TestLensReporter {
56
56
  'Content-Type': 'application/json',
57
57
  ...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
58
58
  },
59
- // Enhanced SSL handling
59
+ // Enhanced SSL handling with flexible TLS configuration
60
60
  httpsAgent: new https.Agent({
61
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'
62
+ // Allow any TLS version for better compatibility
63
+ minVersion: 'TLSv1.2',
64
+ maxVersion: 'TLSv1.3'
65
65
  })
66
66
  });
67
67
 
@@ -589,51 +589,108 @@ class TestLensReporter {
589
589
 
590
590
  async uploadArtifactToS3(filePath, testId, fileName) {
591
591
  try {
592
- // Use the public S3 upload API endpoint
592
+ // Check file size first
593
+ const fileSize = this.getFileSize(filePath);
594
+ const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
595
+
596
+ console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
597
+
593
598
  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)
599
+
600
+ // Step 1: Request pre-signed URL from server
601
+ const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
602
+ const presignedResponse = await this.axiosInstance.post(presignedUrlEndpoint, {
603
+ apiKey: this.config.apiKey,
604
+ testRunId: this.runId,
605
+ testId: testId,
606
+ fileName: fileName,
607
+ fileType: this.getContentType(fileName),
608
+ fileSize: fileSize,
609
+ artifactType: this.getArtifactType(fileName)
610
+ }, {
611
+ timeout: 10000 // Quick timeout for metadata request
607
612
  });
608
613
 
609
- console.log(`📤 Uploading ${fileName} to TestLens S3 via API...`);
614
+ if (!presignedResponse.data.success) {
615
+ throw new Error(`Failed to get presigned URL: ${presignedResponse.data.error || 'Unknown error'}`);
616
+ }
610
617
 
611
- // Make the upload request
612
- const response = await this.axiosInstance.post(uploadUrl, form, {
618
+ const { uploadUrl, s3Key, metadata } = presignedResponse.data;
619
+
620
+ // Step 2: Upload directly to S3 using presigned URL
621
+ console.log(`⬆️ Uploading ${fileName} directly to S3 (bypass server)...`);
622
+
623
+ const fileBuffer = fs.readFileSync(filePath);
624
+
625
+ // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
626
+ // The backend signs with ServerSideEncryption:'AES256', so we must send that header
627
+ // AWS presigned URLs are very strict about header matching
628
+ const uploadResponse = await axios.put(uploadUrl, fileBuffer, {
613
629
  headers: {
614
- ...form.getHeaders(),
615
- // Override content-type to let form-data set it
616
- 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
630
+ 'x-amz-server-side-encryption': 'AES256'
617
631
  },
618
- timeout: 60000 // 60 second timeout for uploads
632
+ maxContentLength: Infinity,
633
+ maxBodyLength: Infinity,
634
+ timeout: Math.max(300000, Math.ceil(fileSize / (1024 * 1024)) * 5000), // 5s per MB, min 5 minutes
635
+ // Don't use custom HTTPS agent for S3 uploads
636
+ httpsAgent: undefined
619
637
  });
620
638
 
621
- if (response.status === 201 && response.data.success) {
622
- const artifact = response.data.artifact;
623
- console.log(`✅ S3 upload completed for ${fileName}`);
639
+ if (uploadResponse.status !== 200) {
640
+ throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
641
+ }
642
+
643
+ console.log(`✅ S3 direct upload completed for ${fileName}`);
644
+
645
+ // Step 3: Confirm upload with server to save metadata
646
+ const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
647
+ const confirmResponse = await this.axiosInstance.post(confirmEndpoint, {
648
+ apiKey: this.config.apiKey,
649
+ testRunId: this.runId,
650
+ testId: testId,
651
+ s3Key: s3Key,
652
+ fileName: fileName,
653
+ fileType: this.getContentType(fileName),
654
+ fileSize: fileSize,
655
+ artifactType: this.getArtifactType(fileName)
656
+ }, {
657
+ timeout: 10000
658
+ });
659
+
660
+ if (confirmResponse.status === 201 && confirmResponse.data.success) {
661
+ const artifact = confirmResponse.data.artifact;
662
+ console.log(`✅ Upload confirmed and saved to database`);
624
663
  return {
625
- key: artifact.s3Key,
664
+ key: s3Key,
626
665
  url: artifact.s3Url,
627
666
  presignedUrl: artifact.presignedUrl,
628
667
  fileSize: artifact.fileSize,
629
668
  contentType: artifact.contentType
630
669
  };
631
670
  } else {
632
- throw new Error(`Upload failed: ${response.data.error || 'Unknown error'}`);
671
+ throw new Error(`Upload confirmation failed: ${confirmResponse.data.error || 'Unknown error'}`);
633
672
  }
634
673
  } 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
674
+ // Better error messages for common issues
675
+ let errorMsg = error.message;
676
+
677
+ if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
678
+ errorMsg = `Upload timeout - file may be too large or connection is slow`;
679
+ } else if (error.response?.status === 413) {
680
+ errorMsg = `File too large (413) - server rejected the upload`;
681
+ } else if (error.response?.status === 400) {
682
+ errorMsg = `Bad request (400) - ${error.response.data?.error || 'check file format'}`;
683
+ } else if (error.response?.status === 403) {
684
+ errorMsg = `Access denied (403) - presigned URL may have expired`;
685
+ }
686
+
687
+ console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
688
+ if (error.response?.data) {
689
+ console.error('Error details:', error.response.data);
690
+ }
691
+
692
+ // Don't throw, just return null to continue with other artifacts
693
+ return null;
637
694
  }
638
695
  }
639
696
 
package/index.ts CHANGED
@@ -185,12 +185,12 @@ export class TestLensReporter implements Reporter {
185
185
  'Content-Type': 'application/json',
186
186
  ...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
187
187
  },
188
- // Enhanced SSL handling
188
+ // Enhanced SSL handling with flexible TLS configuration
189
189
  httpsAgent: new https.Agent({
190
190
  rejectUnauthorized: rejectUnauthorized,
191
- // Additional SSL options for better compatibility
192
- secureProtocol: 'TLSv1_2_method',
193
- ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384'
191
+ // Allow any TLS version for better compatibility
192
+ minVersion: 'TLSv1.2',
193
+ maxVersion: 'TLSv1.3'
194
194
  })
195
195
  });
196
196
 
@@ -472,6 +472,12 @@ export class TestLensReporter implements Reporter {
472
472
  // Upload to S3 first
473
473
  const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
474
474
 
475
+ // Skip if upload failed or file was too large
476
+ if (!s3Data) {
477
+ console.log(`⏭️ Skipping artifact ${attachment.name} - upload failed or file too large`);
478
+ continue;
479
+ }
480
+
475
481
  const artifactData = {
476
482
  testId,
477
483
  type: this.getArtifactType(attachment.name),
@@ -668,44 +674,79 @@ export class TestLensReporter implements Reporter {
668
674
  }
669
675
 
670
676
 
671
- private async uploadArtifactToS3(filePath: string, testId: string, fileName: string): Promise<{ key: string; url: string; presignedUrl: string; fileSize: number; contentType: string }> {
677
+ private async uploadArtifactToS3(filePath: string, testId: string, fileName: string): Promise<{ key: string; url: string; presignedUrl: string; fileSize: number; contentType: string } | null> {
672
678
  try {
673
- // Use the new public S3 upload API endpoint instead of direct AWS SDK calls
679
+ // Check file size first
680
+ const fileSize = this.getFileSize(filePath);
681
+ const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
682
+
683
+ console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
684
+
674
685
  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)
686
+
687
+ // Step 1: Request pre-signed URL from server
688
+ const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
689
+ const presignedResponse = await this.axiosInstance.post(presignedUrlEndpoint, {
690
+ apiKey: this.config.apiKey,
691
+ testRunId: this.runId,
692
+ testId: testId,
693
+ fileName: fileName,
694
+ fileType: this.getContentType(fileName),
695
+ fileSize: fileSize,
696
+ artifactType: this.getArtifactType(fileName)
697
+ }, {
698
+ timeout: 10000 // Quick timeout for metadata request
688
699
  });
689
700
 
690
- console.log(`📤 Uploading ${fileName} to TestLens S3 via API...`);
701
+ if (!presignedResponse.data.success) {
702
+ throw new Error(`Failed to get presigned URL: ${presignedResponse.data.error || 'Unknown error'}`);
703
+ }
691
704
 
692
- // Make the upload request
693
- const response = await this.axiosInstance.post(uploadUrl, form, {
705
+ const { uploadUrl, s3Key, metadata } = presignedResponse.data;
706
+
707
+ // Step 2: Upload directly to S3 using presigned URL
708
+ console.log(`⬆️ Uploading ${fileName} directly to S3 (bypass server)...`);
709
+
710
+ const fileBuffer = fs.readFileSync(filePath);
711
+
712
+ // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
713
+ // The backend signs with ServerSideEncryption:'AES256', so we must send that header
714
+ // AWS presigned URLs are very strict about header matching
715
+ const uploadResponse = await axios.put(uploadUrl, fileBuffer, {
694
716
  headers: {
695
- ...form.getHeaders(),
696
- // Override content-type to let form-data set it
697
- 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
717
+ 'x-amz-server-side-encryption': 'AES256'
698
718
  },
699
- timeout: 60000 // 60 second timeout for uploads
719
+ maxContentLength: Infinity,
720
+ maxBodyLength: Infinity,
721
+ timeout: Math.max(300000, Math.ceil(fileSize / (1024 * 1024)) * 5000), // 5s per MB, min 5 minutes
722
+ // Don't use custom HTTPS agent for S3 uploads
723
+ httpsAgent: undefined
700
724
  });
701
725
 
702
- if (response.status === 201 && response.data.success) {
703
- const artifact = response.data.artifact;
704
- console.log(`✅ S3 upload completed for ${fileName}`);
726
+ if (uploadResponse.status !== 200) {
727
+ throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
728
+ }
705
729
 
706
- // Extract S3 key from URL if not provided
707
- const s3Key = artifact.s3Key || artifact.s3Url.split('/').slice(-1)[0];
730
+ console.log(`✅ S3 direct upload completed for ${fileName}`);
731
+
732
+ // Step 3: Confirm upload with server to save metadata
733
+ const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
734
+ const confirmResponse = await this.axiosInstance.post(confirmEndpoint, {
735
+ apiKey: this.config.apiKey,
736
+ testRunId: this.runId,
737
+ testId: testId,
738
+ s3Key: s3Key,
739
+ fileName: fileName,
740
+ fileType: this.getContentType(fileName),
741
+ fileSize: fileSize,
742
+ artifactType: this.getArtifactType(fileName)
743
+ }, {
744
+ timeout: 10000
745
+ });
708
746
 
747
+ if (confirmResponse.status === 201 && confirmResponse.data.success) {
748
+ const artifact = confirmResponse.data.artifact;
749
+ console.log(`✅ Upload confirmed and saved to database`);
709
750
  return {
710
751
  key: s3Key,
711
752
  url: artifact.s3Url,
@@ -714,11 +755,29 @@ export class TestLensReporter implements Reporter {
714
755
  contentType: artifact.contentType
715
756
  };
716
757
  } else {
717
- throw new Error(`Upload failed: ${response.data.error || 'Unknown error'}`);
758
+ throw new Error(`Upload confirmation failed: ${confirmResponse.data.error || 'Unknown error'}`);
718
759
  }
719
- } catch (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
760
+ } catch (error: any) {
761
+ // Better error messages for common issues
762
+ let errorMsg = error.message;
763
+
764
+ if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
765
+ errorMsg = `Upload timeout - file may be too large or connection is slow`;
766
+ } else if (error.response?.status === 413) {
767
+ errorMsg = `File too large (413) - server rejected the upload`;
768
+ } else if (error.response?.status === 400) {
769
+ errorMsg = `Bad request (400) - ${error.response.data?.error || 'check file format'}`;
770
+ } else if (error.response?.status === 403) {
771
+ errorMsg = `Access denied (403) - presigned URL may have expired`;
772
+ }
773
+
774
+ console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
775
+ if (error.response?.data) {
776
+ console.error('Error details:', error.response.data);
777
+ }
778
+
779
+ // Don't throw, just return null to continue with other artifacts
780
+ return null;
722
781
  }
723
782
  }
724
783
 
package/lib/index.js CHANGED
@@ -45,7 +45,6 @@ const https = __importStar(require("https"));
45
45
  const axios_1 = __importDefault(require("axios"));
46
46
  const child_process_1 = require("child_process");
47
47
  const mime = __importStar(require("mime"));
48
- const form_data_1 = __importDefault(require("form-data"));
49
48
  class TestLensReporter {
50
49
  constructor(options) {
51
50
  this.config = {
@@ -93,12 +92,12 @@ class TestLensReporter {
93
92
  'Content-Type': 'application/json',
94
93
  ...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
95
94
  },
96
- // Enhanced SSL handling
95
+ // Enhanced SSL handling with flexible TLS configuration
97
96
  httpsAgent: new https.Agent({
98
97
  rejectUnauthorized: rejectUnauthorized,
99
- // Additional SSL options for better compatibility
100
- secureProtocol: 'TLSv1_2_method',
101
- ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384'
98
+ // Allow any TLS version for better compatibility
99
+ minVersion: 'TLSv1.2',
100
+ maxVersion: 'TLSv1.3'
102
101
  })
103
102
  });
104
103
  // Add retry interceptor
@@ -342,6 +341,11 @@ class TestLensReporter {
342
341
  try {
343
342
  // Upload to S3 first
344
343
  const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
344
+ // Skip if upload failed or file was too large
345
+ if (!s3Data) {
346
+ console.log(`⏭️ Skipping artifact ${attachment.name} - upload failed or file too large`);
347
+ continue;
348
+ }
345
349
  const artifactData = {
346
350
  testId,
347
351
  type: this.getArtifactType(attachment.name),
@@ -521,35 +525,65 @@ class TestLensReporter {
521
525
  }
522
526
  async uploadArtifactToS3(filePath, testId, fileName) {
523
527
  try {
524
- // Use the new public S3 upload API endpoint instead of direct AWS SDK calls
528
+ // Check file size first
529
+ const fileSize = this.getFileSize(filePath);
530
+ const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
531
+ console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
525
532
  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)
533
+ // Step 1: Request pre-signed URL from server
534
+ const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
535
+ const presignedResponse = await this.axiosInstance.post(presignedUrlEndpoint, {
536
+ apiKey: this.config.apiKey,
537
+ testRunId: this.runId,
538
+ testId: testId,
539
+ fileName: fileName,
540
+ fileType: this.getContentType(fileName),
541
+ fileSize: fileSize,
542
+ artifactType: this.getArtifactType(fileName)
543
+ }, {
544
+ timeout: 10000 // Quick timeout for metadata request
537
545
  });
538
- console.log(`📤 Uploading ${fileName} to TestLens S3 via API...`);
539
- // Make the upload request
540
- const response = await this.axiosInstance.post(uploadUrl, form, {
546
+ if (!presignedResponse.data.success) {
547
+ throw new Error(`Failed to get presigned URL: ${presignedResponse.data.error || 'Unknown error'}`);
548
+ }
549
+ const { uploadUrl, s3Key, metadata } = presignedResponse.data;
550
+ // Step 2: Upload directly to S3 using presigned URL
551
+ console.log(`⬆️ Uploading ${fileName} directly to S3 (bypass server)...`);
552
+ const fileBuffer = fs.readFileSync(filePath);
553
+ // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
554
+ // The backend signs with ServerSideEncryption:'AES256', so we must send that header
555
+ // AWS presigned URLs are very strict about header matching
556
+ const uploadResponse = await axios_1.default.put(uploadUrl, fileBuffer, {
541
557
  headers: {
542
- ...form.getHeaders(),
543
- // Override content-type to let form-data set it
544
- 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
558
+ 'x-amz-server-side-encryption': 'AES256'
545
559
  },
546
- timeout: 60000 // 60 second timeout for uploads
560
+ maxContentLength: Infinity,
561
+ maxBodyLength: Infinity,
562
+ timeout: Math.max(300000, Math.ceil(fileSize / (1024 * 1024)) * 5000), // 5s per MB, min 5 minutes
563
+ // Don't use custom HTTPS agent for S3 uploads
564
+ httpsAgent: undefined
547
565
  });
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];
566
+ if (uploadResponse.status !== 200) {
567
+ throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
568
+ }
569
+ console.log(`✅ S3 direct upload completed for ${fileName}`);
570
+ // Step 3: Confirm upload with server to save metadata
571
+ const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
572
+ const confirmResponse = await this.axiosInstance.post(confirmEndpoint, {
573
+ apiKey: this.config.apiKey,
574
+ testRunId: this.runId,
575
+ testId: testId,
576
+ s3Key: s3Key,
577
+ fileName: fileName,
578
+ fileType: this.getContentType(fileName),
579
+ fileSize: fileSize,
580
+ artifactType: this.getArtifactType(fileName)
581
+ }, {
582
+ timeout: 10000
583
+ });
584
+ if (confirmResponse.status === 201 && confirmResponse.data.success) {
585
+ const artifact = confirmResponse.data.artifact;
586
+ console.log(`✅ Upload confirmed and saved to database`);
553
587
  return {
554
588
  key: s3Key,
555
589
  url: artifact.s3Url,
@@ -559,12 +593,30 @@ class TestLensReporter {
559
593
  };
560
594
  }
561
595
  else {
562
- throw new Error(`Upload failed: ${response.data.error || 'Unknown error'}`);
596
+ throw new Error(`Upload confirmation failed: ${confirmResponse.data.error || 'Unknown error'}`);
563
597
  }
564
598
  }
565
599
  catch (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
600
+ // Better error messages for common issues
601
+ let errorMsg = error.message;
602
+ if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
603
+ errorMsg = `Upload timeout - file may be too large or connection is slow`;
604
+ }
605
+ else if (error.response?.status === 413) {
606
+ errorMsg = `File too large (413) - server rejected the upload`;
607
+ }
608
+ else if (error.response?.status === 400) {
609
+ errorMsg = `Bad request (400) - ${error.response.data?.error || 'check file format'}`;
610
+ }
611
+ else if (error.response?.status === 403) {
612
+ errorMsg = `Access denied (403) - presigned URL may have expired`;
613
+ }
614
+ console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
615
+ if (error.response?.data) {
616
+ console.error('Error details:', error.response.data);
617
+ }
618
+ // Don't throw, just return null to continue with other artifacts
619
+ return null;
568
620
  }
569
621
  }
570
622
  getContentType(fileName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testlens-playwright-reporter",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
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",