testlens-playwright-reporter 0.2.8 → 0.2.9

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
@@ -250,7 +250,73 @@ class TestLensReporter {
250
250
 
251
251
  async onTestEnd(test, result) {
252
252
  const testId = this.getTestId(test);
253
- const testData = this.testMap.get(testId);
253
+ let testData = this.testMap.get(testId);
254
+
255
+ console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
256
+
257
+ // For skipped tests, onTestBegin might not be called, so we need to create the test data here
258
+ if (!testData) {
259
+ console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
260
+ // Create spec data if not exists (skipped tests might not have spec data either)
261
+ const specPath = test.location.file;
262
+ const specKey = `${specPath}-${test.parent.title}`;
263
+
264
+ if (!this.specMap.has(specKey)) {
265
+ const specData = {
266
+ filePath: path.relative(process.cwd(), specPath),
267
+ testSuiteName: test.parent.title,
268
+ tags: this.extractTags(test),
269
+ startTime: new Date().toISOString(),
270
+ status: 'skipped'
271
+ };
272
+ this.specMap.set(specKey, specData);
273
+
274
+ // Send spec start event to API
275
+ await this.sendToApi({
276
+ type: 'specStart',
277
+ runId: this.runId,
278
+ timestamp: new Date().toISOString(),
279
+ spec: specData
280
+ });
281
+ }
282
+
283
+ // Create test data for skipped test
284
+ testData = {
285
+ id: testId,
286
+ name: test.title,
287
+ status: 'skipped',
288
+ originalStatus: 'skipped',
289
+ duration: 0,
290
+ startTime: new Date().toISOString(),
291
+ endTime: new Date().toISOString(),
292
+ errorMessages: [],
293
+ errors: [],
294
+ retryAttempts: test.retries,
295
+ currentRetry: 0,
296
+ annotations: test.annotations.map(ann => ({
297
+ type: ann.type,
298
+ description: ann.description
299
+ })),
300
+ projectName: test.parent.project()?.name || 'default',
301
+ workerIndex: result.workerIndex,
302
+ parallelIndex: result.parallelIndex,
303
+ location: {
304
+ file: path.relative(process.cwd(), test.location.file),
305
+ line: test.location.line,
306
+ column: test.location.column
307
+ }
308
+ };
309
+
310
+ this.testMap.set(testId, testData);
311
+
312
+ // Send test start event first (so the test gets created in DB)
313
+ await this.sendToApi({
314
+ type: 'testStart',
315
+ runId: this.runId,
316
+ timestamp: new Date().toISOString(),
317
+ test: testData
318
+ });
319
+ }
254
320
 
255
321
  if (testData) {
256
322
  // Update test data with latest result
@@ -356,6 +422,7 @@ class TestLensReporter {
356
422
  const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
357
423
 
358
424
  if (isFinalAttempt) {
425
+ console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
359
426
  // Send test end event to API
360
427
  await this.sendToApi({
361
428
  type: 'testEnd',
@@ -741,7 +808,9 @@ class TestLensReporter {
741
808
 
742
809
  getTestId(test) {
743
810
  const cleanTitle = test.title.replace(/@[\w-]+/g, '').trim();
744
- return `${test.location.file}:${test.location.line}:${cleanTitle}`;
811
+ // Normalize path separators to forward slashes for cross-platform consistency
812
+ const normalizedFile = test.location.file.replace(/\\/g, '/');
813
+ return `${normalizedFile}:${test.location.line}:${cleanTitle}`;
745
814
  }
746
815
 
747
816
  getFileSize(filePath) {
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.2.9",
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",