@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.js CHANGED
@@ -9,6 +9,11 @@ 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
+ // Create pino logger instance
14
+ const logger = (0, pino_1.default)({
15
+ level: process.env.LOG_LEVEL || 'info'
16
+ });
12
17
  // Lazy-load mime module to support ESM
13
18
  let mimeModule = null;
14
19
  async function getMime() {
@@ -45,11 +50,11 @@ class TestLensReporter {
45
50
  // For testlensBuildTag, support comma-separated values
46
51
  if (key === 'testlensBuildTag' && value.includes(',')) {
47
52
  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)`);
53
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
49
54
  }
50
55
  else {
51
56
  customArgs[key] = value;
52
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
57
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
53
58
  }
54
59
  break; // Use first match
55
60
  }
@@ -60,6 +65,7 @@ class TestLensReporter {
60
65
  constructor(options) {
61
66
  this.runCreationFailed = false; // Track if run creation failed due to limits
62
67
  this.cliArgs = {}; // Store CLI args separately
68
+ this.pendingUploads = new Set(); // Track pending artifact uploads
63
69
  // Parse custom CLI arguments
64
70
  const customArgs = TestLensReporter.parseCustomArgs();
65
71
  this.cliArgs = customArgs; // Store CLI args separately for later use
@@ -87,32 +93,43 @@ class TestLensReporter {
87
93
  flushInterval: options.flushInterval || 5000,
88
94
  retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
89
95
  timeout: options.timeout || 60000,
96
+ rejectUnauthorized: options.rejectUnauthorized,
97
+ ignoreSslErrors: options.ignoreSslErrors,
90
98
  customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
91
99
  };
92
100
  if (!this.config.apiKey) {
93
101
  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.');
94
102
  }
95
103
  if (apiKey !== options.apiKey) {
96
- console.log('✓ Using API key from environment variable');
104
+ logger.info('✓ Using API key from environment variable');
105
+ }
106
+ // Default environment to allow self-signed certs unless explicitly set
107
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
108
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
97
109
  }
98
110
  // Determine SSL validation behavior
99
- let rejectUnauthorized = true; // Default to secure
100
- // Check various ways SSL validation can be disabled (in order of precedence)
101
- if (this.config.ignoreSslErrors) {
102
- // Explicit configuration option
111
+ let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
112
+ // Check various ways SSL validation can be disabled or enforced (in order of precedence)
113
+ if (this.config.ignoreSslErrors === true) {
103
114
  rejectUnauthorized = false;
104
- console.log('⚠️ SSL certificate validation disabled via ignoreSslErrors option');
115
+ logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
105
116
  }
106
117
  else if (this.config.rejectUnauthorized === false) {
107
- // Explicit configuration option
108
118
  rejectUnauthorized = false;
109
- console.log('⚠️ SSL certificate validation disabled via rejectUnauthorized option');
119
+ logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
120
+ }
121
+ else if (this.config.rejectUnauthorized === true) {
122
+ rejectUnauthorized = true;
110
123
  }
111
124
  else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
112
- // Environment variable override
113
125
  rejectUnauthorized = false;
114
- console.log('⚠️ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
126
+ logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
127
+ }
128
+ else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
129
+ rejectUnauthorized = true;
115
130
  }
131
+ // Mirror the resolved value so all HTTPS requests in this process follow it
132
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
116
133
  // Set up axios instance with retry logic and enhanced SSL handling
117
134
  this.axiosInstance = axios_1.default.create({
118
135
  baseURL: this.config.apiEndpoint,
@@ -151,11 +168,11 @@ class TestLensReporter {
151
168
  this.runCreationFailed = false;
152
169
  // Log custom metadata if any
153
170
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
154
- console.log('\n📋 Custom Metadata Detected:');
171
+ logger.info('\n[METADATA] Custom Metadata Detected:');
155
172
  Object.entries(this.config.customMetadata).forEach(([key, value]) => {
156
- console.log(` ${key}: ${value}`);
173
+ logger.info(` ${key}: ${value}`);
157
174
  });
158
- console.log('');
175
+ logger.info('');
159
176
  }
160
177
  }
161
178
  initializeRunMetadata() {
@@ -166,7 +183,8 @@ class TestLensReporter {
166
183
  browser: 'multiple',
167
184
  os: `${os.type()} ${os.release()}`,
168
185
  playwrightVersion: this.getPlaywrightVersion(),
169
- nodeVersion: process.version
186
+ nodeVersion: process.version,
187
+ testlensVersion: this.getTestLensVersion()
170
188
  };
171
189
  // Add custom metadata if provided
172
190
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
@@ -189,6 +207,15 @@ class TestLensReporter {
189
207
  return 'unknown';
190
208
  }
191
209
  }
210
+ getTestLensVersion() {
211
+ try {
212
+ const testlensPackage = require('./package.json');
213
+ return testlensPackage.version;
214
+ }
215
+ catch (error) {
216
+ return 'unknown';
217
+ }
218
+ }
192
219
  normalizeTestStatus(status) {
193
220
  // Treat timeout as failed for consistency with analytics
194
221
  if (status === 'timedOut') {
@@ -210,24 +237,24 @@ class TestLensReporter {
210
237
  async onBegin(config, suite) {
211
238
  // Show Build Name if provided, otherwise show Run ID
212
239
  if (this.runMetadata.testlensBuildName) {
213
- console.log(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
214
- console.log(` Run ID: ${this.runId}`);
240
+ logger.info(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
241
+ logger.info(` Run ID: ${this.runId}`);
215
242
  }
216
243
  else {
217
- console.log(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
244
+ logger.info(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
218
245
  }
219
246
  // Collect Git information if enabled
220
247
  if (this.config.enableGitInfo) {
221
248
  this.runMetadata.gitInfo = await this.collectGitInfo();
222
249
  if (this.runMetadata.gitInfo) {
223
- console.log(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
250
+ logger.info(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
224
251
  }
225
252
  else {
226
- console.log(`⚠️ Git info collection returned null - not in a git repository or git not available`);
253
+ logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
227
254
  }
228
255
  }
229
256
  else {
230
- console.log(`ℹ️ Git info collection disabled (enableGitInfo: false)`);
257
+ logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
231
258
  }
232
259
  // Add shard information if available
233
260
  if (config.shard) {
@@ -246,7 +273,7 @@ class TestLensReporter {
246
273
  }
247
274
  async onTestBegin(test, result) {
248
275
  // Log which test is starting
249
- console.log(`\n▶️ Running test: ${test.title}`);
276
+ logger.info(`\n[TEST] Running test: ${test.title}`);
250
277
  const specPath = test.location.file;
251
278
  const specKey = `${specPath}-${test.parent.title}`;
252
279
  // Create or update spec data
@@ -319,10 +346,10 @@ class TestLensReporter {
319
346
  async onTestEnd(test, result) {
320
347
  const testId = this.getTestId(test);
321
348
  let testData = this.testMap.get(testId);
322
- console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
349
+ logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
323
350
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
324
351
  if (!testData) {
325
- console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
352
+ logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
326
353
  // Create spec data if not exists (skipped tests might not have spec data either)
327
354
  const specPath = test.location.file;
328
355
  const specKey = `${specPath}-${test.parent.title}`;
@@ -465,23 +492,20 @@ class TestLensReporter {
465
492
  }
466
493
  return testError;
467
494
  });
468
- // Only send testEnd event after final retry attempt
469
- // If test passed or this is the last retry, send the event
470
- const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
471
- if (isFinalAttempt) {
472
- console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
473
- // Send test end event to API and get response
474
- const testEndResponse = await this.sendToApi({
475
- type: 'testEnd',
476
- runId: this.runId,
477
- timestamp: new Date().toISOString(),
478
- test: testData
479
- });
480
- // Handle artifacts (test case is now guaranteed to be in database)
481
- if (this.config.enableArtifacts) {
482
- // Pass test case DB ID if available for faster lookups
483
- await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
484
- }
495
+ // Send testEnd event for all tests, regardless of status
496
+ // This ensures tests that are interrupted or have unexpected statuses are properly recorded
497
+ logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
498
+ // Send test end event to API and get response
499
+ const testEndResponse = await this.sendToApi({
500
+ type: 'testEnd',
501
+ runId: this.runId,
502
+ timestamp: new Date().toISOString(),
503
+ test: testData
504
+ });
505
+ // Handle artifacts (test case is now guaranteed to be in database)
506
+ if (this.config.enableArtifacts) {
507
+ // Pass test case DB ID if available for faster lookups
508
+ await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
485
509
  }
486
510
  }
487
511
  // Update spec status
@@ -497,12 +521,33 @@ class TestLensReporter {
497
521
  specData.status = 'skipped';
498
522
  }
499
523
  // Check if all tests in spec are complete
524
+ // Only consider tests that were actually executed (have testData)
500
525
  const remainingTests = test.parent.tests.filter((t) => {
501
526
  const tId = this.getTestId(t);
502
527
  const tData = this.testMap.get(tId);
503
- return !tData || !tData.endTime;
528
+ // If testData exists but no endTime, it's still running
529
+ return tData && !tData.endTime;
504
530
  });
505
531
  if (remainingTests.length === 0) {
532
+ // Determine final spec status based on all executed tests
533
+ const executedTests = test.parent.tests
534
+ .map((t) => {
535
+ const tId = this.getTestId(t);
536
+ return this.testMap.get(tId);
537
+ })
538
+ .filter((tData) => !!tData);
539
+ if (executedTests.length > 0) {
540
+ const allTestStatuses = executedTests.map(tData => tData.status);
541
+ if (allTestStatuses.every(status => status === 'passed')) {
542
+ specData.status = 'passed';
543
+ }
544
+ else if (allTestStatuses.some(status => status === 'failed')) {
545
+ specData.status = 'failed';
546
+ }
547
+ else if (allTestStatuses.every(status => status === 'skipped')) {
548
+ specData.status = 'skipped';
549
+ }
550
+ }
506
551
  // Aggregate tags from all tests in this spec
507
552
  const allTags = new Set();
508
553
  test.parent.tests.forEach((t) => {
@@ -530,6 +575,17 @@ class TestLensReporter {
530
575
  async onEnd(result) {
531
576
  this.runMetadata.endTime = new Date().toISOString();
532
577
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
578
+ // Wait for all pending artifact uploads to complete before sending runEnd
579
+ if (this.pendingUploads.size > 0) {
580
+ logger.info(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
581
+ try {
582
+ await Promise.all(Array.from(this.pendingUploads));
583
+ logger.info(`[OK] All artifact uploads completed`);
584
+ }
585
+ catch (error) {
586
+ logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
587
+ }
588
+ }
533
589
  // Calculate final stats
534
590
  const totalTests = Array.from(this.testMap.values()).length;
535
591
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -558,13 +614,13 @@ class TestLensReporter {
558
614
  });
559
615
  // Show Build Name if provided, otherwise show Run ID
560
616
  if (this.runMetadata.testlensBuildName) {
561
- console.log(`📊 TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
562
- console.log(` Run ID: ${this.runId}`);
617
+ logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
618
+ logger.info(` Run ID: ${this.runId}`);
563
619
  }
564
620
  else {
565
- console.log(`📊 TestLens Report completed - Run ID: ${this.runId}`);
621
+ logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
566
622
  }
567
- console.log(`🎯 Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
623
+ logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
568
624
  }
569
625
  async sendToApi(payload) {
570
626
  // Skip sending if run creation already failed
@@ -578,7 +634,7 @@ class TestLensReporter {
578
634
  }
579
635
  });
580
636
  if (this.config.enableRealTimeStream) {
581
- console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
637
+ logger.info(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
582
638
  }
583
639
  // Return response data for caller to use
584
640
  return response.data;
@@ -592,65 +648,66 @@ class TestLensReporter {
592
648
  if (payload.type === 'runStart' && errorData?.limit_type === 'test_runs') {
593
649
  this.runCreationFailed = true;
594
650
  }
595
- console.error('\n' + '='.repeat(80));
651
+ logger.error('\n' + '='.repeat(80));
596
652
  if (errorData?.limit_type === 'test_cases') {
597
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
653
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
598
654
  }
599
655
  else if (errorData?.limit_type === 'test_runs') {
600
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
656
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
601
657
  }
602
658
  else {
603
- console.error(' TESTLENS ERROR: Plan Limit Reached');
659
+ logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
604
660
  }
605
- console.error('='.repeat(80));
606
- console.error('');
607
- console.error(errorData?.message || 'You have reached your plan limit.');
608
- console.error('');
609
- console.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
610
- console.error('');
611
- console.error('To continue, please upgrade your plan.');
612
- console.error('Contact: support@alternative-path.com');
613
- console.error('');
614
- console.error('='.repeat(80));
615
- console.error('');
661
+ logger.error('='.repeat(80));
662
+ logger.error('');
663
+ logger.error(errorData?.message || 'You have reached your plan limit.');
664
+ logger.error('');
665
+ logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
666
+ logger.error('');
667
+ logger.error('To continue, please upgrade your plan.');
668
+ logger.error('Contact: support@alternative-path.com');
669
+ logger.error('');
670
+ logger.error('='.repeat(80));
671
+ logger.error('');
616
672
  return; // Don't log the full error object for limit errors
617
673
  }
618
674
  // Check for trial expiration, subscription errors, or limit errors (401)
619
675
  if (status === 401) {
620
676
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
621
677
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
622
- console.error('\n' + '='.repeat(80));
678
+ logger.error('\n' + '='.repeat(80));
623
679
  if (errorData?.error === 'test_cases_limit_reached') {
624
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
680
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
625
681
  }
626
682
  else if (errorData?.error === 'test_runs_limit_reached') {
627
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
683
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
628
684
  }
629
685
  else {
630
- console.error(' TESTLENS ERROR: Your trial plan has ended');
686
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
631
687
  }
632
- console.error('='.repeat(80));
633
- console.error('');
634
- console.error(errorData?.message || 'Your trial period has expired.');
635
- console.error('');
636
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
637
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
638
- console.error('');
688
+ logger.error('='.repeat(80));
689
+ logger.error('');
690
+ logger.error(errorData?.message || 'Your trial period has expired.');
691
+ logger.error('');
692
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
693
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
694
+ logger.error('');
639
695
  if (errorData?.trial_end_date) {
640
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
641
- console.error('');
696
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
697
+ logger.error('');
642
698
  }
643
- console.error('='.repeat(80));
644
- console.error('');
699
+ logger.error('='.repeat(80));
700
+ logger.error('');
645
701
  }
646
702
  else {
647
- console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
703
+ logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
648
704
  }
649
705
  }
650
706
  else if (status !== 403) {
651
707
  // Log other errors (but not 403 which we handled above)
652
- console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
653
- message: error?.message || 'Unknown error',
708
+ logger.error({
709
+ message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
710
+ error: error?.message || 'Unknown error',
654
711
  status: status,
655
712
  statusText: error?.response?.statusText,
656
713
  data: errorData,
@@ -676,12 +733,12 @@ class TestLensReporter {
676
733
  const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
677
734
  // Skip video if disabled in config
678
735
  if (isVideo && !this.config.enableVideo) {
679
- console.log(`⏭️ Skipping video artifact ${attachment.name} - video capture disabled in config`);
736
+ logger.info(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
680
737
  continue;
681
738
  }
682
739
  // Skip screenshot if disabled in config
683
740
  if (isScreenshot && !this.config.enableScreenshot) {
684
- console.log(`⏭️ Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
741
+ logger.info(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
685
742
  continue;
686
743
  }
687
744
  try {
@@ -715,34 +772,53 @@ class TestLensReporter {
715
772
  }
716
773
  }
717
774
  // Upload to S3 first (pass DB ID if available for faster lookup)
718
- const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
719
- // Skip if upload failed or file was too large
720
- if (!s3Data) {
721
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
722
- continue;
723
- }
724
- const artifactData = {
725
- testId,
726
- type: this.getArtifactType(attachment.name),
727
- path: attachment.path,
728
- name: fileName,
729
- contentType: attachment.contentType,
730
- fileSize: this.getFileSize(attachment.path),
731
- storageType: 's3',
732
- s3Key: s3Data.key,
733
- s3Url: s3Data.url
734
- };
735
- // Send artifact data to API
736
- await this.sendToApi({
737
- type: 'artifact',
738
- runId: this.runId,
739
- timestamp: new Date().toISOString(),
740
- artifact: artifactData
775
+ // Create upload promise that we can track
776
+ const uploadPromise = Promise.resolve().then(async () => {
777
+ try {
778
+ if (!attachment.path) {
779
+ logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
780
+ return;
781
+ }
782
+ const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
783
+ // Skip if upload failed or file was too large
784
+ if (!s3Data) {
785
+ logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
786
+ return;
787
+ }
788
+ const artifactData = {
789
+ testId,
790
+ type: this.getArtifactType(attachment.name),
791
+ path: attachment.path,
792
+ name: fileName,
793
+ contentType: attachment.contentType,
794
+ fileSize: this.getFileSize(attachment.path),
795
+ storageType: 's3',
796
+ s3Key: s3Data.key,
797
+ s3Url: s3Data.url
798
+ };
799
+ // Send artifact data to API
800
+ await this.sendToApi({
801
+ type: 'artifact',
802
+ runId: this.runId,
803
+ timestamp: new Date().toISOString(),
804
+ artifact: artifactData
805
+ });
806
+ logger.info(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
807
+ }
808
+ catch (error) {
809
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
810
+ }
741
811
  });
742
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
812
+ // Track this upload and ensure cleanup on completion
813
+ this.pendingUploads.add(uploadPromise);
814
+ uploadPromise.finally(() => {
815
+ this.pendingUploads.delete(uploadPromise);
816
+ });
817
+ // Don't await here - let uploads happen in parallel
818
+ // They will be awaited in onEnd
743
819
  }
744
820
  catch (error) {
745
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, error.message);
821
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
746
822
  }
747
823
  }
748
824
  }
@@ -760,23 +836,28 @@ class TestLensReporter {
760
836
  describe: block.describe // parent describe block name
761
837
  }));
762
838
  // Send to dedicated spec code blocks API endpoint
763
- const baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
764
- const specEndpoint = `${baseUrl}/webhook/playwright/spec-code-blocks`;
839
+ // Extract base URL - handle both full and partial endpoint patterns
840
+ let baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
841
+ if (baseUrl === this.config.apiEndpoint) {
842
+ // Fallback: try alternative pattern if main pattern didn't match
843
+ baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
844
+ }
845
+ const specEndpoint = `${baseUrl}/api/v1/webhook/playwright/spec-code-blocks`;
765
846
  await this.axiosInstance.post(specEndpoint, {
766
847
  filePath: path.relative(process.cwd(), specPath),
767
848
  codeBlocks,
768
849
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
769
850
  });
770
- console.log(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
851
+ logger.info(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
771
852
  }
772
853
  catch (error) {
773
854
  const errorData = error?.response?.data;
774
855
  // Handle duplicate spec code blocks gracefully (when re-running tests)
775
856
  if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
776
- console.log(`ℹ️ Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
857
+ logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
777
858
  return;
778
859
  }
779
- console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
860
+ logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
780
861
  }
781
862
  }
782
863
  extractTestBlocks(filePath) {
@@ -835,7 +916,7 @@ class TestLensReporter {
835
916
  return blocks;
836
917
  }
837
918
  catch (error) {
838
- console.error(`Failed to extract test blocks from ${filePath}:`, error.message);
919
+ logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
839
920
  return [];
840
921
  }
841
922
  }
@@ -858,7 +939,7 @@ class TestLensReporter {
858
939
  }
859
940
  catch (e) {
860
941
  // Remote info is optional - handle gracefully
861
- console.log('ℹ️ No git remote configured, skipping remote info');
942
+ logger.info('[INFO] No git remote configured, skipping remote info');
862
943
  }
863
944
  const isDirty = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
864
945
  return {
@@ -930,7 +1011,7 @@ class TestLensReporter {
930
1011
  // Check file size first
931
1012
  const fileSize = this.getFileSize(filePath);
932
1013
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
933
- console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1014
+ logger.info(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
934
1015
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
935
1016
  // Step 1: Request pre-signed URL from server
936
1017
  const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
@@ -955,7 +1036,7 @@ class TestLensReporter {
955
1036
  }
956
1037
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
957
1038
  // Step 2: Upload directly to S3 using presigned URL
958
- console.log(`⬆️ [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1039
+ logger.info(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
959
1040
  const fileBuffer = fs.readFileSync(filePath);
960
1041
  // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
961
1042
  // The backend signs with ServerSideEncryption:'AES256', so we must send that header
@@ -973,7 +1054,7 @@ class TestLensReporter {
973
1054
  if (uploadResponse.status !== 200) {
974
1055
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
975
1056
  }
976
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1057
+ logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
977
1058
  // Step 3: Confirm upload with server to save metadata
978
1059
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
979
1060
  const confirmBody = {
@@ -995,7 +1076,7 @@ class TestLensReporter {
995
1076
  });
996
1077
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
997
1078
  const artifact = confirmResponse.data.artifact;
998
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1079
+ logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
999
1080
  return {
1000
1081
  key: s3Key,
1001
1082
  url: artifact.s3Url,
@@ -1014,29 +1095,29 @@ class TestLensReporter {
1014
1095
  const errorData = error?.response?.data;
1015
1096
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1016
1097
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1017
- console.error('\\n' + '='.repeat(80));
1098
+ logger.error('\n' + '='.repeat(80));
1018
1099
  if (errorData?.error === 'test_cases_limit_reached') {
1019
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
1100
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1020
1101
  }
1021
1102
  else if (errorData?.error === 'test_runs_limit_reached') {
1022
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
1103
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1023
1104
  }
1024
1105
  else {
1025
- console.error(' TESTLENS ERROR: Your trial plan has ended');
1106
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1026
1107
  }
1027
- console.error('='.repeat(80));
1028
- console.error('');
1029
- console.error(errorData?.message || 'Your trial period has expired.');
1030
- console.error('');
1031
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
1032
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1033
- console.error('');
1108
+ logger.error('='.repeat(80));
1109
+ logger.error('');
1110
+ logger.error(errorData?.message || 'Your trial period has expired.');
1111
+ logger.error('');
1112
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1113
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1114
+ logger.error('');
1034
1115
  if (errorData?.trial_end_date) {
1035
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1036
- console.error('');
1116
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1117
+ logger.error('');
1037
1118
  }
1038
- console.error('='.repeat(80));
1039
- console.error('');
1119
+ logger.error('='.repeat(80));
1120
+ logger.error('');
1040
1121
  return null;
1041
1122
  }
1042
1123
  }
@@ -1054,9 +1135,9 @@ class TestLensReporter {
1054
1135
  else if (error.response?.status === 403) {
1055
1136
  errorMsg = `Access denied (403) - presigned URL may have expired`;
1056
1137
  }
1057
- console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
1138
+ logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1058
1139
  if (error.response?.data) {
1059
- console.error('Error details:', error.response.data);
1140
+ logger.error({ errorDetails: error.response.data }, 'Error details');
1060
1141
  }
1061
1142
  // Don't throw, just return null to continue with other artifacts
1062
1143
  return null;
@@ -1074,7 +1155,7 @@ class TestLensReporter {
1074
1155
  }
1075
1156
  }
1076
1157
  catch (error) {
1077
- console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
1158
+ logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
1078
1159
  }
1079
1160
  // Fallback to basic content type mapping
1080
1161
  const contentTypes = {
@@ -1093,31 +1174,15 @@ class TestLensReporter {
1093
1174
  };
1094
1175
  return contentTypes[ext] || 'application/octet-stream';
1095
1176
  }
1096
- generateS3Key(runId, testId, fileName) {
1097
- const date = new Date().toISOString().slice(0, 10);
1098
- const safeTestId = this.sanitizeForS3(testId);
1099
- const safeFileName = this.sanitizeForS3(fileName);
1100
- const ext = path.extname(fileName);
1101
- const baseName = path.basename(fileName, ext);
1102
- return `test-artifacts/${date}/${runId}/${safeTestId}/${safeFileName}${ext}`;
1103
- }
1104
- sanitizeForS3(value) {
1105
- return value
1106
- .replace(/[\/:*?"<>|]/g, '-')
1107
- .replace(/[-\u001f\u007f]/g, '-')
1108
- .replace(/[^-~]/g, '-')
1109
- .replace(/\s+/g, '-')
1110
- .replace(/[_]/g, '-')
1111
- .replace(/-+/g, '-')
1112
- .replace(/^-|-$/g, '');
1113
- }
1177
+ // Note: S3 key generation and sanitization are handled server-side
1178
+ // generateS3Key() and sanitizeForS3() methods removed as they were not used
1114
1179
  getFileSize(filePath) {
1115
1180
  try {
1116
1181
  const stats = fs.statSync(filePath);
1117
1182
  return stats.size;
1118
1183
  }
1119
1184
  catch (error) {
1120
- console.warn(`Could not get file size for ${filePath}:`, error.message);
1185
+ logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
1121
1186
  return 0;
1122
1187
  }
1123
1188
  }