@alternative-path/testlens-playwright-reporter 0.4.3 → 0.4.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.
package/index.ts CHANGED
@@ -6,7 +6,12 @@ import * as https from 'https';
6
6
  import axios, { AxiosInstance } from 'axios';
7
7
  import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
8
8
  import { execSync } from 'child_process';
9
- import FormData from 'form-data';
9
+ import pino from 'pino';
10
+
11
+ // Create pino logger instance
12
+ const logger = pino({
13
+ level: process.env.LOG_LEVEL || 'info'
14
+ });
10
15
 
11
16
  // Lazy-load mime module to support ESM
12
17
  let mimeModule: any = null;
@@ -113,6 +118,7 @@ export interface RunMetadata {
113
118
  os: string;
114
119
  playwrightVersion: string;
115
120
  nodeVersion: string;
121
+ testlensVersion?: string;
116
122
  gitInfo?: GitInfo | null;
117
123
  shardInfo?: {
118
124
  current: number;
@@ -184,6 +190,7 @@ export class TestLensReporter implements Reporter {
184
190
  private testMap: Map<string, TestData>;
185
191
  private runCreationFailed: boolean = false; // Track if run creation failed due to limits
186
192
  private cliArgs: Record<string, any> = {}; // Store CLI args separately
193
+ private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
187
194
 
188
195
  /**
189
196
  * Parse custom metadata from environment variables
@@ -212,10 +219,10 @@ export class TestLensReporter implements Reporter {
212
219
  // For testlensBuildTag, support comma-separated values
213
220
  if (key === 'testlensBuildTag' && value.includes(',')) {
214
221
  customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
215
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
222
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
216
223
  } else {
217
224
  customArgs[key] = value;
218
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
225
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
219
226
  }
220
227
  break; // Use first match
221
228
  }
@@ -255,6 +262,8 @@ export class TestLensReporter implements Reporter {
255
262
  flushInterval: options.flushInterval || 5000,
256
263
  retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
257
264
  timeout: options.timeout || 60000,
265
+ rejectUnauthorized: options.rejectUnauthorized,
266
+ ignoreSslErrors: options.ignoreSslErrors,
258
267
  customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
259
268
  } as Required<TestLensReporterConfig>;
260
269
 
@@ -263,27 +272,36 @@ export class TestLensReporter implements Reporter {
263
272
  }
264
273
 
265
274
  if (apiKey !== options.apiKey) {
266
- console.log('✓ Using API key from environment variable');
275
+ logger.info('✓ Using API key from environment variable');
276
+ }
277
+
278
+ // Default environment to allow self-signed certs unless explicitly set
279
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
280
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
267
281
  }
268
282
 
269
283
  // Determine SSL validation behavior
270
- let rejectUnauthorized = true; // Default to secure
284
+ let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
271
285
 
272
- // Check various ways SSL validation can be disabled (in order of precedence)
273
- if (this.config.ignoreSslErrors) {
274
- // Explicit configuration option
286
+ // Check various ways SSL validation can be disabled or enforced (in order of precedence)
287
+ if (this.config.ignoreSslErrors === true) {
275
288
  rejectUnauthorized = false;
276
- console.log('⚠️ SSL certificate validation disabled via ignoreSslErrors option');
289
+ logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
277
290
  } else if (this.config.rejectUnauthorized === false) {
278
- // Explicit configuration option
279
291
  rejectUnauthorized = false;
280
- console.log('⚠️ SSL certificate validation disabled via rejectUnauthorized option');
292
+ logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
293
+ } else if (this.config.rejectUnauthorized === true) {
294
+ rejectUnauthorized = true;
281
295
  } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
282
- // Environment variable override
283
296
  rejectUnauthorized = false;
284
- console.log('⚠️ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
297
+ logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
298
+ } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
299
+ rejectUnauthorized = true;
285
300
  }
286
301
 
302
+ // Mirror the resolved value so all HTTPS requests in this process follow it
303
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
304
+
287
305
  // Set up axios instance with retry logic and enhanced SSL handling
288
306
  this.axiosInstance = axios.create({
289
307
  baseURL: this.config.apiEndpoint,
@@ -331,11 +349,11 @@ export class TestLensReporter implements Reporter {
331
349
 
332
350
  // Log custom metadata if any
333
351
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
334
- console.log('\n📋 Custom Metadata Detected:');
352
+ logger.info('\n[METADATA] Custom Metadata Detected:');
335
353
  Object.entries(this.config.customMetadata).forEach(([key, value]) => {
336
- console.log(` ${key}: ${value}`);
354
+ logger.info(` ${key}: ${value}`);
337
355
  });
338
- console.log('');
356
+ logger.info('');
339
357
  }
340
358
  }
341
359
 
@@ -347,7 +365,8 @@ export class TestLensReporter implements Reporter {
347
365
  browser: 'multiple',
348
366
  os: `${os.type()} ${os.release()}`,
349
367
  playwrightVersion: this.getPlaywrightVersion(),
350
- nodeVersion: process.version
368
+ nodeVersion: process.version,
369
+ testlensVersion: this.getTestLensVersion()
351
370
  };
352
371
 
353
372
  // Add custom metadata if provided
@@ -374,6 +393,15 @@ export class TestLensReporter implements Reporter {
374
393
  }
375
394
  }
376
395
 
396
+ private getTestLensVersion(): string {
397
+ try {
398
+ const testlensPackage = require('./package.json');
399
+ return testlensPackage.version;
400
+ } catch (error) {
401
+ return 'unknown';
402
+ }
403
+ }
404
+
377
405
  private normalizeTestStatus(status: string): string {
378
406
  // Treat timeout as failed for consistency with analytics
379
407
  if (status === 'timedOut') {
@@ -397,22 +425,22 @@ export class TestLensReporter implements Reporter {
397
425
  async onBegin(config: FullConfig, suite: Suite): Promise<void> {
398
426
  // Show Build Name if provided, otherwise show Run ID
399
427
  if (this.runMetadata.testlensBuildName) {
400
- console.log(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
401
- console.log(` Run ID: ${this.runId}`);
428
+ logger.info(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
429
+ logger.info(` Run ID: ${this.runId}`);
402
430
  } else {
403
- console.log(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
431
+ logger.info(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
404
432
  }
405
433
 
406
434
  // Collect Git information if enabled
407
435
  if (this.config.enableGitInfo) {
408
436
  this.runMetadata.gitInfo = await this.collectGitInfo();
409
437
  if (this.runMetadata.gitInfo) {
410
- console.log(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
438
+ logger.info(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
411
439
  } else {
412
- console.log(`⚠️ Git info collection returned null - not in a git repository or git not available`);
440
+ logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
413
441
  }
414
442
  } else {
415
- console.log(`ℹ️ Git info collection disabled (enableGitInfo: false)`);
443
+ logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
416
444
  }
417
445
 
418
446
  // Add shard information if available
@@ -434,7 +462,7 @@ export class TestLensReporter implements Reporter {
434
462
 
435
463
  async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
436
464
  // Log which test is starting
437
- console.log(`\n▶️ Running test: ${test.title}`);
465
+ logger.info(`\n[TEST] Running test: ${test.title}`);
438
466
 
439
467
  const specPath = test.location.file;
440
468
  const specKey = `${specPath}-${test.parent.title}`;
@@ -515,11 +543,11 @@ export class TestLensReporter implements Reporter {
515
543
  const testId = this.getTestId(test);
516
544
  let testData = this.testMap.get(testId);
517
545
 
518
- console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
546
+ logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
519
547
 
520
548
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
521
549
  if (!testData) {
522
- console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
550
+ logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
523
551
  // Create spec data if not exists (skipped tests might not have spec data either)
524
552
  const specPath = test.location.file;
525
553
  const specKey = `${specPath}-${test.parent.title}`;
@@ -683,25 +711,21 @@ export class TestLensReporter implements Reporter {
683
711
  return testError;
684
712
  });
685
713
 
686
- // Only send testEnd event after final retry attempt
687
- // If test passed or this is the last retry, send the event
688
- const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
689
-
690
- if (isFinalAttempt) {
691
- console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
692
- // Send test end event to API and get response
693
- const testEndResponse = await this.sendToApi({
694
- type: 'testEnd',
695
- runId: this.runId,
696
- timestamp: new Date().toISOString(),
697
- test: testData
698
- });
714
+ // Send testEnd event for all tests, regardless of status
715
+ // This ensures tests that are interrupted or have unexpected statuses are properly recorded
716
+ logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
717
+ // Send test end event to API and get response
718
+ const testEndResponse = await this.sendToApi({
719
+ type: 'testEnd',
720
+ runId: this.runId,
721
+ timestamp: new Date().toISOString(),
722
+ test: testData
723
+ });
699
724
 
700
- // Handle artifacts (test case is now guaranteed to be in database)
701
- if (this.config.enableArtifacts) {
702
- // Pass test case DB ID if available for faster lookups
703
- await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
704
- }
725
+ // Handle artifacts (test case is now guaranteed to be in database)
726
+ if (this.config.enableArtifacts) {
727
+ // Pass test case DB ID if available for faster lookups
728
+ await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
705
729
  }
706
730
  }
707
731
 
@@ -718,13 +742,33 @@ export class TestLensReporter implements Reporter {
718
742
  }
719
743
 
720
744
  // Check if all tests in spec are complete
745
+ // Only consider tests that were actually executed (have testData)
721
746
  const remainingTests = test.parent.tests.filter((t: any) => {
722
747
  const tId = this.getTestId(t);
723
748
  const tData = this.testMap.get(tId);
724
- return !tData || !tData.endTime;
749
+ // If testData exists but no endTime, it's still running
750
+ return tData && !tData.endTime;
725
751
  });
726
752
 
727
753
  if (remainingTests.length === 0) {
754
+ // Determine final spec status based on all executed tests
755
+ const executedTests = test.parent.tests
756
+ .map((t: any) => {
757
+ const tId = this.getTestId(t);
758
+ return this.testMap.get(tId);
759
+ })
760
+ .filter((tData: TestData | undefined): tData is TestData => !!tData);
761
+
762
+ if (executedTests.length > 0) {
763
+ const allTestStatuses = executedTests.map(tData => tData.status);
764
+ if (allTestStatuses.every(status => status === 'passed')) {
765
+ specData.status = 'passed';
766
+ } else if (allTestStatuses.some(status => status === 'failed')) {
767
+ specData.status = 'failed';
768
+ } else if (allTestStatuses.every(status => status === 'skipped')) {
769
+ specData.status = 'skipped';
770
+ }
771
+ }
728
772
  // Aggregate tags from all tests in this spec
729
773
  const allTags = new Set<string>();
730
774
  test.parent.tests.forEach((t: any) => {
@@ -757,6 +801,17 @@ export class TestLensReporter implements Reporter {
757
801
  this.runMetadata.endTime = new Date().toISOString();
758
802
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
759
803
 
804
+ // Wait for all pending artifact uploads to complete before sending runEnd
805
+ if (this.pendingUploads.size > 0) {
806
+ logger.info(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
807
+ try {
808
+ await Promise.all(Array.from(this.pendingUploads));
809
+ logger.info(`[OK] All artifact uploads completed`);
810
+ } catch (error) {
811
+ logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
812
+ }
813
+ }
814
+
760
815
  // Calculate final stats
761
816
  const totalTests = Array.from(this.testMap.values()).length;
762
817
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -788,12 +843,12 @@ export class TestLensReporter implements Reporter {
788
843
 
789
844
  // Show Build Name if provided, otherwise show Run ID
790
845
  if (this.runMetadata.testlensBuildName) {
791
- console.log(`📊 TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
792
- console.log(` Run ID: ${this.runId}`);
846
+ logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
847
+ logger.info(` Run ID: ${this.runId}`);
793
848
  } else {
794
- console.log(`📊 TestLens Report completed - Run ID: ${this.runId}`);
849
+ logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
795
850
  }
796
- console.log(`🎯 Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
851
+ logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
797
852
  }
798
853
 
799
854
  private async sendToApi(payload: any): Promise<any> {
@@ -809,7 +864,7 @@ export class TestLensReporter implements Reporter {
809
864
  }
810
865
  });
811
866
  if (this.config.enableRealTimeStream) {
812
- console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
867
+ logger.info(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
813
868
  }
814
869
  // Return response data for caller to use
815
870
  return response.data;
@@ -824,25 +879,25 @@ export class TestLensReporter implements Reporter {
824
879
  this.runCreationFailed = true;
825
880
  }
826
881
 
827
- console.error('\n' + '='.repeat(80));
882
+ logger.error('\n' + '='.repeat(80));
828
883
  if (errorData?.limit_type === 'test_cases') {
829
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
884
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
830
885
  } else if (errorData?.limit_type === 'test_runs') {
831
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
886
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
832
887
  } else {
833
- console.error(' TESTLENS ERROR: Plan Limit Reached');
888
+ logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
834
889
  }
835
- console.error('='.repeat(80));
836
- console.error('');
837
- console.error(errorData?.message || 'You have reached your plan limit.');
838
- console.error('');
839
- console.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
840
- console.error('');
841
- console.error('To continue, please upgrade your plan.');
842
- console.error('Contact: support@alternative-path.com');
843
- console.error('');
844
- console.error('='.repeat(80));
845
- console.error('');
890
+ logger.error('='.repeat(80));
891
+ logger.error('');
892
+ logger.error(errorData?.message || 'You have reached your plan limit.');
893
+ logger.error('');
894
+ logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
895
+ logger.error('');
896
+ logger.error('To continue, please upgrade your plan.');
897
+ logger.error('Contact: support@alternative-path.com');
898
+ logger.error('');
899
+ logger.error('='.repeat(80));
900
+ logger.error('');
846
901
  return; // Don't log the full error object for limit errors
847
902
  }
848
903
 
@@ -850,36 +905,37 @@ export class TestLensReporter implements Reporter {
850
905
  if (status === 401) {
851
906
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
852
907
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
853
- console.error('\n' + '='.repeat(80));
908
+ logger.error('\n' + '='.repeat(80));
854
909
 
855
910
  if (errorData?.error === 'test_cases_limit_reached') {
856
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
911
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
857
912
  } else if (errorData?.error === 'test_runs_limit_reached') {
858
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
913
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
859
914
  } else {
860
- console.error(' TESTLENS ERROR: Your trial plan has ended');
915
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
861
916
  }
862
917
 
863
- console.error('='.repeat(80));
864
- console.error('');
865
- console.error(errorData?.message || 'Your trial period has expired.');
866
- console.error('');
867
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
868
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
869
- console.error('');
918
+ logger.error('='.repeat(80));
919
+ logger.error('');
920
+ logger.error(errorData?.message || 'Your trial period has expired.');
921
+ logger.error('');
922
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
923
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
924
+ logger.error('');
870
925
  if (errorData?.trial_end_date) {
871
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
872
- console.error('');
926
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
927
+ logger.error('');
873
928
  }
874
- console.error('='.repeat(80));
875
- console.error('');
929
+ logger.error('='.repeat(80));
930
+ logger.error('');
876
931
  } else {
877
- console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
932
+ logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
878
933
  }
879
934
  } else if (status !== 403) {
880
935
  // Log other errors (but not 403 which we handled above)
881
- console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
882
- message: error?.message || 'Unknown error',
936
+ logger.error({
937
+ message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
938
+ error: error?.message || 'Unknown error',
883
939
  status: status,
884
940
  statusText: error?.response?.statusText,
885
941
  data: errorData,
@@ -910,13 +966,13 @@ export class TestLensReporter implements Reporter {
910
966
 
911
967
  // Skip video if disabled in config
912
968
  if (isVideo && !this.config.enableVideo) {
913
- console.log(`⏭️ Skipping video artifact ${attachment.name} - video capture disabled in config`);
969
+ logger.info(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
914
970
  continue;
915
971
  }
916
972
 
917
973
  // Skip screenshot if disabled in config
918
974
  if (isScreenshot && !this.config.enableScreenshot) {
919
- console.log(`⏭️ Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
975
+ logger.info(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
920
976
  continue;
921
977
  }
922
978
 
@@ -952,37 +1008,58 @@ export class TestLensReporter implements Reporter {
952
1008
  }
953
1009
 
954
1010
  // Upload to S3 first (pass DB ID if available for faster lookup)
955
- const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
956
-
957
- // Skip if upload failed or file was too large
958
- if (!s3Data) {
959
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
960
- continue;
961
- }
962
-
963
- const artifactData = {
964
- testId,
965
- type: this.getArtifactType(attachment.name),
966
- path: attachment.path,
967
- name: fileName,
968
- contentType: attachment.contentType,
969
- fileSize: this.getFileSize(attachment.path),
970
- storageType: 's3',
971
- s3Key: s3Data.key,
972
- s3Url: s3Data.url
973
- };
1011
+ // Create upload promise that we can track
1012
+ const uploadPromise = Promise.resolve().then(async () => {
1013
+ try {
1014
+ if (!attachment.path) {
1015
+ logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
1016
+ return;
1017
+ }
1018
+
1019
+ const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
1020
+
1021
+ // Skip if upload failed or file was too large
1022
+ if (!s3Data) {
1023
+ logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1024
+ return;
1025
+ }
1026
+
1027
+ const artifactData = {
1028
+ testId,
1029
+ type: this.getArtifactType(attachment.name),
1030
+ path: attachment.path,
1031
+ name: fileName,
1032
+ contentType: attachment.contentType,
1033
+ fileSize: this.getFileSize(attachment.path),
1034
+ storageType: 's3',
1035
+ s3Key: s3Data.key,
1036
+ s3Url: s3Data.url
1037
+ };
974
1038
 
975
- // Send artifact data to API
976
- await this.sendToApi({
977
- type: 'artifact',
978
- runId: this.runId,
979
- timestamp: new Date().toISOString(),
980
- artifact: artifactData
1039
+ // Send artifact data to API
1040
+ await this.sendToApi({
1041
+ type: 'artifact',
1042
+ runId: this.runId,
1043
+ timestamp: new Date().toISOString(),
1044
+ artifact: artifactData
1045
+ });
1046
+
1047
+ logger.info(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1048
+ } catch (error) {
1049
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
1050
+ }
981
1051
  });
982
1052
 
983
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1053
+ // Track this upload and ensure cleanup on completion
1054
+ this.pendingUploads.add(uploadPromise);
1055
+ uploadPromise.finally(() => {
1056
+ this.pendingUploads.delete(uploadPromise);
1057
+ });
1058
+
1059
+ // Don't await here - let uploads happen in parallel
1060
+ // They will be awaited in onEnd
984
1061
  } catch (error) {
985
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
1062
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
986
1063
  }
987
1064
  }
988
1065
  }
@@ -1003,8 +1080,13 @@ export class TestLensReporter implements Reporter {
1003
1080
  }));
1004
1081
 
1005
1082
  // Send to dedicated spec code blocks API endpoint
1006
- const baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
1007
- const specEndpoint = `${baseUrl}/webhook/playwright/spec-code-blocks`;
1083
+ // Extract base URL - handle both full and partial endpoint patterns
1084
+ let baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1085
+ if (baseUrl === this.config.apiEndpoint) {
1086
+ // Fallback: try alternative pattern if main pattern didn't match
1087
+ baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
1088
+ }
1089
+ const specEndpoint = `${baseUrl}/api/v1/webhook/playwright/spec-code-blocks`;
1008
1090
 
1009
1091
  await this.axiosInstance.post(specEndpoint, {
1010
1092
  filePath: path.relative(process.cwd(), specPath),
@@ -1012,17 +1094,17 @@ export class TestLensReporter implements Reporter {
1012
1094
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
1013
1095
  });
1014
1096
 
1015
- console.log(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1097
+ logger.info(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1016
1098
  } catch (error: any) {
1017
1099
  const errorData = error?.response?.data;
1018
1100
 
1019
1101
  // Handle duplicate spec code blocks gracefully (when re-running tests)
1020
1102
  if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
1021
- console.log(`ℹ️ Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1103
+ logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1022
1104
  return;
1023
1105
  }
1024
1106
 
1025
- console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
1107
+ logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
1026
1108
  }
1027
1109
  }
1028
1110
 
@@ -1088,7 +1170,7 @@ export class TestLensReporter implements Reporter {
1088
1170
 
1089
1171
  return blocks;
1090
1172
  } catch (error: any) {
1091
- console.error(`Failed to extract test blocks from ${filePath}:`, error.message);
1173
+ logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
1092
1174
  return [];
1093
1175
  }
1094
1176
  }
@@ -1112,7 +1194,7 @@ export class TestLensReporter implements Reporter {
1112
1194
  }
1113
1195
  } catch (e) {
1114
1196
  // Remote info is optional - handle gracefully
1115
- console.log('ℹ️ No git remote configured, skipping remote info');
1197
+ logger.info('[INFO] No git remote configured, skipping remote info');
1116
1198
  }
1117
1199
 
1118
1200
  const isDirty = execSync('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
@@ -1193,7 +1275,7 @@ export class TestLensReporter implements Reporter {
1193
1275
  const fileSize = this.getFileSize(filePath);
1194
1276
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
1195
1277
 
1196
- console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1278
+ logger.info(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1197
1279
 
1198
1280
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1199
1281
 
@@ -1225,7 +1307,7 @@ export class TestLensReporter implements Reporter {
1225
1307
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1226
1308
 
1227
1309
  // Step 2: Upload directly to S3 using presigned URL
1228
- console.log(`⬆️ [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1310
+ logger.info(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1229
1311
 
1230
1312
  const fileBuffer = fs.readFileSync(filePath);
1231
1313
 
@@ -1247,7 +1329,7 @@ export class TestLensReporter implements Reporter {
1247
1329
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1248
1330
  }
1249
1331
 
1250
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1332
+ logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1251
1333
 
1252
1334
  // Step 3: Confirm upload with server to save metadata
1253
1335
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
@@ -1273,7 +1355,7 @@ export class TestLensReporter implements Reporter {
1273
1355
 
1274
1356
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1275
1357
  const artifact = confirmResponse.data.artifact;
1276
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1358
+ logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1277
1359
  return {
1278
1360
  key: s3Key,
1279
1361
  url: artifact.s3Url,
@@ -1291,29 +1373,29 @@ export class TestLensReporter implements Reporter {
1291
1373
 
1292
1374
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1293
1375
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1294
- console.error('\\n' + '='.repeat(80));
1376
+ logger.error('\n' + '='.repeat(80));
1295
1377
 
1296
1378
  if (errorData?.error === 'test_cases_limit_reached') {
1297
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
1379
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1298
1380
  } else if (errorData?.error === 'test_runs_limit_reached') {
1299
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
1381
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1300
1382
  } else {
1301
- console.error(' TESTLENS ERROR: Your trial plan has ended');
1383
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1302
1384
  }
1303
1385
 
1304
- console.error('='.repeat(80));
1305
- console.error('');
1306
- console.error(errorData?.message || 'Your trial period has expired.');
1307
- console.error('');
1308
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
1309
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1310
- console.error('');
1386
+ logger.error('='.repeat(80));
1387
+ logger.error('');
1388
+ logger.error(errorData?.message || 'Your trial period has expired.');
1389
+ logger.error('');
1390
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1391
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1392
+ logger.error('');
1311
1393
  if (errorData?.trial_end_date) {
1312
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1313
- console.error('');
1394
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1395
+ logger.error('');
1314
1396
  }
1315
- console.error('='.repeat(80));
1316
- console.error('');
1397
+ logger.error('='.repeat(80));
1398
+ logger.error('');
1317
1399
  return null;
1318
1400
  }
1319
1401
  }
@@ -1331,9 +1413,9 @@ export class TestLensReporter implements Reporter {
1331
1413
  errorMsg = `Access denied (403) - presigned URL may have expired`;
1332
1414
  }
1333
1415
 
1334
- console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
1416
+ logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1335
1417
  if (error.response?.data) {
1336
- console.error('Error details:', error.response.data);
1418
+ logger.error({ errorDetails: error.response.data }, 'Error details');
1337
1419
  }
1338
1420
 
1339
1421
  // Don't throw, just return null to continue with other artifacts
@@ -1352,7 +1434,7 @@ export class TestLensReporter implements Reporter {
1352
1434
  return mimeType || 'application/octet-stream';
1353
1435
  }
1354
1436
  } catch (error: any) {
1355
- console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
1437
+ logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
1356
1438
  }
1357
1439
  // Fallback to basic content type mapping
1358
1440
  const contentTypes: Record<string, string> = {
@@ -1372,36 +1454,19 @@ export class TestLensReporter implements Reporter {
1372
1454
  return contentTypes[ext] || 'application/octet-stream';
1373
1455
  }
1374
1456
 
1375
- private generateS3Key(runId: string, testId: string, fileName: string): string {
1376
- const date = new Date().toISOString().slice(0, 10);
1377
- const safeTestId = this.sanitizeForS3(testId);
1378
- const safeFileName = this.sanitizeForS3(fileName);
1379
- const ext = path.extname(fileName);
1380
- const baseName = path.basename(fileName, ext);
1381
-
1382
- return `test-artifacts/${date}/${runId}/${safeTestId}/${safeFileName}${ext}`;
1383
- }
1384
-
1385
- private sanitizeForS3(value: string): string {
1386
- return value
1387
- .replace(/[\/:*?"<>|]/g, '-')
1388
- .replace(/[-\u001f\u007f]/g, '-')
1389
- .replace(/[^-~]/g, '-')
1390
- .replace(/\s+/g, '-')
1391
- .replace(/[_]/g, '-')
1392
- .replace(/-+/g, '-')
1393
- .replace(/^-|-$/g, '');
1394
- }
1457
+ // Note: S3 key generation and sanitization are handled server-side
1458
+ // generateS3Key() and sanitizeForS3() methods removed as they were not used
1395
1459
 
1396
1460
  private getFileSize(filePath: string): number {
1397
1461
  try {
1398
1462
  const stats = fs.statSync(filePath);
1399
1463
  return stats.size;
1400
1464
  } catch (error) {
1401
- console.warn(`Could not get file size for ${filePath}:`, (error as Error).message);
1465
+ logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
1402
1466
  return 0;
1403
1467
  }
1404
1468
  }
1405
1469
  }
1406
1470
 
1407
1471
  export default TestLensReporter;
1472
+