@alternative-path/testlens-playwright-reporter 0.4.2 → 0.4.4

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.ts CHANGED
@@ -3,7 +3,6 @@ import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import * as fs from 'fs';
5
5
  import * as https from 'https';
6
- import * as tls from 'tls';
7
6
  import axios, { AxiosInstance } from 'axios';
8
7
  import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
9
8
  import { execSync } from 'child_process';
@@ -47,8 +46,6 @@ export interface TestLensReporterConfig {
47
46
  rejectUnauthorized?: boolean;
48
47
  /** Alternative SSL option - set to true to ignore SSL certificate errors */
49
48
  ignoreSslErrors?: boolean;
50
- /** Path to a custom CA certificate file (PEM format) to use for SSL verification */
51
- caCertificate?: string;
52
49
  /** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
53
50
  customMetadata?: Record<string, string | string[]>;
54
51
  }
@@ -80,8 +77,6 @@ export interface TestLensReporterOptions {
80
77
  rejectUnauthorized?: boolean;
81
78
  /** Alternative SSL option - set to true to ignore SSL certificate errors */
82
79
  ignoreSslErrors?: boolean;
83
- /** Path to a custom CA certificate file (PEM format) to use for SSL verification */
84
- caCertificate?: string;
85
80
  /** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
86
81
  customMetadata?: Record<string, string | string[]>;
87
82
  }
@@ -118,6 +113,7 @@ export interface RunMetadata {
118
113
  os: string;
119
114
  playwrightVersion: string;
120
115
  nodeVersion: string;
116
+ testlensVersion?: string;
121
117
  gitInfo?: GitInfo | null;
122
118
  shardInfo?: {
123
119
  current: number;
@@ -183,118 +179,13 @@ export interface SpecData {
183
179
  export class TestLensReporter implements Reporter {
184
180
  private config: Required<TestLensReporterConfig>;
185
181
  private axiosInstance: AxiosInstance;
186
-
187
- /**
188
- * Get bundled CA certificates for TestLens
189
- * Combines custom CA bundle with Node.js root certificates
190
- * This ensures SSL works with both testlens.qa-path.com and other HTTPS endpoints
191
- */
192
- private static getBundledCaCertificates(): Buffer[] | undefined {
193
- const allCerts: Buffer[] = [];
194
-
195
- // First, add our bundled TestLens CA certificate chain
196
- try {
197
- const certPath = path.join(__dirname, 'testlens-ca-bundle.pem');
198
- if (fs.existsSync(certPath)) {
199
- const certData = fs.readFileSync(certPath, 'utf8');
200
- // Split the bundle into individual certificates
201
- const certs = certData.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
202
- if (certs && certs.length > 0) {
203
- const buffers = certs.map(cert => Buffer.from(cert, 'utf8'));
204
- allCerts.push(...buffers);
205
- if (process.env.DEBUG) {
206
- console.log(`✓ Loaded bundled TestLens CA certificates (${buffers.length} certificate(s))`);
207
- }
208
- }
209
- }
210
- } catch (error) {
211
- if (process.env.DEBUG) {
212
- console.log('⚠️ Bundled CA certificate not available');
213
- }
214
- }
215
-
216
- // Then, add Node.js built-in root certificates for general SSL support
217
- try {
218
- if (tls.rootCertificates && Array.isArray(tls.rootCertificates) && tls.rootCertificates.length > 0) {
219
- const rootCerts = tls.rootCertificates.map(cert => Buffer.from(cert, 'utf8'));
220
- allCerts.push(...rootCerts);
221
- if (process.env.DEBUG) {
222
- console.log(`✓ Added Node.js built-in root certificates (${rootCerts.length} certificates)`);
223
- }
224
- }
225
- } catch (error) {
226
- if (process.env.DEBUG) {
227
- console.log('⚠️ Node.js built-in root certificates not available');
228
- }
229
- }
230
-
231
- return allCerts.length > 0 ? allCerts : undefined;
232
- }
233
-
234
- /**
235
- * Automatically detect and load system CA certificates
236
- * Uses bundled certificates as primary source, falls back to system certificates
237
- */
238
- private static getSystemCaCertificates(): Buffer[] | undefined {
239
- // First, try to use our bundled certificates (Node.js rootCertificates)
240
- const bundledCerts = TestLensReporter.getBundledCaCertificates();
241
- if (bundledCerts && bundledCerts.length > 0) {
242
- return bundledCerts;
243
- }
244
-
245
- // Fallback: Try to load from file system (for older Node.js versions or special cases)
246
- const platform = os.platform();
247
- const certificates: Buffer[] = [];
248
- const certPaths: string[] = [];
249
-
250
- if (platform === 'darwin') {
251
- // macOS: Common certificate locations
252
- certPaths.push(
253
- '/etc/ssl/cert.pem',
254
- '/usr/local/etc/openssl/cert.pem',
255
- '/opt/homebrew/etc/openssl/cert.pem',
256
- '/System/Library/OpenSSL/certs/cert.pem'
257
- );
258
- } else {
259
- // Linux and other Unix-like systems
260
- certPaths.push(
261
- '/etc/ssl/certs/ca-certificates.crt',
262
- '/etc/ssl/certs/ca-bundle.crt',
263
- '/etc/pki/tls/certs/ca-bundle.crt',
264
- '/etc/ssl/ca-bundle.pem',
265
- '/usr/share/ssl/certs/ca-bundle.crt',
266
- '/usr/local/share/certs/ca-root-nss.crt',
267
- '/etc/ca-certificates/extracted/tls-ca-bundle.pem'
268
- );
269
- }
270
-
271
- // Try to load certificates from common locations
272
- for (const certPath of certPaths) {
273
- try {
274
- if (certPath && fs.existsSync(certPath)) {
275
- const certData = fs.readFileSync(certPath);
276
- certificates.push(certData);
277
- // Only log in debug mode to avoid noise
278
- if (process.env.DEBUG) {
279
- console.log(`✓ Loaded CA certificates from: ${certPath}`);
280
- }
281
- }
282
- } catch (error) {
283
- // Silently continue if a certificate file can't be read
284
- // This is expected as not all paths will exist on every system
285
- }
286
- }
287
-
288
- // Return undefined if no certificates found (let Node.js use defaults)
289
- // Return certificates array if we found any
290
- return certificates.length > 0 ? certificates : undefined;
291
- }
292
182
  private runId: string;
293
183
  private runMetadata: RunMetadata;
294
184
  private specMap: Map<string, SpecData>;
295
185
  private testMap: Map<string, TestData>;
296
186
  private runCreationFailed: boolean = false; // Track if run creation failed due to limits
297
187
  private cliArgs: Record<string, any> = {}; // Store CLI args separately
188
+ private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
298
189
 
299
190
  /**
300
191
  * Parse custom metadata from environment variables
@@ -379,44 +270,6 @@ export class TestLensReporter implements Reporter {
379
270
 
380
271
  // Determine SSL validation behavior
381
272
  let rejectUnauthorized = true; // Default to secure
382
- let ca: Buffer | string | Buffer[] | undefined = undefined;
383
-
384
- // Use bundled CA certificates as primary source (Node.js rootCertificates)
385
- // This ensures consistent behavior across all platforms
386
- const bundledCerts = TestLensReporter.getBundledCaCertificates();
387
-
388
- // Load custom CA certificate if explicitly provided (for advanced users)
389
- // Custom certificates will be combined with bundled certificates
390
- if (this.config.caCertificate) {
391
- try {
392
- if (fs.existsSync(this.config.caCertificate)) {
393
- const customCert = fs.readFileSync(this.config.caCertificate);
394
- // Combine bundled certs with custom cert
395
- if (bundledCerts && Array.isArray(bundledCerts) && bundledCerts.length > 0) {
396
- ca = [...bundledCerts, customCert];
397
- console.log(`✓ Using bundled CA certificates + custom certificate: ${this.config.caCertificate}`);
398
- } else {
399
- ca = customCert;
400
- console.log(`✓ Using custom CA certificate: ${this.config.caCertificate}`);
401
- }
402
- } else {
403
- console.warn(`⚠️ CA certificate file not found: ${this.config.caCertificate}`);
404
- // Fall back to bundled certs if custom cert not found
405
- ca = bundledCerts || undefined;
406
- }
407
- } catch (error) {
408
- console.warn(`⚠️ Failed to read CA certificate file: ${this.config.caCertificate}`, (error as Error).message);
409
- // Fall back to bundled certs if custom cert read fails
410
- ca = bundledCerts || undefined;
411
- }
412
- } else {
413
- // Use bundled certificates as primary source
414
- // This works reliably across all platforms (Windows, macOS, Linux)
415
- ca = bundledCerts || undefined;
416
- if (ca && process.env.DEBUG) {
417
- console.log(`✓ Using bundled CA certificates (${Array.isArray(ca) ? ca.length : 1} certificates)`);
418
- }
419
- }
420
273
 
421
274
  // Check various ways SSL validation can be disabled (in order of precedence)
422
275
  if (this.config.ignoreSslErrors) {
@@ -434,25 +287,6 @@ export class TestLensReporter implements Reporter {
434
287
  }
435
288
 
436
289
  // Set up axios instance with retry logic and enhanced SSL handling
437
- const httpsAgentOptions: https.AgentOptions = {
438
- rejectUnauthorized: rejectUnauthorized,
439
- // Allow any TLS version for better compatibility
440
- minVersion: 'TLSv1.2',
441
- maxVersion: 'TLSv1.3'
442
- };
443
-
444
- // Add CA certificates if available
445
- // On Windows, ca will be undefined to let Node.js use Windows certificate store automatically
446
- // On Unix systems, ca will contain certificates if found, or undefined to use Node.js defaults
447
- if (ca !== undefined) {
448
- if (Array.isArray(ca) && ca.length > 0) {
449
- httpsAgentOptions.ca = ca;
450
- } else if (typeof ca === 'string' || Buffer.isBuffer(ca)) {
451
- httpsAgentOptions.ca = ca;
452
- }
453
- // If ca is undefined, we don't set it, allowing Node.js to use its default certificate store
454
- }
455
-
456
290
  this.axiosInstance = axios.create({
457
291
  baseURL: this.config.apiEndpoint,
458
292
  timeout: this.config.timeout,
@@ -461,7 +295,12 @@ export class TestLensReporter implements Reporter {
461
295
  ...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
462
296
  },
463
297
  // Enhanced SSL handling with flexible TLS configuration
464
- httpsAgent: new https.Agent(httpsAgentOptions)
298
+ httpsAgent: new https.Agent({
299
+ rejectUnauthorized: rejectUnauthorized,
300
+ // Allow any TLS version for better compatibility
301
+ minVersion: 'TLSv1.2',
302
+ maxVersion: 'TLSv1.3'
303
+ })
465
304
  });
466
305
 
467
306
  // Add retry interceptor
@@ -510,7 +349,8 @@ export class TestLensReporter implements Reporter {
510
349
  browser: 'multiple',
511
350
  os: `${os.type()} ${os.release()}`,
512
351
  playwrightVersion: this.getPlaywrightVersion(),
513
- nodeVersion: process.version
352
+ nodeVersion: process.version,
353
+ testlensVersion: this.getTestLensVersion()
514
354
  };
515
355
 
516
356
  // Add custom metadata if provided
@@ -537,6 +377,15 @@ export class TestLensReporter implements Reporter {
537
377
  }
538
378
  }
539
379
 
380
+ private getTestLensVersion(): string {
381
+ try {
382
+ const testlensPackage = require('./package.json');
383
+ return testlensPackage.version;
384
+ } catch (error) {
385
+ return 'unknown';
386
+ }
387
+ }
388
+
540
389
  private normalizeTestStatus(status: string): string {
541
390
  // Treat timeout as failed for consistency with analytics
542
391
  if (status === 'timedOut') {
@@ -846,25 +695,21 @@ export class TestLensReporter implements Reporter {
846
695
  return testError;
847
696
  });
848
697
 
849
- // Only send testEnd event after final retry attempt
850
- // If test passed or this is the last retry, send the event
851
- const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
852
-
853
- if (isFinalAttempt) {
854
- console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
855
- // Send test end event to API and get response
856
- const testEndResponse = await this.sendToApi({
857
- type: 'testEnd',
858
- runId: this.runId,
859
- timestamp: new Date().toISOString(),
860
- test: testData
861
- });
698
+ // Send testEnd event for all tests, regardless of status
699
+ // This ensures tests that are interrupted or have unexpected statuses are properly recorded
700
+ console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
701
+ // Send test end event to API and get response
702
+ const testEndResponse = await this.sendToApi({
703
+ type: 'testEnd',
704
+ runId: this.runId,
705
+ timestamp: new Date().toISOString(),
706
+ test: testData
707
+ });
862
708
 
863
- // Handle artifacts (test case is now guaranteed to be in database)
864
- if (this.config.enableArtifacts) {
865
- // Pass test case DB ID if available for faster lookups
866
- await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
867
- }
709
+ // Handle artifacts (test case is now guaranteed to be in database)
710
+ if (this.config.enableArtifacts) {
711
+ // Pass test case DB ID if available for faster lookups
712
+ await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
868
713
  }
869
714
  }
870
715
 
@@ -881,13 +726,33 @@ export class TestLensReporter implements Reporter {
881
726
  }
882
727
 
883
728
  // Check if all tests in spec are complete
729
+ // Only consider tests that were actually executed (have testData)
884
730
  const remainingTests = test.parent.tests.filter((t: any) => {
885
731
  const tId = this.getTestId(t);
886
732
  const tData = this.testMap.get(tId);
887
- return !tData || !tData.endTime;
733
+ // If testData exists but no endTime, it's still running
734
+ return tData && !tData.endTime;
888
735
  });
889
736
 
890
737
  if (remainingTests.length === 0) {
738
+ // Determine final spec status based on all executed tests
739
+ const executedTests = test.parent.tests
740
+ .map((t: any) => {
741
+ const tId = this.getTestId(t);
742
+ return this.testMap.get(tId);
743
+ })
744
+ .filter((tData: TestData | undefined): tData is TestData => !!tData);
745
+
746
+ if (executedTests.length > 0) {
747
+ const allTestStatuses = executedTests.map(tData => tData.status);
748
+ if (allTestStatuses.every(status => status === 'passed')) {
749
+ specData.status = 'passed';
750
+ } else if (allTestStatuses.some(status => status === 'failed')) {
751
+ specData.status = 'failed';
752
+ } else if (allTestStatuses.every(status => status === 'skipped')) {
753
+ specData.status = 'skipped';
754
+ }
755
+ }
891
756
  // Aggregate tags from all tests in this spec
892
757
  const allTags = new Set<string>();
893
758
  test.parent.tests.forEach((t: any) => {
@@ -920,6 +785,17 @@ export class TestLensReporter implements Reporter {
920
785
  this.runMetadata.endTime = new Date().toISOString();
921
786
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
922
787
 
788
+ // Wait for all pending artifact uploads to complete before sending runEnd
789
+ if (this.pendingUploads.size > 0) {
790
+ console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
791
+ try {
792
+ await Promise.all(Array.from(this.pendingUploads));
793
+ console.log(`✅ All artifact uploads completed`);
794
+ } catch (error) {
795
+ console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
796
+ }
797
+ }
798
+
923
799
  // Calculate final stats
924
800
  const totalTests = Array.from(this.testMap.values()).length;
925
801
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -1039,68 +915,17 @@ export class TestLensReporter implements Reporter {
1039
915
  } else {
1040
916
  console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
1041
917
  }
1042
- } else {
1043
- // Check for SSL certificate errors
1044
- const isSslError = error?.code === 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY' ||
1045
- error?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
1046
- error?.code === 'CERT_HAS_EXPIRED' ||
1047
- error?.code === 'SELF_SIGNED_CERT_IN_CHAIN' ||
1048
- error?.message?.includes('certificate') ||
1049
- error?.message?.includes('SSL') ||
1050
- error?.message?.includes('TLS');
1051
-
1052
- if (isSslError && status !== 403) {
1053
- console.error('\n' + '='.repeat(80));
1054
- console.error('❌ SSL Certificate Error');
1055
- console.error('='.repeat(80));
1056
- console.error('');
1057
- console.error(`Failed to send ${payload.type} event to TestLens due to SSL certificate issue.`);
1058
- console.error('');
1059
- console.error('The reporter automatically attempts to detect and use system CA certificates.');
1060
- console.error('If this error persists, it may indicate:');
1061
- console.error(' - Missing or outdated system CA certificates');
1062
- console.error(' - Corporate proxy with custom CA certificate');
1063
- console.error(' - Incomplete certificate chain from the server');
1064
- console.error('');
1065
- console.error('Error details:');
1066
- console.error(` Code: ${error?.code || 'Unknown'}`);
1067
- console.error(` Message: ${error?.message || 'Unknown error'}`);
1068
- console.error('');
1069
- console.error('Possible solutions:');
1070
- console.error('');
1071
- console.error('1. Update your system\'s CA certificate store:');
1072
- console.error(' - Windows: Update Windows root certificates via Windows Update');
1073
- console.error(' - macOS: Run: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <certificate.pem>');
1074
- console.error(' - Linux: Update ca-certificates package: sudo apt-get update && sudo apt-get install ca-certificates');
1075
- console.error('');
1076
- console.error('2. If you have a custom CA certificate file (e.g., from corporate proxy),');
1077
- console.error(' you can specify it in your config (optional):');
1078
- console.error(' reporter: [');
1079
- console.error(' [\'@testlens/playwright-reporter\', {');
1080
- console.error(' caCertificate: \'/path/to/your/ca-certificate.pem\'');
1081
- console.error(' }]');
1082
- console.error(' ]');
1083
- console.error('');
1084
- console.error('3. Contact your network administrator if you\'re behind a corporate proxy');
1085
- console.error(' that uses a custom CA certificate.');
1086
- console.error('');
1087
- console.error('⚠️ WARNING: Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables SSL verification');
1088
- console.error(' and is insecure. Only use this as a last resort in development.');
1089
- console.error('');
1090
- console.error('='.repeat(80));
1091
- console.error('');
1092
- } else if (status !== 403) {
1093
- // Log other errors (but not 403 which we handled above)
1094
- console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
1095
- message: error?.message || 'Unknown error',
1096
- status: status,
1097
- statusText: error?.response?.statusText,
1098
- data: errorData,
1099
- code: error?.code,
1100
- url: error?.config?.url,
1101
- method: error?.config?.method
1102
- });
1103
- }
918
+ } else if (status !== 403) {
919
+ // Log other errors (but not 403 which we handled above)
920
+ console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
921
+ message: error?.message || 'Unknown error',
922
+ status: status,
923
+ statusText: error?.response?.statusText,
924
+ data: errorData,
925
+ code: error?.code,
926
+ url: error?.config?.url,
927
+ method: error?.config?.method
928
+ });
1104
929
  }
1105
930
 
1106
931
  // Don't throw error to avoid breaking test execution
@@ -1166,37 +991,58 @@ export class TestLensReporter implements Reporter {
1166
991
  }
1167
992
 
1168
993
  // Upload to S3 first (pass DB ID if available for faster lookup)
1169
- const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
1170
-
1171
- // Skip if upload failed or file was too large
1172
- if (!s3Data) {
1173
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1174
- continue;
1175
- }
1176
-
1177
- const artifactData = {
1178
- testId,
1179
- type: this.getArtifactType(attachment.name),
1180
- path: attachment.path,
1181
- name: fileName,
1182
- contentType: attachment.contentType,
1183
- fileSize: this.getFileSize(attachment.path),
1184
- storageType: 's3',
1185
- s3Key: s3Data.key,
1186
- s3Url: s3Data.url
1187
- };
994
+ // Create upload promise that we can track
995
+ const uploadPromise = Promise.resolve().then(async () => {
996
+ try {
997
+ if (!attachment.path) {
998
+ console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
999
+ return;
1000
+ }
1001
+
1002
+ const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
1003
+
1004
+ // Skip if upload failed or file was too large
1005
+ if (!s3Data) {
1006
+ console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1007
+ return;
1008
+ }
1009
+
1010
+ const artifactData = {
1011
+ testId,
1012
+ type: this.getArtifactType(attachment.name),
1013
+ path: attachment.path,
1014
+ name: fileName,
1015
+ contentType: attachment.contentType,
1016
+ fileSize: this.getFileSize(attachment.path),
1017
+ storageType: 's3',
1018
+ s3Key: s3Data.key,
1019
+ s3Url: s3Data.url
1020
+ };
1188
1021
 
1189
- // Send artifact data to API
1190
- await this.sendToApi({
1191
- type: 'artifact',
1192
- runId: this.runId,
1193
- timestamp: new Date().toISOString(),
1194
- artifact: artifactData
1022
+ // Send artifact data to API
1023
+ await this.sendToApi({
1024
+ type: 'artifact',
1025
+ runId: this.runId,
1026
+ timestamp: new Date().toISOString(),
1027
+ artifact: artifactData
1028
+ });
1029
+
1030
+ console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1031
+ } catch (error) {
1032
+ console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
1033
+ }
1195
1034
  });
1196
1035
 
1197
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1036
+ // Track this upload and ensure cleanup on completion
1037
+ this.pendingUploads.add(uploadPromise);
1038
+ uploadPromise.finally(() => {
1039
+ this.pendingUploads.delete(uploadPromise);
1040
+ });
1041
+
1042
+ // Don't await here - let uploads happen in parallel
1043
+ // They will be awaited in onEnd
1198
1044
  } catch (error) {
1199
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
1045
+ console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, (error as Error).message);
1200
1046
  }
1201
1047
  }
1202
1048
  }
@@ -1217,8 +1063,13 @@ export class TestLensReporter implements Reporter {
1217
1063
  }));
1218
1064
 
1219
1065
  // Send to dedicated spec code blocks API endpoint
1220
- const baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
1221
- const specEndpoint = `${baseUrl}/webhook/playwright/spec-code-blocks`;
1066
+ // Extract base URL - handle both full and partial endpoint patterns
1067
+ let baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1068
+ if (baseUrl === this.config.apiEndpoint) {
1069
+ // Fallback: try alternative pattern if main pattern didn't match
1070
+ baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
1071
+ }
1072
+ const specEndpoint = `${baseUrl}/api/v1/webhook/playwright/spec-code-blocks`;
1222
1073
 
1223
1074
  await this.axiosInstance.post(specEndpoint, {
1224
1075
  filePath: path.relative(process.cwd(), specPath),
@@ -1236,58 +1087,7 @@ export class TestLensReporter implements Reporter {
1236
1087
  return;
1237
1088
  }
1238
1089
 
1239
- // Check for SSL certificate errors
1240
- const isSslError = error?.code === 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY' ||
1241
- error?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
1242
- error?.code === 'CERT_HAS_EXPIRED' ||
1243
- error?.code === 'SELF_SIGNED_CERT_IN_CHAIN' ||
1244
- error?.message?.includes('certificate') ||
1245
- error?.message?.includes('SSL') ||
1246
- error?.message?.includes('TLS');
1247
-
1248
- if (isSslError) {
1249
- console.error('\n' + '='.repeat(80));
1250
- console.error('❌ SSL Certificate Error');
1251
- console.error('='.repeat(80));
1252
- console.error('');
1253
- console.error('Failed to send spec code blocks due to SSL certificate issue.');
1254
- console.error('');
1255
- console.error('The reporter automatically attempts to detect and use system CA certificates.');
1256
- console.error('If this error persists, it may indicate:');
1257
- console.error(' - Missing or outdated system CA certificates');
1258
- console.error(' - Corporate proxy with custom CA certificate');
1259
- console.error(' - Incomplete certificate chain from the server');
1260
- console.error('');
1261
- console.error('Error details:');
1262
- console.error(` Code: ${error?.code || 'Unknown'}`);
1263
- console.error(` Message: ${error?.message || 'Unknown error'}`);
1264
- console.error('');
1265
- console.error('Possible solutions:');
1266
- console.error('');
1267
- console.error('1. Update your system\'s CA certificate store:');
1268
- console.error(' - Windows: Update Windows root certificates via Windows Update');
1269
- console.error(' - macOS: Run: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <certificate.pem>');
1270
- console.error(' - Linux: Update ca-certificates package: sudo apt-get update && sudo apt-get install ca-certificates');
1271
- console.error('');
1272
- console.error('2. If you have a custom CA certificate file (e.g., from corporate proxy),');
1273
- console.error(' you can specify it in your config (optional):');
1274
- console.error(' reporter: [');
1275
- console.error(' [\'@testlens/playwright-reporter\', {');
1276
- console.error(' caCertificate: \'/path/to/your/ca-certificate.pem\'');
1277
- console.error(' }]');
1278
- console.error(' ]');
1279
- console.error('');
1280
- console.error('3. Contact your network administrator if you\'re behind a corporate proxy');
1281
- console.error(' that uses a custom CA certificate.');
1282
- console.error('');
1283
- console.error('⚠️ WARNING: Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables SSL verification');
1284
- console.error(' and is insecure. Only use this as a last resort in development.');
1285
- console.error('');
1286
- console.error('='.repeat(80));
1287
- console.error('');
1288
- } else {
1289
- console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
1290
- }
1090
+ console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
1291
1091
  }
1292
1092
  }
1293
1093
 
@@ -1504,8 +1304,8 @@ export class TestLensReporter implements Reporter {
1504
1304
  maxContentLength: Infinity,
1505
1305
  maxBodyLength: Infinity,
1506
1306
  timeout: Math.max(600000, Math.ceil(fileSize / (1024 * 1024)) * 10000), // 10s per MB, min 10 minutes - increased for large trace files
1507
- // Use the same HTTPS agent with CA certificates for S3 uploads
1508
- httpsAgent: (this.axiosInstance.defaults as any).httpsAgent
1307
+ // Don't use custom HTTPS agent for S3 uploads
1308
+ httpsAgent: undefined
1509
1309
  });
1510
1310
 
1511
1311
  if (uploadResponse.status !== 200) {
@@ -1556,7 +1356,7 @@ export class TestLensReporter implements Reporter {
1556
1356
 
1557
1357
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1558
1358
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1559
- console.error('\\n' + '='.repeat(80));
1359
+ console.error('\n' + '='.repeat(80));
1560
1360
 
1561
1361
  if (errorData?.error === 'test_cases_limit_reached') {
1562
1362
  console.error('❌ TESTLENS ERROR: Test Cases Limit Reached');
@@ -1637,26 +1437,8 @@ export class TestLensReporter implements Reporter {
1637
1437
  return contentTypes[ext] || 'application/octet-stream';
1638
1438
  }
1639
1439
 
1640
- private generateS3Key(runId: string, testId: string, fileName: string): string {
1641
- const date = new Date().toISOString().slice(0, 10);
1642
- const safeTestId = this.sanitizeForS3(testId);
1643
- const safeFileName = this.sanitizeForS3(fileName);
1644
- const ext = path.extname(fileName);
1645
- const baseName = path.basename(fileName, ext);
1646
-
1647
- return `test-artifacts/${date}/${runId}/${safeTestId}/${safeFileName}${ext}`;
1648
- }
1649
-
1650
- private sanitizeForS3(value: string): string {
1651
- return value
1652
- .replace(/[\/:*?"<>|]/g, '-')
1653
- .replace(/[-\u001f\u007f]/g, '-')
1654
- .replace(/[^-~]/g, '-')
1655
- .replace(/\s+/g, '-')
1656
- .replace(/[_]/g, '-')
1657
- .replace(/-+/g, '-')
1658
- .replace(/^-|-$/g, '');
1659
- }
1440
+ // Note: S3 key generation and sanitization are handled server-side
1441
+ // generateS3Key() and sanitizeForS3() methods removed as they were not used
1660
1442
 
1661
1443
  private getFileSize(filePath: string): number {
1662
1444
  try {
@@ -1670,3 +1452,4 @@ export class TestLensReporter implements Reporter {
1670
1452
  }
1671
1453
 
1672
1454
  export default TestLensReporter;
1455
+