@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.js CHANGED
@@ -9,6 +9,66 @@ const fs = tslib_1.__importStar(require("fs"));
9
9
  const https = tslib_1.__importStar(require("https"));
10
10
  const axios_1 = tslib_1.__importDefault(require("axios"));
11
11
  const child_process_1 = require("child_process");
12
+ const pino_1 = tslib_1.__importDefault(require("pino"));
13
+ const isVerboseLogs = process.env.TESTLENS_LOG_VERBOSE === '1' ||
14
+ process.env.TESTLENS_LOG_VERBOSE === 'true';
15
+ const noisyInfoPrefixes = [
16
+ '[OK]',
17
+ '[UPLOAD]',
18
+ '[ARTIFACT]',
19
+ '[SKIP]',
20
+ '[TestLens] onTestEnd called',
21
+ '[TestLens] Sending testEnd'
22
+ ];
23
+ const noisyInfoSubstrings = [
24
+ 'Sent runStart event to TestLens',
25
+ 'Sent specStart event to TestLens',
26
+ 'Sent testStart event to TestLens',
27
+ 'Sent testEnd event to TestLens',
28
+ 'Sent runEnd event to TestLens',
29
+ 'Sent artifact event to TestLens',
30
+ 'S3 upload completed',
31
+ 'Upload confirmed',
32
+ 'Uploading ',
33
+ 'Processed artifact',
34
+ 'Spec code blocks',
35
+ '\n[METADATA]',
36
+ 'Custom Metadata Detected',
37
+ 'Using API key from environment variable'
38
+ ];
39
+ const shouldSuppressInfo = (msg) => {
40
+ if (isVerboseLogs)
41
+ return false;
42
+ if (!msg)
43
+ return false;
44
+ return (noisyInfoPrefixes.some(prefix => msg.startsWith(prefix)) ||
45
+ noisyInfoSubstrings.some(sub => msg.includes(sub)));
46
+ };
47
+ // Create pino logger instance
48
+ const logger = (0, pino_1.default)({
49
+ level: process.env.TESTLENS_LOG_LEVEL || process.env.LOG_LEVEL || 'info',
50
+ base: null, // remove pid/hostname
51
+ timestamp: false, // remove time
52
+ formatters: {
53
+ level: () => ({}) // remove level
54
+ },
55
+ hooks: {
56
+ logMethod(args, method, level) {
57
+ // pino level is numeric (info = 30)
58
+ if (level === 30) {
59
+ const msg = typeof args[0] === 'string'
60
+ ? args[0]
61
+ : typeof args[1] === 'string'
62
+ ? args[1]
63
+ : '';
64
+ if (shouldSuppressInfo(msg)) {
65
+ return;
66
+ }
67
+ }
68
+ return method.apply(this, args);
69
+ }
70
+ }
71
+ });
12
72
  // Lazy-load mime module to support ESM
13
73
  let mimeModule = null;
14
74
  async function getMime() {
@@ -45,11 +105,11 @@ class TestLensReporter {
45
105
  // For testlensBuildTag, support comma-separated values
46
106
  if (key === 'testlensBuildTag' && value.includes(',')) {
47
107
  customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
48
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
108
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
49
109
  }
50
110
  else {
51
111
  customArgs[key] = value;
52
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
112
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
53
113
  }
54
114
  break; // Use first match
55
115
  }
@@ -61,6 +121,12 @@ class TestLensReporter {
61
121
  this.runCreationFailed = false; // Track if run creation failed due to limits
62
122
  this.cliArgs = {}; // Store CLI args separately
63
123
  this.pendingUploads = new Set(); // Track pending artifact uploads
124
+ this.artifactStats = {
125
+ uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
126
+ skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
127
+ failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
128
+ };
129
+ this.artifactsSeen = 0;
64
130
  // Parse custom CLI arguments
65
131
  const customArgs = TestLensReporter.parseCustomArgs();
66
132
  this.cliArgs = customArgs; // Store CLI args separately for later use
@@ -88,32 +154,43 @@ class TestLensReporter {
88
154
  flushInterval: options.flushInterval || 5000,
89
155
  retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
90
156
  timeout: options.timeout || 60000,
157
+ rejectUnauthorized: options.rejectUnauthorized,
158
+ ignoreSslErrors: options.ignoreSslErrors,
91
159
  customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
92
160
  };
93
161
  if (!this.config.apiKey) {
94
162
  throw new Error('API_KEY is required for TestLensReporter. Pass it as apiKey option in your playwright config or set one of these environment variables: TESTLENS_API_KEY, TESTLENS_KEY, PLAYWRIGHT_API_KEY, PW_API_KEY, API_KEY, or APIKEY.');
95
163
  }
96
164
  if (apiKey !== options.apiKey) {
97
- console.log('✓ Using API key from environment variable');
165
+ logger.info('✓ Using API key from environment variable');
166
+ }
167
+ // Default environment to allow self-signed certs unless explicitly set
168
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
169
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
98
170
  }
99
171
  // Determine SSL validation behavior
100
- let rejectUnauthorized = true; // Default to secure
101
- // Check various ways SSL validation can be disabled (in order of precedence)
102
- if (this.config.ignoreSslErrors) {
103
- // Explicit configuration option
172
+ let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
173
+ // Check various ways SSL validation can be disabled or enforced (in order of precedence)
174
+ if (this.config.ignoreSslErrors === true) {
104
175
  rejectUnauthorized = false;
105
- console.log('⚠️ SSL certificate validation disabled via ignoreSslErrors option');
176
+ logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
106
177
  }
107
178
  else if (this.config.rejectUnauthorized === false) {
108
- // Explicit configuration option
109
179
  rejectUnauthorized = false;
110
- console.log('⚠️ SSL certificate validation disabled via rejectUnauthorized option');
180
+ logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
181
+ }
182
+ else if (this.config.rejectUnauthorized === true) {
183
+ rejectUnauthorized = true;
111
184
  }
112
185
  else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
113
- // Environment variable override
114
186
  rejectUnauthorized = false;
115
- console.log('⚠️ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
187
+ logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
188
+ }
189
+ else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
190
+ rejectUnauthorized = true;
116
191
  }
192
+ // Mirror the resolved value so all HTTPS requests in this process follow it
193
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
117
194
  // Set up axios instance with retry logic and enhanced SSL handling
118
195
  this.axiosInstance = axios_1.default.create({
119
196
  baseURL: this.config.apiEndpoint,
@@ -152,11 +229,11 @@ class TestLensReporter {
152
229
  this.runCreationFailed = false;
153
230
  // Log custom metadata if any
154
231
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
155
- console.log('\n📋 Custom Metadata Detected:');
232
+ logger.info('\n[METADATA] Custom Metadata Detected:');
156
233
  Object.entries(this.config.customMetadata).forEach(([key, value]) => {
157
- console.log(` ${key}: ${value}`);
234
+ logger.info(` ${key}: ${value}`);
158
235
  });
159
- console.log('');
236
+ logger.info('');
160
237
  }
161
238
  }
162
239
  initializeRunMetadata() {
@@ -219,26 +296,27 @@ class TestLensReporter {
219
296
  return status;
220
297
  }
221
298
  async onBegin(config, suite) {
299
+ logger.info(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
222
300
  // Show Build Name if provided, otherwise show Run ID
223
301
  if (this.runMetadata.testlensBuildName) {
224
- console.log(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
225
- console.log(` Run ID: ${this.runId}`);
302
+ logger.info(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
303
+ logger.info(` Run ID: ${this.runId}`);
226
304
  }
227
305
  else {
228
- console.log(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
306
+ logger.info(`TestLens Reporter starting - Run ID: ${this.runId}`);
229
307
  }
230
308
  // Collect Git information if enabled
231
309
  if (this.config.enableGitInfo) {
232
310
  this.runMetadata.gitInfo = await this.collectGitInfo();
233
311
  if (this.runMetadata.gitInfo) {
234
- console.log(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
312
+ logger.info(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
235
313
  }
236
314
  else {
237
- console.log(`⚠️ Git info collection returned null - not in a git repository or git not available`);
315
+ logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
238
316
  }
239
317
  }
240
318
  else {
241
- console.log(`ℹ️ Git info collection disabled (enableGitInfo: false)`);
319
+ logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
242
320
  }
243
321
  // Add shard information if available
244
322
  if (config.shard) {
@@ -257,7 +335,7 @@ class TestLensReporter {
257
335
  }
258
336
  async onTestBegin(test, result) {
259
337
  // Log which test is starting
260
- console.log(`\n▶️ Running test: ${test.title}`);
338
+ logger.info(`[TEST] Running test: ${test.title}`);
261
339
  const specPath = test.location.file;
262
340
  const specKey = `${specPath}-${test.parent.title}`;
263
341
  // Create or update spec data
@@ -330,10 +408,23 @@ class TestLensReporter {
330
408
  async onTestEnd(test, result) {
331
409
  const testId = this.getTestId(test);
332
410
  let testData = this.testMap.get(testId);
333
- console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
411
+ logger.info(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
412
+ if (result.attachments && result.attachments.length > 0) {
413
+ for (const attachment of result.attachments) {
414
+ const artifactType = this.getArtifactType(attachment.name);
415
+ this.artifactsSeen += 1;
416
+ if (attachment.path) {
417
+ this.bumpArtifactStat('uploaded', artifactType);
418
+ }
419
+ else {
420
+ this.bumpArtifactStat('skipped', artifactType);
421
+ }
422
+ }
423
+ }
424
+ logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
334
425
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
335
426
  if (!testData) {
336
- console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
427
+ logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
337
428
  // Create spec data if not exists (skipped tests might not have spec data either)
338
429
  const specPath = test.location.file;
339
430
  const specKey = `${specPath}-${test.parent.title}`;
@@ -478,7 +569,7 @@ class TestLensReporter {
478
569
  });
479
570
  // Send testEnd event for all tests, regardless of status
480
571
  // This ensures tests that are interrupted or have unexpected statuses are properly recorded
481
- console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
572
+ logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
482
573
  // Send test end event to API and get response
483
574
  const testEndResponse = await this.sendToApi({
484
575
  type: 'testEnd',
@@ -491,6 +582,13 @@ class TestLensReporter {
491
582
  // Pass test case DB ID if available for faster lookups
492
583
  await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
493
584
  }
585
+ else if (result.attachments && result.attachments.length > 0) {
586
+ for (const attachment of result.attachments) {
587
+ const artifactType = this.getArtifactType(attachment.name);
588
+ this.artifactsSeen += 1;
589
+ this.bumpArtifactStat('skipped', artifactType);
590
+ }
591
+ }
494
592
  }
495
593
  // Update spec status
496
594
  const specPath = test.location.file;
@@ -561,15 +659,29 @@ class TestLensReporter {
561
659
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
562
660
  // Wait for all pending artifact uploads to complete before sending runEnd
563
661
  if (this.pendingUploads.size > 0) {
564
- console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
662
+ logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
565
663
  try {
566
664
  await Promise.all(Array.from(this.pendingUploads));
567
- console.log(`✅ All artifact uploads completed`);
665
+ logger.debug(`[OK] All artifact uploads completed`);
568
666
  }
569
667
  catch (error) {
570
- console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
668
+ logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
571
669
  }
572
670
  }
671
+ const uploaded = this.artifactStats.uploaded;
672
+ const skipped = this.artifactStats.skipped;
673
+ const failed = this.artifactStats.failed;
674
+ // Always show summary so users can confirm artifact settings
675
+ const summary = `[ARTIFACTS] seen=${this.artifactsSeen} | ` +
676
+ `uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
677
+ `skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
678
+ `failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
679
+ if (logger.levelVal > 30) {
680
+ console.log(summary);
681
+ }
682
+ else {
683
+ logger.info(summary);
684
+ }
573
685
  // Calculate final stats
574
686
  const totalTests = Array.from(this.testMap.values()).length;
575
687
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -598,13 +710,13 @@ class TestLensReporter {
598
710
  });
599
711
  // Show Build Name if provided, otherwise show Run ID
600
712
  if (this.runMetadata.testlensBuildName) {
601
- console.log(`📊 TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
602
- console.log(` Run ID: ${this.runId}`);
713
+ logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
714
+ logger.info(` Run ID: ${this.runId}`);
603
715
  }
604
716
  else {
605
- console.log(`📊 TestLens Report completed - Run ID: ${this.runId}`);
717
+ logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
606
718
  }
607
- console.log(`🎯 Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
719
+ logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
608
720
  }
609
721
  async sendToApi(payload) {
610
722
  // Skip sending if run creation already failed
@@ -618,7 +730,7 @@ class TestLensReporter {
618
730
  }
619
731
  });
620
732
  if (this.config.enableRealTimeStream) {
621
- console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
733
+ logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
622
734
  }
623
735
  // Return response data for caller to use
624
736
  return response.data;
@@ -632,65 +744,66 @@ class TestLensReporter {
632
744
  if (payload.type === 'runStart' && errorData?.limit_type === 'test_runs') {
633
745
  this.runCreationFailed = true;
634
746
  }
635
- console.error('\n' + '='.repeat(80));
747
+ logger.error('\n' + '='.repeat(80));
636
748
  if (errorData?.limit_type === 'test_cases') {
637
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
749
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
638
750
  }
639
751
  else if (errorData?.limit_type === 'test_runs') {
640
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
752
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
641
753
  }
642
754
  else {
643
- console.error(' TESTLENS ERROR: Plan Limit Reached');
755
+ logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
644
756
  }
645
- console.error('='.repeat(80));
646
- console.error('');
647
- console.error(errorData?.message || 'You have reached your plan limit.');
648
- console.error('');
649
- console.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
650
- console.error('');
651
- console.error('To continue, please upgrade your plan.');
652
- console.error('Contact: support@alternative-path.com');
653
- console.error('');
654
- console.error('='.repeat(80));
655
- console.error('');
757
+ logger.error('='.repeat(80));
758
+ logger.error('');
759
+ logger.error(errorData?.message || 'You have reached your plan limit.');
760
+ logger.error('');
761
+ logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
762
+ logger.error('');
763
+ logger.error('To continue, please upgrade your plan.');
764
+ logger.error('Contact: support@alternative-path.com');
765
+ logger.error('');
766
+ logger.error('='.repeat(80));
767
+ logger.error('');
656
768
  return; // Don't log the full error object for limit errors
657
769
  }
658
770
  // Check for trial expiration, subscription errors, or limit errors (401)
659
771
  if (status === 401) {
660
772
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
661
773
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
662
- console.error('\n' + '='.repeat(80));
774
+ logger.error('\n' + '='.repeat(80));
663
775
  if (errorData?.error === 'test_cases_limit_reached') {
664
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
776
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
665
777
  }
666
778
  else if (errorData?.error === 'test_runs_limit_reached') {
667
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
779
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
668
780
  }
669
781
  else {
670
- console.error(' TESTLENS ERROR: Your trial plan has ended');
782
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
671
783
  }
672
- console.error('='.repeat(80));
673
- console.error('');
674
- console.error(errorData?.message || 'Your trial period has expired.');
675
- console.error('');
676
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
677
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
678
- console.error('');
784
+ logger.error('='.repeat(80));
785
+ logger.error('');
786
+ logger.error(errorData?.message || 'Your trial period has expired.');
787
+ logger.error('');
788
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
789
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
790
+ logger.error('');
679
791
  if (errorData?.trial_end_date) {
680
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
681
- console.error('');
792
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
793
+ logger.error('');
682
794
  }
683
- console.error('='.repeat(80));
684
- console.error('');
795
+ logger.error('='.repeat(80));
796
+ logger.error('');
685
797
  }
686
798
  else {
687
- console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
799
+ logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
688
800
  }
689
801
  }
690
802
  else if (status !== 403) {
691
803
  // Log other errors (but not 403 which we handled above)
692
- console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
693
- message: error?.message || 'Unknown error',
804
+ logger.error({
805
+ message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
806
+ error: error?.message || 'Unknown error',
694
807
  status: status,
695
808
  statusText: error?.response?.statusText,
696
809
  data: errorData,
@@ -709,19 +822,21 @@ class TestLensReporter {
709
822
  }
710
823
  const attachments = result.attachments;
711
824
  for (const attachment of attachments) {
825
+ const artifactType = this.getArtifactType(attachment.name);
712
826
  if (attachment.path) {
713
827
  // Check if attachment should be processed based on config
714
- const artifactType = this.getArtifactType(attachment.name);
715
828
  const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
716
829
  const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
717
830
  // Skip video if disabled in config
718
831
  if (isVideo && !this.config.enableVideo) {
719
- console.log(`⏭️ Skipping video artifact ${attachment.name} - video capture disabled in config`);
832
+ logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
833
+ this.bumpArtifactStat('skipped', artifactType);
720
834
  continue;
721
835
  }
722
836
  // Skip screenshot if disabled in config
723
837
  if (isScreenshot && !this.config.enableScreenshot) {
724
- console.log(`⏭️ Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
838
+ logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
839
+ this.bumpArtifactStat('skipped', artifactType);
725
840
  continue;
726
841
  }
727
842
  try {
@@ -759,13 +874,15 @@ class TestLensReporter {
759
874
  const uploadPromise = Promise.resolve().then(async () => {
760
875
  try {
761
876
  if (!attachment.path) {
762
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
877
+ logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
878
+ this.bumpArtifactStat('skipped', artifactType);
763
879
  return;
764
880
  }
765
881
  const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
766
882
  // Skip if upload failed or file was too large
767
883
  if (!s3Data) {
768
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
884
+ logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
885
+ this.bumpArtifactStat('failed', artifactType);
769
886
  return;
770
887
  }
771
888
  const artifactData = {
@@ -786,10 +903,11 @@ class TestLensReporter {
786
903
  timestamp: new Date().toISOString(),
787
904
  artifact: artifactData
788
905
  });
789
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
906
+ logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
790
907
  }
791
908
  catch (error) {
792
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, error.message);
909
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
910
+ this.bumpArtifactStat('failed', artifactType);
793
911
  }
794
912
  });
795
913
  // Track this upload and ensure cleanup on completion
@@ -801,9 +919,13 @@ class TestLensReporter {
801
919
  // They will be awaited in onEnd
802
920
  }
803
921
  catch (error) {
804
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, error.message);
922
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
923
+ this.bumpArtifactStat('failed', artifactType);
805
924
  }
806
925
  }
926
+ else {
927
+ this.bumpArtifactStat('skipped', artifactType);
928
+ }
807
929
  }
808
930
  }
809
931
  async sendSpecCodeBlocks(specPath) {
@@ -831,16 +953,16 @@ class TestLensReporter {
831
953
  codeBlocks,
832
954
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
833
955
  });
834
- console.log(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
956
+ logger.info(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
835
957
  }
836
958
  catch (error) {
837
959
  const errorData = error?.response?.data;
838
960
  // Handle duplicate spec code blocks gracefully (when re-running tests)
839
961
  if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
840
- console.log(`ℹ️ Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
962
+ logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
841
963
  return;
842
964
  }
843
- console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
965
+ logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
844
966
  }
845
967
  }
846
968
  extractTestBlocks(filePath) {
@@ -899,7 +1021,7 @@ class TestLensReporter {
899
1021
  return blocks;
900
1022
  }
901
1023
  catch (error) {
902
- console.error(`Failed to extract test blocks from ${filePath}:`, error.message);
1024
+ logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
903
1025
  return [];
904
1026
  }
905
1027
  }
@@ -922,7 +1044,7 @@ class TestLensReporter {
922
1044
  }
923
1045
  catch (e) {
924
1046
  // Remote info is optional - handle gracefully
925
- console.log('ℹ️ No git remote configured, skipping remote info');
1047
+ logger.info('[INFO] No git remote configured, skipping remote info');
926
1048
  }
927
1049
  const isDirty = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
928
1050
  return {
@@ -951,6 +1073,12 @@ class TestLensReporter {
951
1073
  return 'trace';
952
1074
  return 'attachment';
953
1075
  }
1076
+ bumpArtifactStat(stat, type) {
1077
+ const bucket = this.artifactStats[stat];
1078
+ if (bucket[type] !== undefined) {
1079
+ bucket[type] += 1;
1080
+ }
1081
+ }
954
1082
  extractTags(test) {
955
1083
  const tags = [];
956
1084
  // Playwright stores tags in the _tags property
@@ -994,7 +1122,7 @@ class TestLensReporter {
994
1122
  // Check file size first
995
1123
  const fileSize = this.getFileSize(filePath);
996
1124
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
997
- console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1125
+ logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
998
1126
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
999
1127
  // Step 1: Request pre-signed URL from server
1000
1128
  const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
@@ -1019,7 +1147,7 @@ class TestLensReporter {
1019
1147
  }
1020
1148
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1021
1149
  // Step 2: Upload directly to S3 using presigned URL
1022
- console.log(`⬆️ [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1150
+ logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1023
1151
  const fileBuffer = fs.readFileSync(filePath);
1024
1152
  // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
1025
1153
  // The backend signs with ServerSideEncryption:'AES256', so we must send that header
@@ -1037,7 +1165,7 @@ class TestLensReporter {
1037
1165
  if (uploadResponse.status !== 200) {
1038
1166
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1039
1167
  }
1040
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1168
+ logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1041
1169
  // Step 3: Confirm upload with server to save metadata
1042
1170
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
1043
1171
  const confirmBody = {
@@ -1059,7 +1187,7 @@ class TestLensReporter {
1059
1187
  });
1060
1188
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1061
1189
  const artifact = confirmResponse.data.artifact;
1062
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1190
+ logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1063
1191
  return {
1064
1192
  key: s3Key,
1065
1193
  url: artifact.s3Url,
@@ -1078,29 +1206,29 @@ class TestLensReporter {
1078
1206
  const errorData = error?.response?.data;
1079
1207
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1080
1208
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1081
- console.error('\n' + '='.repeat(80));
1209
+ logger.error('\n' + '='.repeat(80));
1082
1210
  if (errorData?.error === 'test_cases_limit_reached') {
1083
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
1211
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1084
1212
  }
1085
1213
  else if (errorData?.error === 'test_runs_limit_reached') {
1086
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
1214
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1087
1215
  }
1088
1216
  else {
1089
- console.error(' TESTLENS ERROR: Your trial plan has ended');
1217
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1090
1218
  }
1091
- console.error('='.repeat(80));
1092
- console.error('');
1093
- console.error(errorData?.message || 'Your trial period has expired.');
1094
- console.error('');
1095
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
1096
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1097
- console.error('');
1219
+ logger.error('='.repeat(80));
1220
+ logger.error('');
1221
+ logger.error(errorData?.message || 'Your trial period has expired.');
1222
+ logger.error('');
1223
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1224
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1225
+ logger.error('');
1098
1226
  if (errorData?.trial_end_date) {
1099
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1100
- console.error('');
1227
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1228
+ logger.error('');
1101
1229
  }
1102
- console.error('='.repeat(80));
1103
- console.error('');
1230
+ logger.error('='.repeat(80));
1231
+ logger.error('');
1104
1232
  return null;
1105
1233
  }
1106
1234
  }
@@ -1118,9 +1246,9 @@ class TestLensReporter {
1118
1246
  else if (error.response?.status === 403) {
1119
1247
  errorMsg = `Access denied (403) - presigned URL may have expired`;
1120
1248
  }
1121
- console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
1249
+ logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1122
1250
  if (error.response?.data) {
1123
- console.error('Error details:', error.response.data);
1251
+ logger.error({ errorDetails: error.response.data }, 'Error details');
1124
1252
  }
1125
1253
  // Don't throw, just return null to continue with other artifacts
1126
1254
  return null;
@@ -1138,7 +1266,7 @@ class TestLensReporter {
1138
1266
  }
1139
1267
  }
1140
1268
  catch (error) {
1141
- console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
1269
+ logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
1142
1270
  }
1143
1271
  // Fallback to basic content type mapping
1144
1272
  const contentTypes = {
@@ -1165,7 +1293,7 @@ class TestLensReporter {
1165
1293
  return stats.size;
1166
1294
  }
1167
1295
  catch (error) {
1168
- console.warn(`Could not get file size for ${filePath}:`, error.message);
1296
+ logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
1169
1297
  return 0;
1170
1298
  }
1171
1299
  }