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 +87 -0
- package/index.js +67 -46
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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