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 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
@@ -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": "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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
-
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: EMBEDDED_CONFIG.TEST_API_ENDPOINT || options.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.status === 'timedOut').length;
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
- data: error?.response?.data
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
- 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
- }
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: 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
- })
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}${s3Data ? ' (uploaded to S3)' : ''}`);
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
- 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
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
- const presignedUrl = await getSignedUrl(this.s3Client!, getCommand, {
746
- expiresIn: 604800 // 7 days
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
- 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
- };
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(ext: string): string {
765
- const contentTypes: { [key: string]: string } = {
766
- '.mp4': 'video/mp4',
767
- '.webm': 'video/webm',
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 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
- };
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: EMBEDDED_CONFIG.TEST_API_ENDPOINT || options.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.status === 'timedOut').length;
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
- data: error?.response?.data
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
- 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
- }
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: 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
- })
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}${s3Data ? ' (uploaded to S3)' : ''}`);
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
- 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
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
- const presignedUrl = await (0, s3_request_presigner_1.getSignedUrl)(this.s3Client, getCommand, {
579
- expiresIn: 604800 // 7 days
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
- 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
- };
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(ext) {
596
- const contentTypes = {
597
- '.mp4': 'video/mp4',
598
- '.webm': 'video/webm',
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.0",
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.io"
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