@alternative-path/testlens-playwright-reporter 0.4.5 → 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.d.ts CHANGED
@@ -161,6 +161,8 @@ export declare class TestLensReporter implements Reporter {
161
161
  private runCreationFailed;
162
162
  private cliArgs;
163
163
  private pendingUploads;
164
+ private artifactStats;
165
+ private artifactsSeen;
164
166
  /**
165
167
  * Parse custom metadata from environment variables
166
168
  * Checks for common metadata environment variables
@@ -184,6 +186,7 @@ export declare class TestLensReporter implements Reporter {
184
186
  private extractTestBlocks;
185
187
  private collectGitInfo;
186
188
  private getArtifactType;
189
+ private bumpArtifactStat;
187
190
  private extractTags;
188
191
  private getTestId;
189
192
  private uploadArtifactToS3;
package/index.js CHANGED
@@ -10,9 +10,64 @@ 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
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
+ };
13
47
  // Create pino logger instance
14
48
  const logger = (0, pino_1.default)({
15
- level: process.env.LOG_LEVEL || 'info'
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
+ }
16
71
  });
17
72
  // Lazy-load mime module to support ESM
18
73
  let mimeModule = null;
@@ -66,6 +121,12 @@ class TestLensReporter {
66
121
  this.runCreationFailed = false; // Track if run creation failed due to limits
67
122
  this.cliArgs = {}; // Store CLI args separately
68
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;
69
130
  // Parse custom CLI arguments
70
131
  const customArgs = TestLensReporter.parseCustomArgs();
71
132
  this.cliArgs = customArgs; // Store CLI args separately for later use
@@ -235,19 +296,20 @@ class TestLensReporter {
235
296
  return status;
236
297
  }
237
298
  async onBegin(config, suite) {
299
+ logger.info(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
238
300
  // Show Build Name if provided, otherwise show Run ID
239
301
  if (this.runMetadata.testlensBuildName) {
240
- logger.info(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
302
+ logger.info(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
241
303
  logger.info(` Run ID: ${this.runId}`);
242
304
  }
243
305
  else {
244
- logger.info(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
306
+ logger.info(`TestLens Reporter starting - Run ID: ${this.runId}`);
245
307
  }
246
308
  // Collect Git information if enabled
247
309
  if (this.config.enableGitInfo) {
248
310
  this.runMetadata.gitInfo = await this.collectGitInfo();
249
311
  if (this.runMetadata.gitInfo) {
250
- logger.info(`📦 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}`);
251
313
  }
252
314
  else {
253
315
  logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
@@ -273,7 +335,7 @@ class TestLensReporter {
273
335
  }
274
336
  async onTestBegin(test, result) {
275
337
  // Log which test is starting
276
- logger.info(`\n[TEST] Running test: ${test.title}`);
338
+ logger.info(`[TEST] Running test: ${test.title}`);
277
339
  const specPath = test.location.file;
278
340
  const specKey = `${specPath}-${test.parent.title}`;
279
341
  // Create or update spec data
@@ -346,6 +408,19 @@ class TestLensReporter {
346
408
  async onTestEnd(test, result) {
347
409
  const testId = this.getTestId(test);
348
410
  let testData = this.testMap.get(testId);
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
+ }
349
424
  logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
350
425
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
351
426
  if (!testData) {
@@ -507,6 +582,13 @@ class TestLensReporter {
507
582
  // Pass test case DB ID if available for faster lookups
508
583
  await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
509
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
+ }
510
592
  }
511
593
  // Update spec status
512
594
  const specPath = test.location.file;
@@ -577,15 +659,29 @@ class TestLensReporter {
577
659
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
578
660
  // Wait for all pending artifact uploads to complete before sending runEnd
579
661
  if (this.pendingUploads.size > 0) {
580
- logger.info(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
662
+ logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
581
663
  try {
582
664
  await Promise.all(Array.from(this.pendingUploads));
583
- logger.info(`[OK] All artifact uploads completed`);
665
+ logger.debug(`[OK] All artifact uploads completed`);
584
666
  }
585
667
  catch (error) {
586
668
  logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
587
669
  }
588
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
+ }
589
685
  // Calculate final stats
590
686
  const totalTests = Array.from(this.testMap.values()).length;
591
687
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -634,7 +730,7 @@ class TestLensReporter {
634
730
  }
635
731
  });
636
732
  if (this.config.enableRealTimeStream) {
637
- logger.info(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
733
+ logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
638
734
  }
639
735
  // Return response data for caller to use
640
736
  return response.data;
@@ -726,19 +822,21 @@ class TestLensReporter {
726
822
  }
727
823
  const attachments = result.attachments;
728
824
  for (const attachment of attachments) {
825
+ const artifactType = this.getArtifactType(attachment.name);
729
826
  if (attachment.path) {
730
827
  // Check if attachment should be processed based on config
731
- const artifactType = this.getArtifactType(attachment.name);
732
828
  const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
733
829
  const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
734
830
  // Skip video if disabled in config
735
831
  if (isVideo && !this.config.enableVideo) {
736
- logger.info(`[SKIP] 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);
737
834
  continue;
738
835
  }
739
836
  // Skip screenshot if disabled in config
740
837
  if (isScreenshot && !this.config.enableScreenshot) {
741
- logger.info(`[SKIP] 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);
742
840
  continue;
743
841
  }
744
842
  try {
@@ -776,13 +874,15 @@ class TestLensReporter {
776
874
  const uploadPromise = Promise.resolve().then(async () => {
777
875
  try {
778
876
  if (!attachment.path) {
779
- logger.info(`[SKIP] [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);
780
879
  return;
781
880
  }
782
881
  const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
783
882
  // Skip if upload failed or file was too large
784
883
  if (!s3Data) {
785
- logger.info(`[SKIP] [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);
786
886
  return;
787
887
  }
788
888
  const artifactData = {
@@ -803,10 +903,11 @@ class TestLensReporter {
803
903
  timestamp: new Date().toISOString(),
804
904
  artifact: artifactData
805
905
  });
806
- logger.info(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
906
+ logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
807
907
  }
808
908
  catch (error) {
809
909
  logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
910
+ this.bumpArtifactStat('failed', artifactType);
810
911
  }
811
912
  });
812
913
  // Track this upload and ensure cleanup on completion
@@ -819,8 +920,12 @@ class TestLensReporter {
819
920
  }
820
921
  catch (error) {
821
922
  logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
923
+ this.bumpArtifactStat('failed', artifactType);
822
924
  }
823
925
  }
926
+ else {
927
+ this.bumpArtifactStat('skipped', artifactType);
928
+ }
824
929
  }
825
930
  }
826
931
  async sendSpecCodeBlocks(specPath) {
@@ -848,7 +953,7 @@ class TestLensReporter {
848
953
  codeBlocks,
849
954
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
850
955
  });
851
- logger.info(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
956
+ logger.info(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
852
957
  }
853
958
  catch (error) {
854
959
  const errorData = error?.response?.data;
@@ -968,6 +1073,12 @@ class TestLensReporter {
968
1073
  return 'trace';
969
1074
  return 'attachment';
970
1075
  }
1076
+ bumpArtifactStat(stat, type) {
1077
+ const bucket = this.artifactStats[stat];
1078
+ if (bucket[type] !== undefined) {
1079
+ bucket[type] += 1;
1080
+ }
1081
+ }
971
1082
  extractTags(test) {
972
1083
  const tags = [];
973
1084
  // Playwright stores tags in the _tags property
@@ -1011,7 +1122,7 @@ class TestLensReporter {
1011
1122
  // Check file size first
1012
1123
  const fileSize = this.getFileSize(filePath);
1013
1124
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
1014
- logger.info(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1125
+ logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1015
1126
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1016
1127
  // Step 1: Request pre-signed URL from server
1017
1128
  const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
@@ -1036,7 +1147,7 @@ class TestLensReporter {
1036
1147
  }
1037
1148
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1038
1149
  // Step 2: Upload directly to S3 using presigned URL
1039
- logger.info(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1150
+ logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1040
1151
  const fileBuffer = fs.readFileSync(filePath);
1041
1152
  // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
1042
1153
  // The backend signs with ServerSideEncryption:'AES256', so we must send that header
@@ -1054,7 +1165,7 @@ class TestLensReporter {
1054
1165
  if (uploadResponse.status !== 200) {
1055
1166
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1056
1167
  }
1057
- logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1168
+ logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1058
1169
  // Step 3: Confirm upload with server to save metadata
1059
1170
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
1060
1171
  const confirmBody = {
@@ -1076,7 +1187,7 @@ class TestLensReporter {
1076
1187
  });
1077
1188
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1078
1189
  const artifact = confirmResponse.data.artifact;
1079
- logger.info(`[OK] [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)'}`);
1080
1191
  return {
1081
1192
  key: s3Key,
1082
1193
  url: artifact.s3Url,
package/index.ts CHANGED
@@ -8,9 +8,68 @@ import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwri
8
8
  import { execSync } from 'child_process';
9
9
  import pino from 'pino';
10
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
+
11
48
  // Create pino logger instance
12
49
  const logger = pino({
13
- level: process.env.LOG_LEVEL || 'info'
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
+ }
14
73
  });
15
74
 
16
75
  // Lazy-load mime module to support ESM
@@ -191,6 +250,12 @@ export class TestLensReporter implements Reporter {
191
250
  private runCreationFailed: boolean = false; // Track if run creation failed due to limits
192
251
  private cliArgs: Record<string, any> = {}; // Store CLI args separately
193
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;
194
259
 
195
260
  /**
196
261
  * Parse custom metadata from environment variables
@@ -423,19 +488,20 @@ export class TestLensReporter implements Reporter {
423
488
  }
424
489
 
425
490
  async onBegin(config: FullConfig, suite: Suite): Promise<void> {
491
+ logger.info(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
426
492
  // Show Build Name if provided, otherwise show Run ID
427
493
  if (this.runMetadata.testlensBuildName) {
428
- logger.info(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
494
+ logger.info(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
429
495
  logger.info(` Run ID: ${this.runId}`);
430
496
  } else {
431
- logger.info(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
497
+ logger.info(`TestLens Reporter starting - Run ID: ${this.runId}`);
432
498
  }
433
499
 
434
500
  // Collect Git information if enabled
435
501
  if (this.config.enableGitInfo) {
436
502
  this.runMetadata.gitInfo = await this.collectGitInfo();
437
503
  if (this.runMetadata.gitInfo) {
438
- logger.info(`📦 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}`);
439
505
  } else {
440
506
  logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
441
507
  }
@@ -462,7 +528,7 @@ export class TestLensReporter implements Reporter {
462
528
 
463
529
  async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
464
530
  // Log which test is starting
465
- logger.info(`\n[TEST] Running test: ${test.title}`);
531
+ logger.info(`[TEST] Running test: ${test.title}`);
466
532
 
467
533
  const specPath = test.location.file;
468
534
  const specKey = `${specPath}-${test.parent.title}`;
@@ -543,6 +609,20 @@ export class TestLensReporter implements Reporter {
543
609
  const testId = this.getTestId(test);
544
610
  let testData = this.testMap.get(testId);
545
611
 
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
+
546
626
  logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
547
627
 
548
628
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
@@ -726,6 +806,12 @@ export class TestLensReporter implements Reporter {
726
806
  if (this.config.enableArtifacts) {
727
807
  // Pass test case DB ID if available for faster lookups
728
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
+ }
729
815
  }
730
816
  }
731
817
 
@@ -803,15 +889,30 @@ export class TestLensReporter implements Reporter {
803
889
 
804
890
  // Wait for all pending artifact uploads to complete before sending runEnd
805
891
  if (this.pendingUploads.size > 0) {
806
- logger.info(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
892
+ logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
807
893
  try {
808
894
  await Promise.all(Array.from(this.pendingUploads));
809
- logger.info(`[OK] All artifact uploads completed`);
895
+ logger.debug(`[OK] All artifact uploads completed`);
810
896
  } catch (error) {
811
897
  logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
812
898
  }
813
899
  }
814
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
+
815
916
  // Calculate final stats
816
917
  const totalTests = Array.from(this.testMap.values()).length;
817
918
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -864,7 +965,7 @@ export class TestLensReporter implements Reporter {
864
965
  }
865
966
  });
866
967
  if (this.config.enableRealTimeStream) {
867
- logger.info(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
968
+ logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
868
969
  }
869
970
  // Return response data for caller to use
870
971
  return response.data;
@@ -958,21 +1059,23 @@ export class TestLensReporter implements Reporter {
958
1059
  const attachments = result.attachments;
959
1060
 
960
1061
  for (const attachment of attachments) {
1062
+ const artifactType = this.getArtifactType(attachment.name);
961
1063
  if (attachment.path) {
962
1064
  // Check if attachment should be processed based on config
963
- const artifactType = this.getArtifactType(attachment.name);
964
1065
  const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
965
1066
  const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
966
1067
 
967
1068
  // Skip video if disabled in config
968
1069
  if (isVideo && !this.config.enableVideo) {
969
- logger.info(`[SKIP] 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);
970
1072
  continue;
971
1073
  }
972
1074
 
973
1075
  // Skip screenshot if disabled in config
974
1076
  if (isScreenshot && !this.config.enableScreenshot) {
975
- logger.info(`[SKIP] 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);
976
1079
  continue;
977
1080
  }
978
1081
 
@@ -1012,7 +1115,8 @@ export class TestLensReporter implements Reporter {
1012
1115
  const uploadPromise = Promise.resolve().then(async () => {
1013
1116
  try {
1014
1117
  if (!attachment.path) {
1015
- logger.info(`[SKIP] [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);
1016
1120
  return;
1017
1121
  }
1018
1122
 
@@ -1020,7 +1124,8 @@ export class TestLensReporter implements Reporter {
1020
1124
 
1021
1125
  // Skip if upload failed or file was too large
1022
1126
  if (!s3Data) {
1023
- logger.info(`[SKIP] [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);
1024
1129
  return;
1025
1130
  }
1026
1131
 
@@ -1044,9 +1149,10 @@ export class TestLensReporter implements Reporter {
1044
1149
  artifact: artifactData
1045
1150
  });
1046
1151
 
1047
- logger.info(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1152
+ logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1048
1153
  } catch (error) {
1049
1154
  logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
1155
+ this.bumpArtifactStat('failed', artifactType);
1050
1156
  }
1051
1157
  });
1052
1158
 
@@ -1060,7 +1166,10 @@ export class TestLensReporter implements Reporter {
1060
1166
  // They will be awaited in onEnd
1061
1167
  } catch (error) {
1062
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);
1063
1170
  }
1171
+ } else {
1172
+ this.bumpArtifactStat('skipped', artifactType);
1064
1173
  }
1065
1174
  }
1066
1175
  }
@@ -1094,7 +1203,7 @@ export class TestLensReporter implements Reporter {
1094
1203
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
1095
1204
  });
1096
1205
 
1097
- logger.info(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1206
+ logger.info(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1098
1207
  } catch (error: any) {
1099
1208
  const errorData = error?.response?.data;
1100
1209
 
@@ -1223,6 +1332,13 @@ export class TestLensReporter implements Reporter {
1223
1332
  return 'attachment';
1224
1333
  }
1225
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
+
1226
1342
  private extractTags(test: TestCase): string[] {
1227
1343
  const tags: string[] = [];
1228
1344
 
@@ -1275,7 +1391,7 @@ export class TestLensReporter implements Reporter {
1275
1391
  const fileSize = this.getFileSize(filePath);
1276
1392
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
1277
1393
 
1278
- logger.info(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1394
+ logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1279
1395
 
1280
1396
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1281
1397
 
@@ -1307,7 +1423,7 @@ export class TestLensReporter implements Reporter {
1307
1423
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1308
1424
 
1309
1425
  // Step 2: Upload directly to S3 using presigned URL
1310
- logger.info(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1426
+ logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1311
1427
 
1312
1428
  const fileBuffer = fs.readFileSync(filePath);
1313
1429
 
@@ -1329,7 +1445,7 @@ export class TestLensReporter implements Reporter {
1329
1445
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1330
1446
  }
1331
1447
 
1332
- logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1448
+ logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1333
1449
 
1334
1450
  // Step 3: Confirm upload with server to save metadata
1335
1451
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
@@ -1355,7 +1471,7 @@ export class TestLensReporter implements Reporter {
1355
1471
 
1356
1472
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1357
1473
  const artifact = confirmResponse.data.artifact;
1358
- logger.info(`[OK] [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)'}`);
1359
1475
  return {
1360
1476
  key: s3Key,
1361
1477
  url: artifact.s3Url,
@@ -1469,4 +1585,3 @@ export class TestLensReporter implements Reporter {
1469
1585
  }
1470
1586
 
1471
1587
  export default TestLensReporter;
1472
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alternative-path/testlens-playwright-reporter",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Universal Playwright reporter for TestLens - works with both TypeScript and JavaScript projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/postinstall.js CHANGED
@@ -1,44 +1,49 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const pino = require('pino');
4
-
5
- const logger = pino({
6
- level: process.env.LOG_LEVEL || 'info'
7
- });
8
-
9
- try {
10
- // Get the parent project's node_modules/.bin directory
11
- // When installed, we are in: project/node_modules/testlens-playwright-reporter/
12
- // We need to go to: project/node_modules/.bin/
13
- const parentBinDir = path.resolve(__dirname, '..', '.bin');
14
- const localBinDir = path.resolve(__dirname, 'node_modules', '.bin');
15
-
16
- // Check if parent bin directory exists
17
- if (!fs.existsSync(parentBinDir)) {
18
- logger.info('Parent .bin directory not found, skipping cross-env setup');
19
- process.exit(0);
20
- }
21
-
22
- // Check if cross-env exists in our node_modules
23
- const crossEnvCmd = process.platform === 'win32' ? 'cross-env.cmd' : 'cross-env';
24
- const crossEnvPs1 = 'cross-env.ps1';
25
- const sourceCmdPath = path.join(localBinDir, crossEnvCmd);
26
- const sourcePs1Path = path.join(localBinDir, crossEnvPs1);
27
-
28
- if (fs.existsSync(sourceCmdPath)) {
29
- const targetCmdPath = path.join(parentBinDir, crossEnvCmd);
30
-
31
- // Copy the file
32
- fs.copyFileSync(sourceCmdPath, targetCmdPath);
33
- logger.info('✓ cross-env binary installed');
34
-
35
- // Also copy PowerShell script on Windows
36
- if (process.platform === 'win32' && fs.existsSync(sourcePs1Path)) {
37
- const targetPs1Path = path.join(parentBinDir, crossEnvPs1);
38
- fs.copyFileSync(sourcePs1Path, targetPs1Path);
39
- }
40
- }
41
- } catch (error) {
42
- // Don't fail installation if this doesn't work
43
- logger.info('Note: Could not setup cross-env automatically:', error.message);
44
- }
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const pino = require('pino');
4
+
5
+ const logger = pino({
6
+ level: process.env.LOG_LEVEL || 'info',
7
+ base: null,
8
+ timestamp: false,
9
+ formatters: {
10
+ level: () => ({})
11
+ }
12
+ });
13
+
14
+ try {
15
+ // Get the parent project's node_modules/.bin directory
16
+ // When installed, we are in: project/node_modules/testlens-playwright-reporter/
17
+ // We need to go to: project/node_modules/.bin/
18
+ const parentBinDir = path.resolve(__dirname, '..', '.bin');
19
+ const localBinDir = path.resolve(__dirname, 'node_modules', '.bin');
20
+
21
+ // Check if parent bin directory exists
22
+ if (!fs.existsSync(parentBinDir)) {
23
+ logger.info('Parent .bin directory not found, skipping cross-env setup');
24
+ process.exit(0);
25
+ }
26
+
27
+ // Check if cross-env exists in our node_modules
28
+ const crossEnvCmd = process.platform === 'win32' ? 'cross-env.cmd' : 'cross-env';
29
+ const crossEnvPs1 = 'cross-env.ps1';
30
+ const sourceCmdPath = path.join(localBinDir, crossEnvCmd);
31
+ const sourcePs1Path = path.join(localBinDir, crossEnvPs1);
32
+
33
+ if (fs.existsSync(sourceCmdPath)) {
34
+ const targetCmdPath = path.join(parentBinDir, crossEnvCmd);
35
+
36
+ // Copy the file
37
+ fs.copyFileSync(sourceCmdPath, targetCmdPath);
38
+ logger.info('✓ cross-env binary installed');
39
+
40
+ // Also copy PowerShell script on Windows
41
+ if (process.platform === 'win32' && fs.existsSync(sourcePs1Path)) {
42
+ const targetPs1Path = path.join(parentBinDir, crossEnvPs1);
43
+ fs.copyFileSync(sourcePs1Path, targetPs1Path);
44
+ }
45
+ }
46
+ } catch (error) {
47
+ // Don't fail installation if this doesn't work
48
+ logger.info('Note: Could not setup cross-env automatically:', error.message);
49
+ }