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 CHANGED
@@ -17,46 +17,122 @@ npm install testlens-playwright-reporter
17
17
 
18
18
  ## Configuration
19
19
 
20
- ### TypeScript (`playwright.config.ts`)
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
- ### JavaScript (`playwright.config.js`)
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
- 'testlensBuildTag': ['BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag'],
32
- 'testlensBuildName': ['BUILDNAME', 'BUILD_NAME', 'TestlensBuildName'],
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
- customArgs[key] = value;
45
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
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: { ...customArgs, ...options.customMetadata } // CLI args + config metadata
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
- metadata.testlensBuildName = this.config.customMetadata.testlensBuildName;
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
- await this.processArtifacts(testId, result);
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} (uploaded to S3)`);
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 from custom metadata if present
897
- if (this.config.customMetadata?.testlensBuildTag) {
898
- tags.push(`@${this.config.customMetadata.testlensBuildTag}`);
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 presignedResponse = await this.axiosInstance.post(presignedUrlEndpoint, {
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 (bypass server)...`);
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 direct upload completed for ${fileName}`);
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 confirmResponse = await this.axiosInstance.post(confirmEndpoint, {
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 and saved to database`);
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, string> {
192
- const customArgs: Record<string, 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
- 'testlensBuildTag': ['BUILDTAG', 'BUILD_TAG','TestlensBuildTag'],
197
- 'testlensBuildName': ['BUILDNAME', 'BUILD_NAME', 'TestlensBuildName'],
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
- customArgs[key] = value;
211
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
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: { ...customArgs, ...options.customMetadata } // CLI args + config metadata
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
- metadata.testlensBuildName = this.config.customMetadata.testlensBuildName;
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
- await this.processArtifacts(testId, result);
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<void> {
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} (uploaded to S3)`);
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 from custom metadata if present
1156
- if (this.config.customMetadata?.testlensBuildTag) {
1157
- tags.push(`@${this.config.customMetadata.testlensBuildTag}`);
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 presignedResponse = await this.axiosInstance.post(presignedUrlEndpoint, {
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 (bypass server)...`);
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 direct upload completed for ${fileName}`);
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 confirmResponse = await this.axiosInstance.post(confirmEndpoint, {
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 and saved to database`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testlens-playwright-reporter",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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",