testlens-playwright-reporter 0.2.8 → 0.3.0

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/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # TestLens Playwright Reporter
2
+
3
+ A Playwright reporter for [TestLens](https://testlens.qa-path.com) - real-time test monitoring dashboard.
4
+
5
+ ## Features
6
+
7
+ - 🚀 **Real-time streaming** - Watch test results as they happen in the dashboard
8
+ - 📸 **Artifact support** - Shows screenshots, videos, and traces
9
+ - 🔄 **Retry tracking** - Monitor test retries and identify flaky tests
10
+ - ⚡ **Cross-platform** - Works on Windows, macOS, and Linux
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install testlens-playwright-reporter
16
+ ```
17
+
18
+ ## Configuration
19
+
20
+ ### TypeScript (`playwright.config.ts`)
21
+
22
+ ```typescript
23
+ import { defineConfig } from '@playwright/test';
24
+
25
+ export default defineConfig({
26
+ use: {
27
+ // Enable these for better debugging and artifact capture
28
+ screenshot: 'on',
29
+ video: 'on',
30
+ trace: 'on',
31
+ },
32
+ reporter: [
33
+ ['testlens-playwright-reporter', {
34
+ apiKey: 'your-api-key-here',
35
+ }]
36
+ ],
37
+ });
38
+ ```
39
+
40
+ ### JavaScript (`playwright.config.js`)
41
+
42
+ ```javascript
43
+ const { defineConfig } = require('@playwright/test');
44
+
45
+ module.exports = defineConfig({
46
+ use: {
47
+ // Enable these for better debugging and artifact capture
48
+ screenshot: 'on',
49
+ video: 'on',
50
+ trace: 'on',
51
+ },
52
+ reporter: [
53
+ ['testlens-playwright-reporter', {
54
+ apiKey: 'your-api-key-here',
55
+ }]
56
+ ],
57
+ });
58
+ ```
59
+
60
+ > 💡 **Tip:** Keep `screenshot`, `video`, and `trace` set to `'on'` for better debugging experience. TestLens automatically uploads these artifacts for failed tests, making it easier to identify issues.
61
+
62
+ ### Configuration Options
63
+
64
+ | Option | Type | Default | Description |
65
+ |--------|------|---------|-------------|
66
+ | `apiKey` | `string` | **Required** | Your TestLens API key |
67
+
68
+ ## Artifacts
69
+
70
+ TestLens automatically captures and uploads:
71
+
72
+ | Artifact | Description |
73
+ |----------|-------------|
74
+ | **Screenshots** | Visual snapshots of test failures |
75
+ | **Videos** | Full video recording of test execution |
76
+ | **Traces** | Playwright trace files for step-by-step debugging |
77
+
78
+ These artifacts are viewable directly in the TestLens dashboard for easy debugging.
79
+
80
+ ## Requirements
81
+
82
+ - Node.js >= 16.0.0
83
+ - Playwright >= 1.40.0
84
+
85
+ ## License
86
+
87
+ MIT License
package/index.js CHANGED
@@ -46,15 +46,12 @@ class TestLensReporter {
46
46
  if (this.config.ignoreSslErrors) {
47
47
  // Explicit configuration option
48
48
  rejectUnauthorized = false;
49
- console.log('âš ī¸ SSL certificate validation disabled via ignoreSslErrors option');
50
49
  } else if (this.config.rejectUnauthorized === false) {
51
50
  // Explicit configuration option
52
51
  rejectUnauthorized = false;
53
- console.log('âš ī¸ SSL certificate validation disabled via rejectUnauthorized option');
54
52
  } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
55
53
  // Environment variable override
56
54
  rejectUnauthorized = false;
57
- console.log('âš ī¸ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
58
55
  }
59
56
 
60
57
  // Set up axios instance with retry logic and enhanced SSL handling
@@ -144,18 +141,9 @@ class TestLensReporter {
144
141
  }
145
142
 
146
143
  async onBegin(config, suite) {
147
- console.log(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
148
-
149
144
  // Collect Git information if enabled
150
145
  if (this.config.enableGitInfo) {
151
146
  this.runMetadata.gitInfo = await this.collectGitInfo();
152
- if (this.runMetadata.gitInfo) {
153
- console.log(`đŸ“Ļ Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
154
- } else {
155
- console.log(`âš ī¸ Git info collection returned null - not in a git repository or git not available`);
156
- }
157
- } else {
158
- console.log(`â„šī¸ Git info collection disabled (enableGitInfo: false)`);
159
147
  }
160
148
 
161
149
  // Add shard information if available
@@ -250,7 +238,70 @@ class TestLensReporter {
250
238
 
251
239
  async onTestEnd(test, result) {
252
240
  const testId = this.getTestId(test);
253
- const testData = this.testMap.get(testId);
241
+ let testData = this.testMap.get(testId);
242
+
243
+ // For skipped tests, onTestBegin might not be called, so we need to create the test data here
244
+ if (!testData) {
245
+ // Create spec data if not exists (skipped tests might not have spec data either)
246
+ const specPath = test.location.file;
247
+ const specKey = `${specPath}-${test.parent.title}`;
248
+
249
+ if (!this.specMap.has(specKey)) {
250
+ const specData = {
251
+ filePath: path.relative(process.cwd(), specPath),
252
+ testSuiteName: test.parent.title,
253
+ tags: this.extractTags(test),
254
+ startTime: new Date().toISOString(),
255
+ status: 'skipped'
256
+ };
257
+ this.specMap.set(specKey, specData);
258
+
259
+ // Send spec start event to API
260
+ await this.sendToApi({
261
+ type: 'specStart',
262
+ runId: this.runId,
263
+ timestamp: new Date().toISOString(),
264
+ spec: specData
265
+ });
266
+ }
267
+
268
+ // Create test data for skipped test
269
+ testData = {
270
+ id: testId,
271
+ name: test.title,
272
+ status: 'skipped',
273
+ originalStatus: 'skipped',
274
+ duration: 0,
275
+ startTime: new Date().toISOString(),
276
+ endTime: new Date().toISOString(),
277
+ errorMessages: [],
278
+ errors: [],
279
+ retryAttempts: test.retries,
280
+ currentRetry: 0,
281
+ annotations: test.annotations.map(ann => ({
282
+ type: ann.type,
283
+ description: ann.description
284
+ })),
285
+ projectName: test.parent.project()?.name || 'default',
286
+ workerIndex: result.workerIndex,
287
+ parallelIndex: result.parallelIndex,
288
+ location: {
289
+ file: path.relative(process.cwd(), test.location.file),
290
+ line: test.location.line,
291
+ column: test.location.column
292
+ }
293
+ };
294
+
295
+ this.testMap.set(testId, testData);
296
+
297
+ // Send test start event first (so the test gets created in DB)
298
+ await this.sendToApi({
299
+ type: 'testStart',
300
+ runId: this.runId,
301
+ timestamp: new Date().toISOString(),
302
+ test: testData
303
+ });
304
+ }
254
305
 
255
306
  if (testData) {
256
307
  // Update test data with latest result
@@ -441,19 +492,12 @@ class TestLensReporter {
441
492
  });
442
493
 
443
494
  // Wait for background artifact processing to complete (up to 10 seconds)
444
- console.log('âŗ Waiting for background artifact processing to complete...');
445
495
  await new Promise(resolve => setTimeout(resolve, 10000));
446
-
447
- console.log(`📊 TestLens Report completed - Run ID: ${this.runId}`);
448
- console.log(`đŸŽ¯ Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
449
496
  }
450
497
 
451
498
  async sendToApi(payload) {
452
499
  try {
453
500
  const response = await this.axiosInstance.post('', payload);
454
- if (this.config.enableRealTimeStream) {
455
- console.log(`✅ Sent ${payload.type} event to TestLens`);
456
- }
457
501
  } catch (error) {
458
502
  console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
459
503
  message: error?.message || 'Unknown error',
@@ -467,19 +511,10 @@ class TestLensReporter {
467
511
 
468
512
  async processArtifacts(testId, result) {
469
513
  const attachments = result.attachments;
470
- console.log(`🔍 Processing artifacts for test ${testId}: ${attachments ? attachments.length : 0} attachments found`);
471
-
472
- if (attachments && attachments.length > 0) {
473
- console.log('📎 Attachment details:');
474
- attachments.forEach((attachment, index) => {
475
- console.log(` ${index + 1}. ${attachment.name} (${attachment.contentType}) - Path: ${attachment.path}`);
476
- });
477
- }
478
514
 
479
515
  // Process artifacts with controlled async handling to ensure uploads complete
480
516
  if (attachments && attachments.length > 0) {
481
517
  // Process all artifacts asynchronously but track completion
482
- console.log(`🔄 Processing ${attachments.length} artifacts asynchronously...`);
483
518
 
484
519
  // Use process.nextTick to defer processing to next event loop iteration
485
520
  process.nextTick(async () => {
@@ -493,18 +528,15 @@ class TestLensReporter {
493
528
 
494
529
  // Skip video if disabled in config
495
530
  if (isVideo && !this.config.enableVideo) {
496
- console.log(`â­ī¸ Skipping video artifact ${attachment.name} - video capture disabled in config`);
497
531
  continue;
498
532
  }
499
533
 
500
534
  // Skip screenshot if disabled in config
501
535
  if (isScreenshot && !this.config.enableScreenshot) {
502
- console.log(`â­ī¸ Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
503
536
  continue;
504
537
  }
505
538
 
506
539
  try {
507
- console.log(`📤 Processing ${attachment.name} asynchronously...`);
508
540
 
509
541
  // Determine proper filename with extension
510
542
  // Playwright attachment.name often doesn't have extension, so we need to derive it
@@ -541,7 +573,6 @@ class TestLensReporter {
541
573
 
542
574
  // Skip if upload failed or file was too large
543
575
  if (!s3Data) {
544
- console.log(`â­ī¸ Skipping artifact ${attachment.name} - upload failed or file too large`);
545
576
  continue;
546
577
  }
547
578
 
@@ -564,8 +595,6 @@ class TestLensReporter {
564
595
  timestamp: new Date().toISOString(),
565
596
  artifact: artifactData
566
597
  });
567
-
568
- console.log(`📎 Processed artifact: ${fileName} (uploaded to S3)`);
569
598
  } catch (error) {
570
599
  console.error(`❌ Failed to process ${attachment.name}:`, error.message);
571
600
  }
@@ -598,8 +627,6 @@ class TestLensReporter {
598
627
  codeBlocks,
599
628
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
600
629
  });
601
-
602
- console.log(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
603
630
  } catch (error) {
604
631
  console.error('Failed to send spec code blocks:', error?.response?.data || error?.message || 'Unknown error');
605
632
  }
@@ -693,7 +720,6 @@ class TestLensReporter {
693
720
  }
694
721
  } catch (e) {
695
722
  // Remote info is optional - handle gracefully
696
- console.log('â„šī¸ No git remote configured, skipping remote info');
697
723
  }
698
724
 
699
725
  const isDirty = execSync('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
@@ -741,7 +767,9 @@ class TestLensReporter {
741
767
 
742
768
  getTestId(test) {
743
769
  const cleanTitle = test.title.replace(/@[\w-]+/g, '').trim();
744
- return `${test.location.file}:${test.location.line}:${cleanTitle}`;
770
+ // Normalize path separators to forward slashes for cross-platform consistency
771
+ const normalizedFile = test.location.file.replace(/\\/g, '/');
772
+ return `${normalizedFile}:${test.location.line}:${cleanTitle}`;
745
773
  }
746
774
 
747
775
  getFileSize(filePath) {
@@ -759,8 +787,6 @@ class TestLensReporter {
759
787
  // Check file size first
760
788
  const fileSize = this.getFileSize(filePath);
761
789
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
762
-
763
- console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
764
790
 
765
791
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
766
792
 
@@ -785,8 +811,6 @@ class TestLensReporter {
785
811
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
786
812
 
787
813
  // Step 2: Upload directly to S3 using presigned URL
788
- console.log(`âŦ†ī¸ Uploading ${fileName} directly to S3 (bypass server)...`);
789
-
790
814
  const fileBuffer = fs.readFileSync(filePath);
791
815
 
792
816
  // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
@@ -807,8 +831,6 @@ class TestLensReporter {
807
831
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
808
832
  }
809
833
 
810
- console.log(`✅ S3 direct upload completed for ${fileName}`);
811
-
812
834
  // Step 3: Confirm upload with server to save metadata
813
835
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
814
836
  const confirmResponse = await this.axiosInstance.post(confirmEndpoint, {
@@ -826,7 +848,6 @@ class TestLensReporter {
826
848
 
827
849
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
828
850
  const artifact = confirmResponse.data.artifact;
829
- console.log(`✅ Upload confirmed and saved to database`);
830
851
  return {
831
852
  key: s3Key,
832
853
  url: artifact.s3Url,
package/index.ts CHANGED
@@ -409,7 +409,73 @@ export class TestLensReporter implements Reporter {
409
409
 
410
410
  async onTestEnd(test: TestCase, result: TestResult): Promise<void> {
411
411
  const testId = this.getTestId(test);
412
- const testData = this.testMap.get(testId);
412
+ let testData = this.testMap.get(testId);
413
+
414
+ console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
415
+
416
+ // For skipped tests, onTestBegin might not be called, so we need to create the test data here
417
+ if (!testData) {
418
+ console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
419
+ // Create spec data if not exists (skipped tests might not have spec data either)
420
+ const specPath = test.location.file;
421
+ const specKey = `${specPath}-${test.parent.title}`;
422
+
423
+ if (!this.specMap.has(specKey)) {
424
+ const specData: SpecData = {
425
+ filePath: path.relative(process.cwd(), specPath),
426
+ testSuiteName: test.parent.title,
427
+ tags: this.extractTags(test),
428
+ startTime: new Date().toISOString(),
429
+ status: 'skipped'
430
+ };
431
+ this.specMap.set(specKey, specData);
432
+
433
+ // Send spec start event to API
434
+ await this.sendToApi({
435
+ type: 'specStart',
436
+ runId: this.runId,
437
+ timestamp: new Date().toISOString(),
438
+ spec: specData
439
+ });
440
+ }
441
+
442
+ // Create test data for skipped test
443
+ testData = {
444
+ id: testId,
445
+ name: test.title,
446
+ status: 'skipped',
447
+ originalStatus: 'skipped',
448
+ duration: 0,
449
+ startTime: new Date().toISOString(),
450
+ endTime: new Date().toISOString(),
451
+ errorMessages: [],
452
+ errors: [],
453
+ retryAttempts: test.retries,
454
+ currentRetry: 0,
455
+ annotations: test.annotations.map((ann: any) => ({
456
+ type: ann.type,
457
+ description: ann.description
458
+ })),
459
+ projectName: test.parent.project()?.name || 'default',
460
+ workerIndex: result.workerIndex,
461
+ parallelIndex: result.parallelIndex,
462
+ location: {
463
+ file: path.relative(process.cwd(), test.location.file),
464
+ line: test.location.line,
465
+ column: test.location.column
466
+ }
467
+ };
468
+
469
+ this.testMap.set(testId, testData);
470
+
471
+ // Send test start event first (so the test gets created in DB)
472
+ await this.sendToApi({
473
+ type: 'testStart',
474
+ runId: this.runId,
475
+ timestamp: new Date().toISOString(),
476
+ test: testData
477
+ });
478
+ }
413
479
 
414
480
  if (testData) {
415
481
  // Update test data with latest result
@@ -515,6 +581,7 @@ export class TestLensReporter implements Reporter {
515
581
  const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
516
582
 
517
583
  if (isFinalAttempt) {
584
+ console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
518
585
  // Send test end event to API
519
586
  await this.sendToApi({
520
587
  type: 'testEnd',
@@ -879,7 +946,9 @@ export class TestLensReporter implements Reporter {
879
946
 
880
947
  private getTestId(test: TestCase): string {
881
948
  const cleanTitle = test.title.replace(/@[\w-]+/g, '').trim();
882
- return `${test.location.file}:${test.location.line}:${cleanTitle}`;
949
+ // Normalize path separators to forward slashes for cross-platform consistency
950
+ const normalizedFile = test.location.file.replace(/\\/g, '/');
951
+ return `${normalizedFile}:${test.location.line}:${cleanTitle}`;
883
952
  }
884
953
 
885
954
 
package/lib/index.js CHANGED
@@ -264,7 +264,66 @@ class TestLensReporter {
264
264
  }
265
265
  async onTestEnd(test, result) {
266
266
  const testId = this.getTestId(test);
267
- const testData = this.testMap.get(testId);
267
+ let testData = this.testMap.get(testId);
268
+ console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
269
+ // For skipped tests, onTestBegin might not be called, so we need to create the test data here
270
+ if (!testData) {
271
+ console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
272
+ // Create spec data if not exists (skipped tests might not have spec data either)
273
+ const specPath = test.location.file;
274
+ const specKey = `${specPath}-${test.parent.title}`;
275
+ if (!this.specMap.has(specKey)) {
276
+ const specData = {
277
+ filePath: path.relative(process.cwd(), specPath),
278
+ testSuiteName: test.parent.title,
279
+ tags: this.extractTags(test),
280
+ startTime: new Date().toISOString(),
281
+ status: 'skipped'
282
+ };
283
+ this.specMap.set(specKey, specData);
284
+ // Send spec start event to API
285
+ await this.sendToApi({
286
+ type: 'specStart',
287
+ runId: this.runId,
288
+ timestamp: new Date().toISOString(),
289
+ spec: specData
290
+ });
291
+ }
292
+ // Create test data for skipped test
293
+ testData = {
294
+ id: testId,
295
+ name: test.title,
296
+ status: 'skipped',
297
+ originalStatus: 'skipped',
298
+ duration: 0,
299
+ startTime: new Date().toISOString(),
300
+ endTime: new Date().toISOString(),
301
+ errorMessages: [],
302
+ errors: [],
303
+ retryAttempts: test.retries,
304
+ currentRetry: 0,
305
+ annotations: test.annotations.map((ann) => ({
306
+ type: ann.type,
307
+ description: ann.description
308
+ })),
309
+ projectName: test.parent.project()?.name || 'default',
310
+ workerIndex: result.workerIndex,
311
+ parallelIndex: result.parallelIndex,
312
+ location: {
313
+ file: path.relative(process.cwd(), test.location.file),
314
+ line: test.location.line,
315
+ column: test.location.column
316
+ }
317
+ };
318
+ this.testMap.set(testId, testData);
319
+ // Send test start event first (so the test gets created in DB)
320
+ await this.sendToApi({
321
+ type: 'testStart',
322
+ runId: this.runId,
323
+ timestamp: new Date().toISOString(),
324
+ test: testData
325
+ });
326
+ }
268
327
  if (testData) {
269
328
  // Update test data with latest result
270
329
  testData.originalStatus = result.status;
@@ -353,6 +412,7 @@ class TestLensReporter {
353
412
  // If test passed or this is the last retry, send the event
354
413
  const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
355
414
  if (isFinalAttempt) {
415
+ console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
356
416
  // Send test end event to API
357
417
  await this.sendToApi({
358
418
  type: 'testEnd',
@@ -681,7 +741,9 @@ class TestLensReporter {
681
741
  }
682
742
  getTestId(test) {
683
743
  const cleanTitle = test.title.replace(/@[\w-]+/g, '').trim();
684
- return `${test.location.file}:${test.location.line}:${cleanTitle}`;
744
+ // Normalize path separators to forward slashes for cross-platform consistency
745
+ const normalizedFile = test.location.file.replace(/\\/g, '/');
746
+ return `${normalizedFile}:${test.location.line}:${cleanTitle}`;
685
747
  }
686
748
  async uploadArtifactToS3(filePath, testId, fileName) {
687
749
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testlens-playwright-reporter",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
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",