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 +87 -0
- package/index.js +71 -2
- package/index.ts +71 -2
- package/lib/index.js +64 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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