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

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,71 @@ 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
+ const isVerboseLogs =
12
+ process.env.TESTLENS_LOG_VERBOSE === '1' ||
13
+ process.env.TESTLENS_LOG_VERBOSE === 'true';
14
+ const noisyInfoPrefixes = [
15
+ '[OK]',
16
+ '[UPLOAD]',
17
+ '[ARTIFACT]',
18
+ '[SKIP]',
19
+ '[TestLens] onTestEnd called',
20
+ '[TestLens] Sending testEnd'
21
+ ];
22
+ const noisyInfoSubstrings = [
23
+ 'Sent runStart event to TestLens',
24
+ 'Sent specStart event to TestLens',
25
+ 'Sent testStart event to TestLens',
26
+ 'Sent testEnd event to TestLens',
27
+ 'Sent runEnd event to TestLens',
28
+ 'Sent artifact event to TestLens',
29
+ 'S3 upload completed',
30
+ 'Upload confirmed',
31
+ 'Uploading ',
32
+ 'Processed artifact',
33
+ 'Spec code blocks',
34
+ '\n[METADATA]',
35
+ 'Custom Metadata Detected',
36
+ 'Using API key from environment variable'
37
+ ];
38
+
39
+ const shouldSuppressInfo = (msg: string): boolean => {
40
+ if (isVerboseLogs) return false;
41
+ if (!msg) return false;
42
+ return (
43
+ noisyInfoPrefixes.some(prefix => msg.startsWith(prefix)) ||
44
+ noisyInfoSubstrings.some(sub => msg.includes(sub))
45
+ );
46
+ };
47
+
48
+ // Create pino logger instance
49
+ const logger = pino({
50
+ level: process.env.TESTLENS_LOG_LEVEL || process.env.LOG_LEVEL || 'info',
51
+ base: null, // remove pid/hostname
52
+ timestamp: false, // remove time
53
+ formatters: {
54
+ level: () => ({}) // remove level
55
+ },
56
+ hooks: {
57
+ logMethod(args, method, level) {
58
+ // pino level is numeric (info = 30)
59
+ if (level === 30) {
60
+ const msg =
61
+ typeof args[0] === 'string'
62
+ ? args[0]
63
+ : typeof args[1] === 'string'
64
+ ? args[1]
65
+ : '';
66
+ if (shouldSuppressInfo(msg)) {
67
+ return;
68
+ }
69
+ }
70
+ return method.apply(this, args as any);
71
+ }
72
+ }
73
+ });
10
74
 
11
75
  // Lazy-load mime module to support ESM
12
76
  let mimeModule: any = null;
@@ -186,6 +250,12 @@ export class TestLensReporter implements Reporter {
186
250
  private runCreationFailed: boolean = false; // Track if run creation failed due to limits
187
251
  private cliArgs: Record<string, any> = {}; // Store CLI args separately
188
252
  private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
253
+ private artifactStats = {
254
+ uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
255
+ skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
256
+ failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
257
+ };
258
+ private artifactsSeen: number = 0;
189
259
 
190
260
  /**
191
261
  * Parse custom metadata from environment variables
@@ -214,10 +284,10 @@ export class TestLensReporter implements Reporter {
214
284
  // For testlensBuildTag, support comma-separated values
215
285
  if (key === 'testlensBuildTag' && value.includes(',')) {
216
286
  customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
217
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
287
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
218
288
  } else {
219
289
  customArgs[key] = value;
220
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
290
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
221
291
  }
222
292
  break; // Use first match
223
293
  }
@@ -257,6 +327,8 @@ export class TestLensReporter implements Reporter {
257
327
  flushInterval: options.flushInterval || 5000,
258
328
  retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
259
329
  timeout: options.timeout || 60000,
330
+ rejectUnauthorized: options.rejectUnauthorized,
331
+ ignoreSslErrors: options.ignoreSslErrors,
260
332
  customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
261
333
  } as Required<TestLensReporterConfig>;
262
334
 
@@ -265,27 +337,36 @@ export class TestLensReporter implements Reporter {
265
337
  }
266
338
 
267
339
  if (apiKey !== options.apiKey) {
268
- console.log('✓ Using API key from environment variable');
340
+ logger.info('✓ Using API key from environment variable');
341
+ }
342
+
343
+ // Default environment to allow self-signed certs unless explicitly set
344
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
345
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
269
346
  }
270
347
 
271
348
  // Determine SSL validation behavior
272
- let rejectUnauthorized = true; // Default to secure
349
+ let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
273
350
 
274
- // Check various ways SSL validation can be disabled (in order of precedence)
275
- if (this.config.ignoreSslErrors) {
276
- // Explicit configuration option
351
+ // Check various ways SSL validation can be disabled or enforced (in order of precedence)
352
+ if (this.config.ignoreSslErrors === true) {
277
353
  rejectUnauthorized = false;
278
- console.log('⚠️ SSL certificate validation disabled via ignoreSslErrors option');
354
+ logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
279
355
  } else if (this.config.rejectUnauthorized === false) {
280
- // Explicit configuration option
281
356
  rejectUnauthorized = false;
282
- console.log('⚠️ SSL certificate validation disabled via rejectUnauthorized option');
357
+ logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
358
+ } else if (this.config.rejectUnauthorized === true) {
359
+ rejectUnauthorized = true;
283
360
  } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
284
- // Environment variable override
285
361
  rejectUnauthorized = false;
286
- console.log('⚠️ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
362
+ logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
363
+ } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
364
+ rejectUnauthorized = true;
287
365
  }
288
366
 
367
+ // Mirror the resolved value so all HTTPS requests in this process follow it
368
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
369
+
289
370
  // Set up axios instance with retry logic and enhanced SSL handling
290
371
  this.axiosInstance = axios.create({
291
372
  baseURL: this.config.apiEndpoint,
@@ -333,11 +414,11 @@ export class TestLensReporter implements Reporter {
333
414
 
334
415
  // Log custom metadata if any
335
416
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
336
- console.log('\n📋 Custom Metadata Detected:');
417
+ logger.info('\n[METADATA] Custom Metadata Detected:');
337
418
  Object.entries(this.config.customMetadata).forEach(([key, value]) => {
338
- console.log(` ${key}: ${value}`);
419
+ logger.info(` ${key}: ${value}`);
339
420
  });
340
- console.log('');
421
+ logger.info('');
341
422
  }
342
423
  }
343
424
 
@@ -407,24 +488,25 @@ export class TestLensReporter implements Reporter {
407
488
  }
408
489
 
409
490
  async onBegin(config: FullConfig, suite: Suite): Promise<void> {
491
+ logger.info(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
410
492
  // Show Build Name if provided, otherwise show Run ID
411
493
  if (this.runMetadata.testlensBuildName) {
412
- console.log(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
413
- console.log(` Run ID: ${this.runId}`);
494
+ logger.info(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
495
+ logger.info(` Run ID: ${this.runId}`);
414
496
  } else {
415
- console.log(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
497
+ logger.info(`TestLens Reporter starting - Run ID: ${this.runId}`);
416
498
  }
417
499
 
418
500
  // Collect Git information if enabled
419
501
  if (this.config.enableGitInfo) {
420
502
  this.runMetadata.gitInfo = await this.collectGitInfo();
421
503
  if (this.runMetadata.gitInfo) {
422
- console.log(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
504
+ logger.info(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
423
505
  } else {
424
- console.log(`⚠️ Git info collection returned null - not in a git repository or git not available`);
506
+ logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
425
507
  }
426
508
  } else {
427
- console.log(`ℹ️ Git info collection disabled (enableGitInfo: false)`);
509
+ logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
428
510
  }
429
511
 
430
512
  // Add shard information if available
@@ -446,7 +528,7 @@ export class TestLensReporter implements Reporter {
446
528
 
447
529
  async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
448
530
  // Log which test is starting
449
- console.log(`\n▶️ Running test: ${test.title}`);
531
+ logger.info(`[TEST] Running test: ${test.title}`);
450
532
 
451
533
  const specPath = test.location.file;
452
534
  const specKey = `${specPath}-${test.parent.title}`;
@@ -527,11 +609,25 @@ export class TestLensReporter implements Reporter {
527
609
  const testId = this.getTestId(test);
528
610
  let testData = this.testMap.get(testId);
529
611
 
530
- console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
612
+ logger.info(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
613
+
614
+ if (result.attachments && result.attachments.length > 0) {
615
+ for (const attachment of result.attachments) {
616
+ const artifactType = this.getArtifactType(attachment.name);
617
+ this.artifactsSeen += 1;
618
+ if (attachment.path) {
619
+ this.bumpArtifactStat('uploaded', artifactType);
620
+ } else {
621
+ this.bumpArtifactStat('skipped', artifactType);
622
+ }
623
+ }
624
+ }
625
+
626
+ logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
531
627
 
532
628
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
533
629
  if (!testData) {
534
- console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
630
+ logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
535
631
  // Create spec data if not exists (skipped tests might not have spec data either)
536
632
  const specPath = test.location.file;
537
633
  const specKey = `${specPath}-${test.parent.title}`;
@@ -697,7 +793,7 @@ export class TestLensReporter implements Reporter {
697
793
 
698
794
  // Send testEnd event for all tests, regardless of status
699
795
  // 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}`);
796
+ logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
701
797
  // Send test end event to API and get response
702
798
  const testEndResponse = await this.sendToApi({
703
799
  type: 'testEnd',
@@ -710,6 +806,12 @@ export class TestLensReporter implements Reporter {
710
806
  if (this.config.enableArtifacts) {
711
807
  // Pass test case DB ID if available for faster lookups
712
808
  await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
809
+ } else if (result.attachments && result.attachments.length > 0) {
810
+ for (const attachment of result.attachments) {
811
+ const artifactType = this.getArtifactType(attachment.name);
812
+ this.artifactsSeen += 1;
813
+ this.bumpArtifactStat('skipped', artifactType);
814
+ }
713
815
  }
714
816
  }
715
817
 
@@ -787,15 +889,30 @@ export class TestLensReporter implements Reporter {
787
889
 
788
890
  // Wait for all pending artifact uploads to complete before sending runEnd
789
891
  if (this.pendingUploads.size > 0) {
790
- console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
892
+ logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
791
893
  try {
792
894
  await Promise.all(Array.from(this.pendingUploads));
793
- console.log(`✅ All artifact uploads completed`);
895
+ logger.debug(`[OK] All artifact uploads completed`);
794
896
  } catch (error) {
795
- console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
897
+ logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
796
898
  }
797
899
  }
798
900
 
901
+ const uploaded = this.artifactStats.uploaded;
902
+ const skipped = this.artifactStats.skipped;
903
+ const failed = this.artifactStats.failed;
904
+ // Always show summary so users can confirm artifact settings
905
+ const summary =
906
+ `[ARTIFACTS] seen=${this.artifactsSeen} | ` +
907
+ `uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
908
+ `skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
909
+ `failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
910
+ if (logger.levelVal > 30) {
911
+ console.log(summary);
912
+ } else {
913
+ logger.info(summary);
914
+ }
915
+
799
916
  // Calculate final stats
800
917
  const totalTests = Array.from(this.testMap.values()).length;
801
918
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -827,12 +944,12 @@ export class TestLensReporter implements Reporter {
827
944
 
828
945
  // Show Build Name if provided, otherwise show Run ID
829
946
  if (this.runMetadata.testlensBuildName) {
830
- console.log(`📊 TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
831
- console.log(` Run ID: ${this.runId}`);
947
+ logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
948
+ logger.info(` Run ID: ${this.runId}`);
832
949
  } else {
833
- console.log(`📊 TestLens Report completed - Run ID: ${this.runId}`);
950
+ logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
834
951
  }
835
- console.log(`🎯 Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
952
+ logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
836
953
  }
837
954
 
838
955
  private async sendToApi(payload: any): Promise<any> {
@@ -848,7 +965,7 @@ export class TestLensReporter implements Reporter {
848
965
  }
849
966
  });
850
967
  if (this.config.enableRealTimeStream) {
851
- console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
968
+ logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
852
969
  }
853
970
  // Return response data for caller to use
854
971
  return response.data;
@@ -863,25 +980,25 @@ export class TestLensReporter implements Reporter {
863
980
  this.runCreationFailed = true;
864
981
  }
865
982
 
866
- console.error('\n' + '='.repeat(80));
983
+ logger.error('\n' + '='.repeat(80));
867
984
  if (errorData?.limit_type === 'test_cases') {
868
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
985
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
869
986
  } else if (errorData?.limit_type === 'test_runs') {
870
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
987
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
871
988
  } else {
872
- console.error(' TESTLENS ERROR: Plan Limit Reached');
989
+ logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
873
990
  }
874
- console.error('='.repeat(80));
875
- console.error('');
876
- console.error(errorData?.message || 'You have reached your plan limit.');
877
- console.error('');
878
- console.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
879
- console.error('');
880
- console.error('To continue, please upgrade your plan.');
881
- console.error('Contact: support@alternative-path.com');
882
- console.error('');
883
- console.error('='.repeat(80));
884
- console.error('');
991
+ logger.error('='.repeat(80));
992
+ logger.error('');
993
+ logger.error(errorData?.message || 'You have reached your plan limit.');
994
+ logger.error('');
995
+ logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
996
+ logger.error('');
997
+ logger.error('To continue, please upgrade your plan.');
998
+ logger.error('Contact: support@alternative-path.com');
999
+ logger.error('');
1000
+ logger.error('='.repeat(80));
1001
+ logger.error('');
885
1002
  return; // Don't log the full error object for limit errors
886
1003
  }
887
1004
 
@@ -889,36 +1006,37 @@ export class TestLensReporter implements Reporter {
889
1006
  if (status === 401) {
890
1007
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
891
1008
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
892
- console.error('\n' + '='.repeat(80));
1009
+ logger.error('\n' + '='.repeat(80));
893
1010
 
894
1011
  if (errorData?.error === 'test_cases_limit_reached') {
895
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
1012
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
896
1013
  } else if (errorData?.error === 'test_runs_limit_reached') {
897
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
1014
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
898
1015
  } else {
899
- console.error(' TESTLENS ERROR: Your trial plan has ended');
1016
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
900
1017
  }
901
1018
 
902
- console.error('='.repeat(80));
903
- console.error('');
904
- console.error(errorData?.message || 'Your trial period has expired.');
905
- console.error('');
906
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
907
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
908
- console.error('');
1019
+ logger.error('='.repeat(80));
1020
+ logger.error('');
1021
+ logger.error(errorData?.message || 'Your trial period has expired.');
1022
+ logger.error('');
1023
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1024
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1025
+ logger.error('');
909
1026
  if (errorData?.trial_end_date) {
910
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
911
- console.error('');
1027
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1028
+ logger.error('');
912
1029
  }
913
- console.error('='.repeat(80));
914
- console.error('');
1030
+ logger.error('='.repeat(80));
1031
+ logger.error('');
915
1032
  } else {
916
- console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
1033
+ logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
917
1034
  }
918
1035
  } else if (status !== 403) {
919
1036
  // 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',
1037
+ logger.error({
1038
+ message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
1039
+ error: error?.message || 'Unknown error',
922
1040
  status: status,
923
1041
  statusText: error?.response?.statusText,
924
1042
  data: errorData,
@@ -941,21 +1059,23 @@ export class TestLensReporter implements Reporter {
941
1059
  const attachments = result.attachments;
942
1060
 
943
1061
  for (const attachment of attachments) {
1062
+ const artifactType = this.getArtifactType(attachment.name);
944
1063
  if (attachment.path) {
945
1064
  // Check if attachment should be processed based on config
946
- const artifactType = this.getArtifactType(attachment.name);
947
1065
  const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
948
1066
  const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
949
1067
 
950
1068
  // Skip video if disabled in config
951
1069
  if (isVideo && !this.config.enableVideo) {
952
- console.log(`⏭️ Skipping video artifact ${attachment.name} - video capture disabled in config`);
1070
+ logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
1071
+ this.bumpArtifactStat('skipped', artifactType);
953
1072
  continue;
954
1073
  }
955
1074
 
956
1075
  // Skip screenshot if disabled in config
957
1076
  if (isScreenshot && !this.config.enableScreenshot) {
958
- console.log(`⏭️ Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
1077
+ logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
1078
+ this.bumpArtifactStat('skipped', artifactType);
959
1079
  continue;
960
1080
  }
961
1081
 
@@ -995,7 +1115,8 @@ export class TestLensReporter implements Reporter {
995
1115
  const uploadPromise = Promise.resolve().then(async () => {
996
1116
  try {
997
1117
  if (!attachment.path) {
998
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
1118
+ logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
1119
+ this.bumpArtifactStat('skipped', artifactType);
999
1120
  return;
1000
1121
  }
1001
1122
 
@@ -1003,7 +1124,8 @@ export class TestLensReporter implements Reporter {
1003
1124
 
1004
1125
  // Skip if upload failed or file was too large
1005
1126
  if (!s3Data) {
1006
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1127
+ logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1128
+ this.bumpArtifactStat('failed', artifactType);
1007
1129
  return;
1008
1130
  }
1009
1131
 
@@ -1027,9 +1149,10 @@ export class TestLensReporter implements Reporter {
1027
1149
  artifact: artifactData
1028
1150
  });
1029
1151
 
1030
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1152
+ logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1031
1153
  } catch (error) {
1032
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
1154
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
1155
+ this.bumpArtifactStat('failed', artifactType);
1033
1156
  }
1034
1157
  });
1035
1158
 
@@ -1042,8 +1165,11 @@ export class TestLensReporter implements Reporter {
1042
1165
  // Don't await here - let uploads happen in parallel
1043
1166
  // They will be awaited in onEnd
1044
1167
  } catch (error) {
1045
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, (error as Error).message);
1168
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
1169
+ this.bumpArtifactStat('failed', artifactType);
1046
1170
  }
1171
+ } else {
1172
+ this.bumpArtifactStat('skipped', artifactType);
1047
1173
  }
1048
1174
  }
1049
1175
  }
@@ -1077,17 +1203,17 @@ export class TestLensReporter implements Reporter {
1077
1203
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
1078
1204
  });
1079
1205
 
1080
- console.log(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1206
+ logger.info(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1081
1207
  } catch (error: any) {
1082
1208
  const errorData = error?.response?.data;
1083
1209
 
1084
1210
  // Handle duplicate spec code blocks gracefully (when re-running tests)
1085
1211
  if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
1086
- console.log(`ℹ️ Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1212
+ logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1087
1213
  return;
1088
1214
  }
1089
1215
 
1090
- console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
1216
+ logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
1091
1217
  }
1092
1218
  }
1093
1219
 
@@ -1153,7 +1279,7 @@ export class TestLensReporter implements Reporter {
1153
1279
 
1154
1280
  return blocks;
1155
1281
  } catch (error: any) {
1156
- console.error(`Failed to extract test blocks from ${filePath}:`, error.message);
1282
+ logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
1157
1283
  return [];
1158
1284
  }
1159
1285
  }
@@ -1177,7 +1303,7 @@ export class TestLensReporter implements Reporter {
1177
1303
  }
1178
1304
  } catch (e) {
1179
1305
  // Remote info is optional - handle gracefully
1180
- console.log('ℹ️ No git remote configured, skipping remote info');
1306
+ logger.info('[INFO] No git remote configured, skipping remote info');
1181
1307
  }
1182
1308
 
1183
1309
  const isDirty = execSync('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
@@ -1206,6 +1332,13 @@ export class TestLensReporter implements Reporter {
1206
1332
  return 'attachment';
1207
1333
  }
1208
1334
 
1335
+ private bumpArtifactStat(stat: 'uploaded' | 'skipped' | 'failed', type: string): void {
1336
+ const bucket = this.artifactStats[stat] as Record<string, number>;
1337
+ if (bucket[type] !== undefined) {
1338
+ bucket[type] += 1;
1339
+ }
1340
+ }
1341
+
1209
1342
  private extractTags(test: TestCase): string[] {
1210
1343
  const tags: string[] = [];
1211
1344
 
@@ -1258,7 +1391,7 @@ export class TestLensReporter implements Reporter {
1258
1391
  const fileSize = this.getFileSize(filePath);
1259
1392
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
1260
1393
 
1261
- console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1394
+ logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1262
1395
 
1263
1396
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1264
1397
 
@@ -1290,7 +1423,7 @@ export class TestLensReporter implements Reporter {
1290
1423
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1291
1424
 
1292
1425
  // Step 2: Upload directly to S3 using presigned URL
1293
- console.log(`⬆️ [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1426
+ logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1294
1427
 
1295
1428
  const fileBuffer = fs.readFileSync(filePath);
1296
1429
 
@@ -1312,7 +1445,7 @@ export class TestLensReporter implements Reporter {
1312
1445
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1313
1446
  }
1314
1447
 
1315
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1448
+ logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1316
1449
 
1317
1450
  // Step 3: Confirm upload with server to save metadata
1318
1451
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
@@ -1338,7 +1471,7 @@ export class TestLensReporter implements Reporter {
1338
1471
 
1339
1472
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1340
1473
  const artifact = confirmResponse.data.artifact;
1341
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1474
+ logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1342
1475
  return {
1343
1476
  key: s3Key,
1344
1477
  url: artifact.s3Url,
@@ -1356,29 +1489,29 @@ export class TestLensReporter implements Reporter {
1356
1489
 
1357
1490
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1358
1491
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1359
- console.error('\n' + '='.repeat(80));
1492
+ logger.error('\n' + '='.repeat(80));
1360
1493
 
1361
1494
  if (errorData?.error === 'test_cases_limit_reached') {
1362
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
1495
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1363
1496
  } else if (errorData?.error === 'test_runs_limit_reached') {
1364
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
1497
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1365
1498
  } else {
1366
- console.error(' TESTLENS ERROR: Your trial plan has ended');
1499
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1367
1500
  }
1368
1501
 
1369
- console.error('='.repeat(80));
1370
- console.error('');
1371
- console.error(errorData?.message || 'Your trial period has expired.');
1372
- console.error('');
1373
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
1374
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1375
- console.error('');
1502
+ logger.error('='.repeat(80));
1503
+ logger.error('');
1504
+ logger.error(errorData?.message || 'Your trial period has expired.');
1505
+ logger.error('');
1506
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1507
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1508
+ logger.error('');
1376
1509
  if (errorData?.trial_end_date) {
1377
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1378
- console.error('');
1510
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1511
+ logger.error('');
1379
1512
  }
1380
- console.error('='.repeat(80));
1381
- console.error('');
1513
+ logger.error('='.repeat(80));
1514
+ logger.error('');
1382
1515
  return null;
1383
1516
  }
1384
1517
  }
@@ -1396,9 +1529,9 @@ export class TestLensReporter implements Reporter {
1396
1529
  errorMsg = `Access denied (403) - presigned URL may have expired`;
1397
1530
  }
1398
1531
 
1399
- console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
1532
+ logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1400
1533
  if (error.response?.data) {
1401
- console.error('Error details:', error.response.data);
1534
+ logger.error({ errorDetails: error.response.data }, 'Error details');
1402
1535
  }
1403
1536
 
1404
1537
  // Don't throw, just return null to continue with other artifacts
@@ -1417,7 +1550,7 @@ export class TestLensReporter implements Reporter {
1417
1550
  return mimeType || 'application/octet-stream';
1418
1551
  }
1419
1552
  } catch (error: any) {
1420
- console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
1553
+ logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
1421
1554
  }
1422
1555
  // Fallback to basic content type mapping
1423
1556
  const contentTypes: Record<string, string> = {
@@ -1445,11 +1578,10 @@ export class TestLensReporter implements Reporter {
1445
1578
  const stats = fs.statSync(filePath);
1446
1579
  return stats.size;
1447
1580
  } catch (error) {
1448
- console.warn(`Could not get file size for ${filePath}:`, (error as Error).message);
1581
+ logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
1449
1582
  return 0;
1450
1583
  }
1451
1584
  }
1452
1585
  }
1453
1586
 
1454
1587
  export default TestLensReporter;
1455
-