testlens-playwright-reporter 0.3.4 → 0.3.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/README.md +83 -7
- package/index.d.ts +4 -3
- package/index.js +57 -28
- package/index.ts +66 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,46 +17,122 @@ npm install testlens-playwright-reporter
|
|
|
17
17
|
|
|
18
18
|
## Configuration
|
|
19
19
|
|
|
20
|
-
###
|
|
20
|
+
### Quick Start (Recommended)
|
|
21
21
|
|
|
22
|
+
The simplest config - API key and metadata are passed via environment variables:
|
|
23
|
+
|
|
24
|
+
**TypeScript (`playwright.config.ts`)**
|
|
25
|
+
```typescript
|
|
26
|
+
import { defineConfig } from '@playwright/test';
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
use: {
|
|
30
|
+
screenshot: 'on',
|
|
31
|
+
video: 'on',
|
|
32
|
+
trace: 'on',
|
|
33
|
+
},
|
|
34
|
+
reporter: [
|
|
35
|
+
['testlens-playwright-reporter']
|
|
36
|
+
// API key is auto-detected from TESTLENS_API_KEY env var
|
|
37
|
+
// Build metadata is auto-detected from testlensBuildName/testlensBuildTag env vars
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**JavaScript (`playwright.config.js`)**
|
|
43
|
+
```javascript
|
|
44
|
+
const { defineConfig } = require('@playwright/test');
|
|
45
|
+
|
|
46
|
+
module.exports = defineConfig({
|
|
47
|
+
use: {
|
|
48
|
+
screenshot: 'on',
|
|
49
|
+
video: 'on',
|
|
50
|
+
trace: 'on',
|
|
51
|
+
},
|
|
52
|
+
reporter: [
|
|
53
|
+
['testlens-playwright-reporter']
|
|
54
|
+
// API key is auto-detected from TESTLENS_API_KEY env var
|
|
55
|
+
// Build metadata is auto-detected from testlensBuildName/testlensBuildTag env vars
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Advanced Configuration (Optional)
|
|
61
|
+
|
|
62
|
+
If you prefer to set API key or metadata explicitly in config:
|
|
63
|
+
|
|
64
|
+
**TypeScript (`playwright.config.ts`)**
|
|
22
65
|
```typescript
|
|
23
66
|
import { defineConfig } from '@playwright/test';
|
|
24
67
|
|
|
25
68
|
export default defineConfig({
|
|
26
69
|
use: {
|
|
27
|
-
// Enable these for better debugging and artifact capture
|
|
28
70
|
screenshot: 'on',
|
|
29
71
|
video: 'on',
|
|
30
72
|
trace: 'on',
|
|
31
73
|
},
|
|
32
74
|
reporter: [
|
|
33
75
|
['testlens-playwright-reporter', {
|
|
34
|
-
apiKey: 'your-api-key-here',
|
|
76
|
+
apiKey: process.env.TESTLENS_API_KEY || 'your-api-key-here',
|
|
77
|
+
|
|
78
|
+
// Optional: explicitly forward build metadata from env vars
|
|
79
|
+
customMetadata: {
|
|
80
|
+
testlensBuildName: process.env.testlensBuildName,
|
|
81
|
+
testlensBuildTag: process.env.testlensBuildTag,
|
|
82
|
+
},
|
|
35
83
|
}]
|
|
36
84
|
],
|
|
37
85
|
});
|
|
38
86
|
```
|
|
39
87
|
|
|
40
|
-
|
|
41
|
-
|
|
88
|
+
**JavaScript (`playwright.config.js`)**
|
|
42
89
|
```javascript
|
|
43
90
|
const { defineConfig } = require('@playwright/test');
|
|
44
91
|
|
|
45
92
|
module.exports = defineConfig({
|
|
46
93
|
use: {
|
|
47
|
-
// Enable these for better debugging and artifact capture
|
|
48
94
|
screenshot: 'on',
|
|
49
95
|
video: 'on',
|
|
50
96
|
trace: 'on',
|
|
51
97
|
},
|
|
52
98
|
reporter: [
|
|
53
99
|
['testlens-playwright-reporter', {
|
|
54
|
-
apiKey: 'your-api-key-here',
|
|
100
|
+
apiKey: process.env.TESTLENS_API_KEY || 'your-api-key-here',
|
|
101
|
+
|
|
102
|
+
// Optional: explicitly forward build metadata from env vars
|
|
103
|
+
customMetadata: {
|
|
104
|
+
testlensBuildName: process.env.testlensBuildName,
|
|
105
|
+
testlensBuildTag: process.env.testlensBuildTag,
|
|
106
|
+
},
|
|
55
107
|
}]
|
|
56
108
|
],
|
|
57
109
|
});
|
|
58
110
|
```
|
|
59
111
|
|
|
112
|
+
## Run With Build Name & Tag (UI Mode)
|
|
113
|
+
|
|
114
|
+
Use `testlens-cross-env` (bundled with this package) to set API key and metadata cross-platform, including Windows.
|
|
115
|
+
|
|
116
|
+
### Command
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx testlens-cross-env TESTLENS_API_KEY="<your-api-key>" testlensBuildName="Testing Build Local Environment" testlensBuildTag="smoke" playwright test --ui
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### What Gets Auto-Detected
|
|
123
|
+
|
|
124
|
+
The reporter automatically reads these environment variables (no config changes needed):
|
|
125
|
+
|
|
126
|
+
- **API Key**: `TESTLENS_API_KEY` (also checks: `testlens_api_key`, `TESTLENS_KEY`, `testlensApiKey`, `PLAYWRIGHT_API_KEY`, `PW_API_KEY`)
|
|
127
|
+
- **Build Name**: `testlensBuildName` (also checks: `TESTLENS_BUILD_NAME`, `BUILDNAME`, `BUILD_NAME`)
|
|
128
|
+
- **Build Tag**: `testlensBuildTag` (also checks: `TESTLENS_BUILD_TAG`, `BUILDTAG`, `BUILD_TAG`)
|
|
129
|
+
|
|
130
|
+
### Notes
|
|
131
|
+
|
|
132
|
+
- No config changes required - just pass env vars in the command
|
|
133
|
+
- `testlensBuildName` and `testlensBuildTag` are sent to TestLens as run `custom_metadata`
|
|
134
|
+
- Multiple tags: use comma-separated list `testlensBuildTag="smoke,regression"`
|
|
135
|
+
|
|
60
136
|
> 💡 **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
137
|
|
|
62
138
|
### Configuration Options
|
package/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface TestLensReporterConfig {
|
|
|
27
27
|
/** Alternative SSL option - set to true to ignore SSL certificate errors */
|
|
28
28
|
ignoreSslErrors?: boolean;
|
|
29
29
|
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
30
|
-
customMetadata?: Record<string, string>;
|
|
30
|
+
customMetadata?: Record<string, string | string[]>;
|
|
31
31
|
}
|
|
32
32
|
export interface TestLensReporterOptions {
|
|
33
33
|
/** TestLens API endpoint URL */
|
|
@@ -57,7 +57,7 @@ export interface TestLensReporterOptions {
|
|
|
57
57
|
/** Alternative SSL option - set to true to ignore SSL certificate errors */
|
|
58
58
|
ignoreSslErrors?: boolean;
|
|
59
59
|
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
60
|
-
customMetadata?: Record<string, string>;
|
|
60
|
+
customMetadata?: Record<string, string | string[]>;
|
|
61
61
|
}
|
|
62
62
|
export interface GitInfo {
|
|
63
63
|
branch: string;
|
|
@@ -100,7 +100,7 @@ export interface RunMetadata {
|
|
|
100
100
|
skippedTests?: number;
|
|
101
101
|
status?: string;
|
|
102
102
|
testlensBuildName?: string;
|
|
103
|
-
customMetadata?: Record<string, string>;
|
|
103
|
+
customMetadata?: Record<string, string | string[]>;
|
|
104
104
|
}
|
|
105
105
|
export interface TestError {
|
|
106
106
|
message: string;
|
|
@@ -158,6 +158,7 @@ export declare class TestLensReporter implements Reporter {
|
|
|
158
158
|
private specMap;
|
|
159
159
|
private testMap;
|
|
160
160
|
private runCreationFailed;
|
|
161
|
+
private cliArgs;
|
|
161
162
|
/**
|
|
162
163
|
* Parse custom metadata from environment variables
|
|
163
164
|
* Checks for common metadata environment variables
|
package/index.js
CHANGED
|
@@ -28,8 +28,9 @@ class TestLensReporter {
|
|
|
28
28
|
const customArgs = {};
|
|
29
29
|
// Common environment variable names for build metadata
|
|
30
30
|
const envVarMappings = {
|
|
31
|
-
|
|
32
|
-
'
|
|
31
|
+
// Support both TestLens-specific names (recommended) and common CI names
|
|
32
|
+
'testlensBuildTag': ['testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
|
|
33
|
+
'testlensBuildName': ['testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
|
|
33
34
|
'environment': ['ENVIRONMENT', 'ENV', 'NODE_ENV', 'DEPLOYMENT_ENV'],
|
|
34
35
|
'branch': ['BRANCH', 'GIT_BRANCH', 'CI_COMMIT_BRANCH', 'GITHUB_REF_NAME'],
|
|
35
36
|
'team': ['TEAM', 'TEAM_NAME'],
|
|
@@ -41,8 +42,15 @@ class TestLensReporter {
|
|
|
41
42
|
for (const envVar of envVars) {
|
|
42
43
|
const value = process.env[envVar];
|
|
43
44
|
if (value) {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// For testlensBuildTag, support comma-separated values
|
|
46
|
+
if (key === 'testlensBuildTag' && value.includes(',')) {
|
|
47
|
+
customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
|
48
|
+
console.log(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
customArgs[key] = value;
|
|
52
|
+
console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
|
|
53
|
+
}
|
|
46
54
|
break; // Use first match
|
|
47
55
|
}
|
|
48
56
|
}
|
|
@@ -51,8 +59,10 @@ class TestLensReporter {
|
|
|
51
59
|
}
|
|
52
60
|
constructor(options) {
|
|
53
61
|
this.runCreationFailed = false; // Track if run creation failed due to limits
|
|
62
|
+
this.cliArgs = {}; // Store CLI args separately
|
|
54
63
|
// Parse custom CLI arguments
|
|
55
64
|
const customArgs = TestLensReporter.parseCustomArgs();
|
|
65
|
+
this.cliArgs = customArgs; // Store CLI args separately for later use
|
|
56
66
|
// Allow API key from environment variable if not provided in config
|
|
57
67
|
// Check multiple environment variable names in priority order (uppercase and lowercase)
|
|
58
68
|
const apiKey = options.apiKey
|
|
@@ -77,7 +87,7 @@ class TestLensReporter {
|
|
|
77
87
|
flushInterval: options.flushInterval || 5000,
|
|
78
88
|
retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
|
|
79
89
|
timeout: options.timeout || 60000,
|
|
80
|
-
customMetadata: { ...
|
|
90
|
+
customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
|
|
81
91
|
};
|
|
82
92
|
if (!this.config.apiKey) {
|
|
83
93
|
throw new Error('API_KEY is required for TestLensReporter. Pass it as apiKey option in your playwright config or set one of these environment variables: TESTLENS_API_KEY, TESTLENS_KEY, PLAYWRIGHT_API_KEY, PW_API_KEY, API_KEY, or APIKEY.');
|
|
@@ -163,7 +173,9 @@ class TestLensReporter {
|
|
|
163
173
|
metadata.customMetadata = this.config.customMetadata;
|
|
164
174
|
// Extract testlensBuildName as a dedicated field for dashboard display
|
|
165
175
|
if (this.config.customMetadata.testlensBuildName) {
|
|
166
|
-
|
|
176
|
+
const buildName = this.config.customMetadata.testlensBuildName;
|
|
177
|
+
// Handle both string and array (take first element if array)
|
|
178
|
+
metadata.testlensBuildName = Array.isArray(buildName) ? buildName[0] : buildName;
|
|
167
179
|
}
|
|
168
180
|
}
|
|
169
181
|
return metadata;
|
|
@@ -458,16 +470,17 @@ class TestLensReporter {
|
|
|
458
470
|
const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
|
|
459
471
|
if (isFinalAttempt) {
|
|
460
472
|
console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
461
|
-
// Send test end event to API
|
|
462
|
-
await this.sendToApi({
|
|
473
|
+
// Send test end event to API and get response
|
|
474
|
+
const testEndResponse = await this.sendToApi({
|
|
463
475
|
type: 'testEnd',
|
|
464
476
|
runId: this.runId,
|
|
465
477
|
timestamp: new Date().toISOString(),
|
|
466
478
|
test: testData
|
|
467
479
|
});
|
|
468
|
-
// Handle artifacts
|
|
480
|
+
// Handle artifacts (test case is now guaranteed to be in database)
|
|
469
481
|
if (this.config.enableArtifacts) {
|
|
470
|
-
|
|
482
|
+
// Pass test case DB ID if available for faster lookups
|
|
483
|
+
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
471
484
|
}
|
|
472
485
|
}
|
|
473
486
|
}
|
|
@@ -556,7 +569,7 @@ class TestLensReporter {
|
|
|
556
569
|
async sendToApi(payload) {
|
|
557
570
|
// Skip sending if run creation already failed
|
|
558
571
|
if (this.runCreationFailed && payload.type !== 'runStart') {
|
|
559
|
-
return;
|
|
572
|
+
return null;
|
|
560
573
|
}
|
|
561
574
|
try {
|
|
562
575
|
const response = await this.axiosInstance.post('', payload, {
|
|
@@ -567,6 +580,8 @@ class TestLensReporter {
|
|
|
567
580
|
if (this.config.enableRealTimeStream) {
|
|
568
581
|
console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
569
582
|
}
|
|
583
|
+
// Return response data for caller to use
|
|
584
|
+
return response.data;
|
|
570
585
|
}
|
|
571
586
|
catch (error) {
|
|
572
587
|
const errorData = error?.response?.data;
|
|
@@ -647,7 +662,7 @@ class TestLensReporter {
|
|
|
647
662
|
// Don't throw error to avoid breaking test execution
|
|
648
663
|
}
|
|
649
664
|
}
|
|
650
|
-
async processArtifacts(testId, result) {
|
|
665
|
+
async processArtifacts(testId, result, testCaseDbId) {
|
|
651
666
|
// Skip artifact processing if run creation failed
|
|
652
667
|
if (this.runCreationFailed) {
|
|
653
668
|
return;
|
|
@@ -699,11 +714,11 @@ class TestLensReporter {
|
|
|
699
714
|
}
|
|
700
715
|
}
|
|
701
716
|
}
|
|
702
|
-
// Upload to S3 first
|
|
703
|
-
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName);
|
|
717
|
+
// Upload to S3 first (pass DB ID if available for faster lookup)
|
|
718
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
|
|
704
719
|
// Skip if upload failed or file was too large
|
|
705
720
|
if (!s3Data) {
|
|
706
|
-
console.log(`⏭️ Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
721
|
+
console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
707
722
|
continue;
|
|
708
723
|
}
|
|
709
724
|
const artifactData = {
|
|
@@ -724,10 +739,10 @@ class TestLensReporter {
|
|
|
724
739
|
timestamp: new Date().toISOString(),
|
|
725
740
|
artifact: artifactData
|
|
726
741
|
});
|
|
727
|
-
console.log(`📎 Processed artifact: ${fileName}
|
|
742
|
+
console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
728
743
|
}
|
|
729
744
|
catch (error) {
|
|
730
|
-
console.error(`❌ Failed to process artifact ${attachment.name}:`, error.message);
|
|
745
|
+
console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, error.message);
|
|
731
746
|
}
|
|
732
747
|
}
|
|
733
748
|
}
|
|
@@ -893,9 +908,13 @@ class TestLensReporter {
|
|
|
893
908
|
if (tagMatches) {
|
|
894
909
|
tags.push(...tagMatches);
|
|
895
910
|
}
|
|
896
|
-
// Add testlensBuildTag
|
|
897
|
-
|
|
898
|
-
|
|
911
|
+
// Add testlensBuildTag: CLI args take precedence over config
|
|
912
|
+
const buildTagSource = this.cliArgs.testlensBuildTag || this.config.customMetadata?.testlensBuildTag;
|
|
913
|
+
if (buildTagSource) {
|
|
914
|
+
const buildTags = Array.isArray(buildTagSource)
|
|
915
|
+
? buildTagSource
|
|
916
|
+
: [buildTagSource];
|
|
917
|
+
buildTags.forEach(tag => tags.push(`@${tag}`));
|
|
899
918
|
}
|
|
900
919
|
// Remove duplicates and return
|
|
901
920
|
return [...new Set(tags)];
|
|
@@ -906,7 +925,7 @@ class TestLensReporter {
|
|
|
906
925
|
const normalizedFile = test.location.file.replace(/\\/g, '/');
|
|
907
926
|
return `${normalizedFile}:${test.location.line}:${cleanTitle}`;
|
|
908
927
|
}
|
|
909
|
-
async uploadArtifactToS3(filePath, testId, fileName) {
|
|
928
|
+
async uploadArtifactToS3(filePath, testId, fileName, testCaseDbId) {
|
|
910
929
|
try {
|
|
911
930
|
// Check file size first
|
|
912
931
|
const fileSize = this.getFileSize(filePath);
|
|
@@ -915,7 +934,7 @@ class TestLensReporter {
|
|
|
915
934
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
916
935
|
// Step 1: Request pre-signed URL from server
|
|
917
936
|
const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
|
|
918
|
-
const
|
|
937
|
+
const requestBody = {
|
|
919
938
|
apiKey: this.config.apiKey,
|
|
920
939
|
testRunId: this.runId,
|
|
921
940
|
testId: testId,
|
|
@@ -923,7 +942,12 @@ class TestLensReporter {
|
|
|
923
942
|
fileType: await this.getContentType(fileName),
|
|
924
943
|
fileSize: fileSize,
|
|
925
944
|
artifactType: this.getArtifactType(fileName)
|
|
926
|
-
}
|
|
945
|
+
};
|
|
946
|
+
// Include DB ID if available for faster lookup (avoids query)
|
|
947
|
+
if (testCaseDbId) {
|
|
948
|
+
requestBody.testCaseDbId = testCaseDbId;
|
|
949
|
+
}
|
|
950
|
+
const presignedResponse = await this.axiosInstance.post(presignedUrlEndpoint, requestBody, {
|
|
927
951
|
timeout: 10000 // Quick timeout for metadata request
|
|
928
952
|
});
|
|
929
953
|
if (!presignedResponse.data.success) {
|
|
@@ -931,7 +955,7 @@ class TestLensReporter {
|
|
|
931
955
|
}
|
|
932
956
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
933
957
|
// Step 2: Upload directly to S3 using presigned URL
|
|
934
|
-
console.log(`⬆️ Uploading ${fileName} directly to S3
|
|
958
|
+
console.log(`⬆️ [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
935
959
|
const fileBuffer = fs.readFileSync(filePath);
|
|
936
960
|
// IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
|
|
937
961
|
// The backend signs with ServerSideEncryption:'AES256', so we must send that header
|
|
@@ -949,10 +973,10 @@ class TestLensReporter {
|
|
|
949
973
|
if (uploadResponse.status !== 200) {
|
|
950
974
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
951
975
|
}
|
|
952
|
-
console.log(`✅ S3
|
|
976
|
+
console.log(`✅ [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
953
977
|
// Step 3: Confirm upload with server to save metadata
|
|
954
978
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
955
|
-
const
|
|
979
|
+
const confirmBody = {
|
|
956
980
|
apiKey: this.config.apiKey,
|
|
957
981
|
testRunId: this.runId,
|
|
958
982
|
testId: testId,
|
|
@@ -961,12 +985,17 @@ class TestLensReporter {
|
|
|
961
985
|
fileType: await this.getContentType(fileName),
|
|
962
986
|
fileSize: fileSize,
|
|
963
987
|
artifactType: this.getArtifactType(fileName)
|
|
964
|
-
}
|
|
988
|
+
};
|
|
989
|
+
// Include DB ID if available for direct insert (avoids query and race condition)
|
|
990
|
+
if (testCaseDbId) {
|
|
991
|
+
confirmBody.testCaseDbId = testCaseDbId;
|
|
992
|
+
}
|
|
993
|
+
const confirmResponse = await this.axiosInstance.post(confirmEndpoint, confirmBody, {
|
|
965
994
|
timeout: 10000
|
|
966
995
|
});
|
|
967
996
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
968
997
|
const artifact = confirmResponse.data.artifact;
|
|
969
|
-
console.log(`✅ Upload confirmed
|
|
998
|
+
console.log(`✅ [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
970
999
|
return {
|
|
971
1000
|
key: s3Key,
|
|
972
1001
|
url: artifact.s3Url,
|
package/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface TestLensReporterConfig {
|
|
|
47
47
|
/** Alternative SSL option - set to true to ignore SSL certificate errors */
|
|
48
48
|
ignoreSslErrors?: boolean;
|
|
49
49
|
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
50
|
-
customMetadata?: Record<string, string>;
|
|
50
|
+
customMetadata?: Record<string, string | string[]>;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export interface TestLensReporterOptions {
|
|
@@ -78,7 +78,7 @@ export interface TestLensReporterOptions {
|
|
|
78
78
|
/** Alternative SSL option - set to true to ignore SSL certificate errors */
|
|
79
79
|
ignoreSslErrors?: boolean;
|
|
80
80
|
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
81
|
-
customMetadata?: Record<string, string>;
|
|
81
|
+
customMetadata?: Record<string, string | string[]>;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export interface GitInfo {
|
|
@@ -124,7 +124,7 @@ export interface RunMetadata {
|
|
|
124
124
|
skippedTests?: number;
|
|
125
125
|
status?: string;
|
|
126
126
|
testlensBuildName?: string;
|
|
127
|
-
customMetadata?: Record<string, string>;
|
|
127
|
+
customMetadata?: Record<string, string | string[]>;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
export interface TestError {
|
|
@@ -183,18 +183,20 @@ export class TestLensReporter implements Reporter {
|
|
|
183
183
|
private specMap: Map<string, SpecData>;
|
|
184
184
|
private testMap: Map<string, TestData>;
|
|
185
185
|
private runCreationFailed: boolean = false; // Track if run creation failed due to limits
|
|
186
|
+
private cliArgs: Record<string, any> = {}; // Store CLI args separately
|
|
186
187
|
|
|
187
188
|
/**
|
|
188
189
|
* Parse custom metadata from environment variables
|
|
189
190
|
* Checks for common metadata environment variables
|
|
190
191
|
*/
|
|
191
|
-
private static parseCustomArgs(): Record<string,
|
|
192
|
-
const customArgs: Record<string,
|
|
192
|
+
private static parseCustomArgs(): Record<string, any> {
|
|
193
|
+
const customArgs: Record<string, any> = {};
|
|
193
194
|
|
|
194
195
|
// Common environment variable names for build metadata
|
|
195
196
|
const envVarMappings: Record<string, string[]> = {
|
|
196
|
-
|
|
197
|
-
'
|
|
197
|
+
// Support both TestLens-specific names (recommended) and common CI names
|
|
198
|
+
'testlensBuildTag': ['testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
|
|
199
|
+
'testlensBuildName': ['testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
|
|
198
200
|
'environment': ['ENVIRONMENT', 'ENV', 'NODE_ENV', 'DEPLOYMENT_ENV'],
|
|
199
201
|
'branch': ['BRANCH', 'GIT_BRANCH', 'CI_COMMIT_BRANCH', 'GITHUB_REF_NAME'],
|
|
200
202
|
'team': ['TEAM', 'TEAM_NAME'],
|
|
@@ -207,8 +209,14 @@ export class TestLensReporter implements Reporter {
|
|
|
207
209
|
for (const envVar of envVars) {
|
|
208
210
|
const value = process.env[envVar];
|
|
209
211
|
if (value) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
// For testlensBuildTag, support comma-separated values
|
|
213
|
+
if (key === 'testlensBuildTag' && value.includes(',')) {
|
|
214
|
+
customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
|
215
|
+
console.log(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
|
|
216
|
+
} else {
|
|
217
|
+
customArgs[key] = value;
|
|
218
|
+
console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
|
|
219
|
+
}
|
|
212
220
|
break; // Use first match
|
|
213
221
|
}
|
|
214
222
|
}
|
|
@@ -220,6 +228,7 @@ export class TestLensReporter implements Reporter {
|
|
|
220
228
|
constructor(options: TestLensReporterOptions) {
|
|
221
229
|
// Parse custom CLI arguments
|
|
222
230
|
const customArgs = TestLensReporter.parseCustomArgs();
|
|
231
|
+
this.cliArgs = customArgs; // Store CLI args separately for later use
|
|
223
232
|
|
|
224
233
|
// Allow API key from environment variable if not provided in config
|
|
225
234
|
// Check multiple environment variable names in priority order (uppercase and lowercase)
|
|
@@ -246,7 +255,7 @@ export class TestLensReporter implements Reporter {
|
|
|
246
255
|
flushInterval: options.flushInterval || 5000,
|
|
247
256
|
retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
|
|
248
257
|
timeout: options.timeout || 60000,
|
|
249
|
-
customMetadata: { ...
|
|
258
|
+
customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
|
|
250
259
|
} as Required<TestLensReporterConfig>;
|
|
251
260
|
|
|
252
261
|
if (!this.config.apiKey) {
|
|
@@ -347,7 +356,9 @@ export class TestLensReporter implements Reporter {
|
|
|
347
356
|
|
|
348
357
|
// Extract testlensBuildName as a dedicated field for dashboard display
|
|
349
358
|
if (this.config.customMetadata.testlensBuildName) {
|
|
350
|
-
|
|
359
|
+
const buildName = this.config.customMetadata.testlensBuildName;
|
|
360
|
+
// Handle both string and array (take first element if array)
|
|
361
|
+
metadata.testlensBuildName = Array.isArray(buildName) ? buildName[0] : buildName;
|
|
351
362
|
}
|
|
352
363
|
}
|
|
353
364
|
|
|
@@ -678,17 +689,18 @@ export class TestLensReporter implements Reporter {
|
|
|
678
689
|
|
|
679
690
|
if (isFinalAttempt) {
|
|
680
691
|
console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
681
|
-
// Send test end event to API
|
|
682
|
-
await this.sendToApi({
|
|
692
|
+
// Send test end event to API and get response
|
|
693
|
+
const testEndResponse = await this.sendToApi({
|
|
683
694
|
type: 'testEnd',
|
|
684
695
|
runId: this.runId,
|
|
685
696
|
timestamp: new Date().toISOString(),
|
|
686
697
|
test: testData
|
|
687
698
|
});
|
|
688
699
|
|
|
689
|
-
// Handle artifacts
|
|
700
|
+
// Handle artifacts (test case is now guaranteed to be in database)
|
|
690
701
|
if (this.config.enableArtifacts) {
|
|
691
|
-
|
|
702
|
+
// Pass test case DB ID if available for faster lookups
|
|
703
|
+
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
692
704
|
}
|
|
693
705
|
}
|
|
694
706
|
}
|
|
@@ -784,10 +796,10 @@ export class TestLensReporter implements Reporter {
|
|
|
784
796
|
console.log(`🎯 Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
|
|
785
797
|
}
|
|
786
798
|
|
|
787
|
-
private async sendToApi(payload: any): Promise<
|
|
799
|
+
private async sendToApi(payload: any): Promise<any> {
|
|
788
800
|
// Skip sending if run creation already failed
|
|
789
801
|
if (this.runCreationFailed && payload.type !== 'runStart') {
|
|
790
|
-
return;
|
|
802
|
+
return null;
|
|
791
803
|
}
|
|
792
804
|
|
|
793
805
|
try {
|
|
@@ -799,6 +811,8 @@ export class TestLensReporter implements Reporter {
|
|
|
799
811
|
if (this.config.enableRealTimeStream) {
|
|
800
812
|
console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
801
813
|
}
|
|
814
|
+
// Return response data for caller to use
|
|
815
|
+
return response.data;
|
|
802
816
|
} catch (error: any) {
|
|
803
817
|
const errorData = error?.response?.data;
|
|
804
818
|
const status = error?.response?.status;
|
|
@@ -879,7 +893,7 @@ export class TestLensReporter implements Reporter {
|
|
|
879
893
|
}
|
|
880
894
|
}
|
|
881
895
|
|
|
882
|
-
private async processArtifacts(testId: string, result: TestResult): Promise<void> {
|
|
896
|
+
private async processArtifacts(testId: string, result: TestResult, testCaseDbId?: string): Promise<void> {
|
|
883
897
|
// Skip artifact processing if run creation failed
|
|
884
898
|
if (this.runCreationFailed) {
|
|
885
899
|
return;
|
|
@@ -937,12 +951,12 @@ export class TestLensReporter implements Reporter {
|
|
|
937
951
|
}
|
|
938
952
|
}
|
|
939
953
|
|
|
940
|
-
// Upload to S3 first
|
|
941
|
-
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName);
|
|
954
|
+
// Upload to S3 first (pass DB ID if available for faster lookup)
|
|
955
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
|
|
942
956
|
|
|
943
957
|
// Skip if upload failed or file was too large
|
|
944
958
|
if (!s3Data) {
|
|
945
|
-
console.log(`⏭️ Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
959
|
+
console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
946
960
|
continue;
|
|
947
961
|
}
|
|
948
962
|
|
|
@@ -966,9 +980,9 @@ export class TestLensReporter implements Reporter {
|
|
|
966
980
|
artifact: artifactData
|
|
967
981
|
});
|
|
968
982
|
|
|
969
|
-
console.log(`📎 Processed artifact: ${fileName}
|
|
983
|
+
console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
970
984
|
} catch (error) {
|
|
971
|
-
console.error(`❌ Failed to process artifact ${attachment.name}:`, (error as Error).message);
|
|
985
|
+
console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
|
|
972
986
|
}
|
|
973
987
|
}
|
|
974
988
|
}
|
|
@@ -1152,9 +1166,13 @@ export class TestLensReporter implements Reporter {
|
|
|
1152
1166
|
tags.push(...tagMatches);
|
|
1153
1167
|
}
|
|
1154
1168
|
|
|
1155
|
-
// Add testlensBuildTag
|
|
1156
|
-
|
|
1157
|
-
|
|
1169
|
+
// Add testlensBuildTag: CLI args take precedence over config
|
|
1170
|
+
const buildTagSource = this.cliArgs.testlensBuildTag || this.config.customMetadata?.testlensBuildTag;
|
|
1171
|
+
if (buildTagSource) {
|
|
1172
|
+
const buildTags = Array.isArray(buildTagSource)
|
|
1173
|
+
? buildTagSource
|
|
1174
|
+
: [buildTagSource];
|
|
1175
|
+
buildTags.forEach(tag => tags.push(`@${tag}`));
|
|
1158
1176
|
}
|
|
1159
1177
|
|
|
1160
1178
|
// Remove duplicates and return
|
|
@@ -1169,7 +1187,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1169
1187
|
}
|
|
1170
1188
|
|
|
1171
1189
|
|
|
1172
|
-
private async uploadArtifactToS3(filePath: string, testId: string, fileName: string): Promise<{ key: string; url: string; presignedUrl: string; fileSize: number; contentType: string } | null> {
|
|
1190
|
+
private async uploadArtifactToS3(filePath: string, testId: string, fileName: string, testCaseDbId?: string): Promise<{ key: string; url: string; presignedUrl: string; fileSize: number; contentType: string } | null> {
|
|
1173
1191
|
try {
|
|
1174
1192
|
// Check file size first
|
|
1175
1193
|
const fileSize = this.getFileSize(filePath);
|
|
@@ -1181,7 +1199,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1181
1199
|
|
|
1182
1200
|
// Step 1: Request pre-signed URL from server
|
|
1183
1201
|
const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
|
|
1184
|
-
const
|
|
1202
|
+
const requestBody: any = {
|
|
1185
1203
|
apiKey: this.config.apiKey,
|
|
1186
1204
|
testRunId: this.runId,
|
|
1187
1205
|
testId: testId,
|
|
@@ -1189,7 +1207,14 @@ export class TestLensReporter implements Reporter {
|
|
|
1189
1207
|
fileType: await this.getContentType(fileName),
|
|
1190
1208
|
fileSize: fileSize,
|
|
1191
1209
|
artifactType: this.getArtifactType(fileName)
|
|
1192
|
-
}
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
// Include DB ID if available for faster lookup (avoids query)
|
|
1213
|
+
if (testCaseDbId) {
|
|
1214
|
+
requestBody.testCaseDbId = testCaseDbId;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const presignedResponse = await this.axiosInstance.post(presignedUrlEndpoint, requestBody, {
|
|
1193
1218
|
timeout: 10000 // Quick timeout for metadata request
|
|
1194
1219
|
});
|
|
1195
1220
|
|
|
@@ -1200,7 +1225,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1200
1225
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1201
1226
|
|
|
1202
1227
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1203
|
-
console.log(`⬆️ Uploading ${fileName} directly to S3
|
|
1228
|
+
console.log(`⬆️ [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1204
1229
|
|
|
1205
1230
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1206
1231
|
|
|
@@ -1222,11 +1247,11 @@ export class TestLensReporter implements Reporter {
|
|
|
1222
1247
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1223
1248
|
}
|
|
1224
1249
|
|
|
1225
|
-
console.log(`✅ S3
|
|
1250
|
+
console.log(`✅ [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1226
1251
|
|
|
1227
1252
|
// Step 3: Confirm upload with server to save metadata
|
|
1228
1253
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
1229
|
-
const
|
|
1254
|
+
const confirmBody: any = {
|
|
1230
1255
|
apiKey: this.config.apiKey,
|
|
1231
1256
|
testRunId: this.runId,
|
|
1232
1257
|
testId: testId,
|
|
@@ -1235,13 +1260,20 @@ export class TestLensReporter implements Reporter {
|
|
|
1235
1260
|
fileType: await this.getContentType(fileName),
|
|
1236
1261
|
fileSize: fileSize,
|
|
1237
1262
|
artifactType: this.getArtifactType(fileName)
|
|
1238
|
-
}
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// Include DB ID if available for direct insert (avoids query and race condition)
|
|
1266
|
+
if (testCaseDbId) {
|
|
1267
|
+
confirmBody.testCaseDbId = testCaseDbId;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const confirmResponse = await this.axiosInstance.post(confirmEndpoint, confirmBody, {
|
|
1239
1271
|
timeout: 10000
|
|
1240
1272
|
});
|
|
1241
1273
|
|
|
1242
1274
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1243
1275
|
const artifact = confirmResponse.data.artifact;
|
|
1244
|
-
console.log(`✅ Upload confirmed
|
|
1276
|
+
console.log(`✅ [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1245
1277
|
return {
|
|
1246
1278
|
key: s3Key,
|
|
1247
1279
|
url: artifact.s3Url,
|
package/package.json
CHANGED