@alternative-path/testlens-playwright-reporter 0.4.3 → 0.4.4

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.
Files changed (5) hide show
  1. package/README.md +3 -3
  2. package/index.d.ts +193 -192
  3. package/index.js +113 -65
  4. package/index.ts +118 -70
  5. package/package.json +73 -82
package/README.md CHANGED
@@ -12,7 +12,7 @@ A Playwright reporter for [TestLens](https://testlens.qa-path.com) - real-time t
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- npm install testlens-playwright-reporter
15
+ npm i @alternative-path/testlens-playwright-reporter
16
16
  ```
17
17
 
18
18
  ## Configuration
@@ -72,7 +72,7 @@ export default defineConfig({
72
72
  trace: 'on',
73
73
  },
74
74
  reporter: [
75
- ['testlens-playwright-reporter', {
75
+ ['@alternative-path/testlens-playwright-reporter', {
76
76
  apiKey: process.env.TESTLENS_API_KEY || 'your-api-key-here',
77
77
 
78
78
  // Optional: explicitly forward build metadata from env vars
@@ -96,7 +96,7 @@ module.exports = defineConfig({
96
96
  trace: 'on',
97
97
  },
98
98
  reporter: [
99
- ['testlens-playwright-reporter', {
99
+ ['@alternative-path/testlens-playwright-reporter', {
100
100
  apiKey: process.env.TESTLENS_API_KEY || 'your-api-key-here',
101
101
 
102
102
  // Optional: explicitly forward build metadata from env vars
package/index.d.ts CHANGED
@@ -1,192 +1,193 @@
1
- import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
2
- export interface TestLensReporterConfig {
3
- /** TestLens API endpoint URL */
4
- apiEndpoint?: string;
5
- /** API key for authentication - can be provided in config or via TESTLENS_API_KEY environment variable */
6
- apiKey?: string;
7
- /** Enable real-time streaming of test events */
8
- enableRealTimeStream?: boolean;
9
- /** Enable Git information collection */
10
- enableGitInfo?: boolean;
11
- /** Enable artifact processing */
12
- enableArtifacts?: boolean;
13
- /** Enable video capture - defaults to true */
14
- enableVideo?: boolean;
15
- /** Enable screenshot capture - defaults to true */
16
- enableScreenshot?: boolean;
17
- /** Batch size for API requests */
18
- batchSize?: number;
19
- /** Flush interval in milliseconds */
20
- flushInterval?: number;
21
- /** Number of retry attempts for failed API calls */
22
- retryAttempts?: number;
23
- /** Request timeout in milliseconds */
24
- timeout?: number;
25
- /** SSL certificate validation - set to false to disable SSL verification */
26
- rejectUnauthorized?: boolean;
27
- /** Alternative SSL option - set to true to ignore SSL certificate errors */
28
- ignoreSslErrors?: boolean;
29
- /** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
30
- customMetadata?: Record<string, string | string[]>;
31
- }
32
- export interface TestLensReporterOptions {
33
- /** TestLens API endpoint URL */
34
- apiEndpoint?: string;
35
- /** API key for authentication - can be provided in config or via TESTLENS_API_KEY environment variable */
36
- apiKey?: string;
37
- /** Enable real-time streaming of test events */
38
- enableRealTimeStream?: boolean;
39
- /** Enable Git information collection */
40
- enableGitInfo?: boolean;
41
- /** Enable artifact processing */
42
- enableArtifacts?: boolean;
43
- /** Enable video capture - defaults to true */
44
- enableVideo?: boolean;
45
- /** Enable screenshot capture - defaults to true */
46
- enableScreenshot?: boolean;
47
- /** Batch size for API requests */
48
- batchSize?: number;
49
- /** Flush interval in milliseconds */
50
- flushInterval?: number;
51
- /** Number of retry attempts for failed API calls */
52
- retryAttempts?: number;
53
- /** Request timeout in milliseconds */
54
- timeout?: number;
55
- /** SSL certificate validation - set to false to disable SSL verification */
56
- rejectUnauthorized?: boolean;
57
- /** Alternative SSL option - set to true to ignore SSL certificate errors */
58
- ignoreSslErrors?: boolean;
59
- /** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
60
- customMetadata?: Record<string, string | string[]>;
61
- }
62
- export interface GitInfo {
63
- branch: string;
64
- commit: string;
65
- shortCommit: string;
66
- author: string;
67
- message: string;
68
- timestamp: string;
69
- isDirty: boolean;
70
- remoteName: string;
71
- remoteUrl: string;
72
- }
73
- export interface CodeBlock {
74
- type: 'test' | 'describe';
75
- name: string;
76
- content: string;
77
- summary?: string;
78
- describe?: string;
79
- startLine?: number;
80
- endLine?: number;
81
- }
82
- export interface RunMetadata {
83
- id: string;
84
- startTime: string;
85
- endTime?: string;
86
- duration?: number;
87
- environment: string;
88
- browser: string;
89
- os: string;
90
- playwrightVersion: string;
91
- nodeVersion: string;
92
- gitInfo?: GitInfo | null;
93
- shardInfo?: {
94
- current: number;
95
- total: number;
96
- };
97
- totalTests?: number;
98
- passedTests?: number;
99
- failedTests?: number;
100
- skippedTests?: number;
101
- status?: string;
102
- testlensBuildName?: string;
103
- customMetadata?: Record<string, string | string[]>;
104
- }
105
- export interface TestError {
106
- message: string;
107
- stack?: string;
108
- location?: {
109
- file: string;
110
- line: number;
111
- column: number;
112
- };
113
- snippet?: string;
114
- expected?: string;
115
- actual?: string;
116
- diff?: string;
117
- matcherName?: string;
118
- timeout?: number;
119
- }
120
- export interface TestData {
121
- id: string;
122
- name: string;
123
- status: string;
124
- originalStatus?: string;
125
- duration: number;
126
- startTime: string;
127
- endTime: string;
128
- errorMessages: string[];
129
- errors?: TestError[];
130
- retryAttempts: number;
131
- currentRetry: number;
132
- annotations: Array<{
133
- type: string;
134
- description?: string;
135
- }>;
136
- projectName: string;
137
- workerIndex?: number;
138
- parallelIndex?: number;
139
- location?: {
140
- file: string;
141
- line: number;
142
- column: number;
143
- };
144
- }
145
- export interface SpecData {
146
- filePath: string;
147
- testSuiteName: string;
148
- tags?: string[];
149
- startTime: string;
150
- endTime?: string;
151
- status: string;
152
- }
153
- export declare class TestLensReporter implements Reporter {
154
- private config;
155
- private axiosInstance;
156
- private runId;
157
- private runMetadata;
158
- private specMap;
159
- private testMap;
160
- private runCreationFailed;
161
- private cliArgs;
162
- /**
163
- * Parse custom metadata from environment variables
164
- * Checks for common metadata environment variables
165
- */
166
- private static parseCustomArgs;
167
- constructor(options: TestLensReporterOptions);
168
- private initializeRunMetadata;
169
- private getPlaywrightVersion;
170
- private normalizeTestStatus;
171
- private normalizeRunStatus;
172
- onBegin(config: FullConfig, suite: Suite): Promise<void>;
173
- onTestBegin(test: TestCase, result: TestResult): Promise<void>;
174
- onTestEnd(test: TestCase, result: TestResult): Promise<void>;
175
- onEnd(result: {
176
- status: string;
177
- }): Promise<void>;
178
- private sendToApi;
179
- private processArtifacts;
180
- private sendSpecCodeBlocks;
181
- private extractTestBlocks;
182
- private collectGitInfo;
183
- private getArtifactType;
184
- private extractTags;
185
- private getTestId;
186
- private uploadArtifactToS3;
187
- private getContentType;
188
- private generateS3Key;
189
- private sanitizeForS3;
190
- private getFileSize;
191
- }
192
- export default TestLensReporter;
1
+ import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
2
+ export interface TestLensReporterConfig {
3
+ /** TestLens API endpoint URL */
4
+ apiEndpoint?: string;
5
+ /** API key for authentication - can be provided in config or via TESTLENS_API_KEY environment variable */
6
+ apiKey?: string;
7
+ /** Enable real-time streaming of test events */
8
+ enableRealTimeStream?: boolean;
9
+ /** Enable Git information collection */
10
+ enableGitInfo?: boolean;
11
+ /** Enable artifact processing */
12
+ enableArtifacts?: boolean;
13
+ /** Enable video capture - defaults to true */
14
+ enableVideo?: boolean;
15
+ /** Enable screenshot capture - defaults to true */
16
+ enableScreenshot?: boolean;
17
+ /** Batch size for API requests */
18
+ batchSize?: number;
19
+ /** Flush interval in milliseconds */
20
+ flushInterval?: number;
21
+ /** Number of retry attempts for failed API calls */
22
+ retryAttempts?: number;
23
+ /** Request timeout in milliseconds */
24
+ timeout?: number;
25
+ /** SSL certificate validation - set to false to disable SSL verification */
26
+ rejectUnauthorized?: boolean;
27
+ /** Alternative SSL option - set to true to ignore SSL certificate errors */
28
+ ignoreSslErrors?: boolean;
29
+ /** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
30
+ customMetadata?: Record<string, string | string[]>;
31
+ }
32
+ export interface TestLensReporterOptions {
33
+ /** TestLens API endpoint URL */
34
+ apiEndpoint?: string;
35
+ /** API key for authentication - can be provided in config or via TESTLENS_API_KEY environment variable */
36
+ apiKey?: string;
37
+ /** Enable real-time streaming of test events */
38
+ enableRealTimeStream?: boolean;
39
+ /** Enable Git information collection */
40
+ enableGitInfo?: boolean;
41
+ /** Enable artifact processing */
42
+ enableArtifacts?: boolean;
43
+ /** Enable video capture - defaults to true */
44
+ enableVideo?: boolean;
45
+ /** Enable screenshot capture - defaults to true */
46
+ enableScreenshot?: boolean;
47
+ /** Batch size for API requests */
48
+ batchSize?: number;
49
+ /** Flush interval in milliseconds */
50
+ flushInterval?: number;
51
+ /** Number of retry attempts for failed API calls */
52
+ retryAttempts?: number;
53
+ /** Request timeout in milliseconds */
54
+ timeout?: number;
55
+ /** SSL certificate validation - set to false to disable SSL verification */
56
+ rejectUnauthorized?: boolean;
57
+ /** Alternative SSL option - set to true to ignore SSL certificate errors */
58
+ ignoreSslErrors?: boolean;
59
+ /** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
60
+ customMetadata?: Record<string, string | string[]>;
61
+ }
62
+ export interface GitInfo {
63
+ branch: string;
64
+ commit: string;
65
+ shortCommit: string;
66
+ author: string;
67
+ message: string;
68
+ timestamp: string;
69
+ isDirty: boolean;
70
+ remoteName: string;
71
+ remoteUrl: string;
72
+ }
73
+ export interface CodeBlock {
74
+ type: 'test' | 'describe';
75
+ name: string;
76
+ content: string;
77
+ summary?: string;
78
+ describe?: string;
79
+ startLine?: number;
80
+ endLine?: number;
81
+ }
82
+ export interface RunMetadata {
83
+ id: string;
84
+ startTime: string;
85
+ endTime?: string;
86
+ duration?: number;
87
+ environment: string;
88
+ browser: string;
89
+ os: string;
90
+ playwrightVersion: string;
91
+ nodeVersion: string;
92
+ testlensVersion?: string;
93
+ gitInfo?: GitInfo | null;
94
+ shardInfo?: {
95
+ current: number;
96
+ total: number;
97
+ };
98
+ totalTests?: number;
99
+ passedTests?: number;
100
+ failedTests?: number;
101
+ skippedTests?: number;
102
+ status?: string;
103
+ testlensBuildName?: string;
104
+ customMetadata?: Record<string, string | string[]>;
105
+ }
106
+ export interface TestError {
107
+ message: string;
108
+ stack?: string;
109
+ location?: {
110
+ file: string;
111
+ line: number;
112
+ column: number;
113
+ };
114
+ snippet?: string;
115
+ expected?: string;
116
+ actual?: string;
117
+ diff?: string;
118
+ matcherName?: string;
119
+ timeout?: number;
120
+ }
121
+ export interface TestData {
122
+ id: string;
123
+ name: string;
124
+ status: string;
125
+ originalStatus?: string;
126
+ duration: number;
127
+ startTime: string;
128
+ endTime: string;
129
+ errorMessages: string[];
130
+ errors?: TestError[];
131
+ retryAttempts: number;
132
+ currentRetry: number;
133
+ annotations: Array<{
134
+ type: string;
135
+ description?: string;
136
+ }>;
137
+ projectName: string;
138
+ workerIndex?: number;
139
+ parallelIndex?: number;
140
+ location?: {
141
+ file: string;
142
+ line: number;
143
+ column: number;
144
+ };
145
+ }
146
+ export interface SpecData {
147
+ filePath: string;
148
+ testSuiteName: string;
149
+ tags?: string[];
150
+ startTime: string;
151
+ endTime?: string;
152
+ status: string;
153
+ }
154
+ export declare class TestLensReporter implements Reporter {
155
+ private config;
156
+ private axiosInstance;
157
+ private runId;
158
+ private runMetadata;
159
+ private specMap;
160
+ private testMap;
161
+ private runCreationFailed;
162
+ private cliArgs;
163
+ private pendingUploads;
164
+ /**
165
+ * Parse custom metadata from environment variables
166
+ * Checks for common metadata environment variables
167
+ */
168
+ private static parseCustomArgs;
169
+ constructor(options: TestLensReporterOptions);
170
+ private initializeRunMetadata;
171
+ private getPlaywrightVersion;
172
+ private getTestLensVersion;
173
+ private normalizeTestStatus;
174
+ private normalizeRunStatus;
175
+ onBegin(config: FullConfig, suite: Suite): Promise<void>;
176
+ onTestBegin(test: TestCase, result: TestResult): Promise<void>;
177
+ onTestEnd(test: TestCase, result: TestResult): Promise<void>;
178
+ onEnd(result: {
179
+ status: string;
180
+ }): Promise<void>;
181
+ private sendToApi;
182
+ private processArtifacts;
183
+ private sendSpecCodeBlocks;
184
+ private extractTestBlocks;
185
+ private collectGitInfo;
186
+ private getArtifactType;
187
+ private extractTags;
188
+ private getTestId;
189
+ private uploadArtifactToS3;
190
+ private getContentType;
191
+ private getFileSize;
192
+ }
193
+ export default TestLensReporter;
package/index.js CHANGED
@@ -60,6 +60,7 @@ class TestLensReporter {
60
60
  constructor(options) {
61
61
  this.runCreationFailed = false; // Track if run creation failed due to limits
62
62
  this.cliArgs = {}; // Store CLI args separately
63
+ this.pendingUploads = new Set(); // Track pending artifact uploads
63
64
  // Parse custom CLI arguments
64
65
  const customArgs = TestLensReporter.parseCustomArgs();
65
66
  this.cliArgs = customArgs; // Store CLI args separately for later use
@@ -166,7 +167,8 @@ class TestLensReporter {
166
167
  browser: 'multiple',
167
168
  os: `${os.type()} ${os.release()}`,
168
169
  playwrightVersion: this.getPlaywrightVersion(),
169
- nodeVersion: process.version
170
+ nodeVersion: process.version,
171
+ testlensVersion: this.getTestLensVersion()
170
172
  };
171
173
  // Add custom metadata if provided
172
174
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
@@ -189,6 +191,15 @@ class TestLensReporter {
189
191
  return 'unknown';
190
192
  }
191
193
  }
194
+ getTestLensVersion() {
195
+ try {
196
+ const testlensPackage = require('./package.json');
197
+ return testlensPackage.version;
198
+ }
199
+ catch (error) {
200
+ return 'unknown';
201
+ }
202
+ }
192
203
  normalizeTestStatus(status) {
193
204
  // Treat timeout as failed for consistency with analytics
194
205
  if (status === 'timedOut') {
@@ -465,23 +476,20 @@ class TestLensReporter {
465
476
  }
466
477
  return testError;
467
478
  });
468
- // Only send testEnd event after final retry attempt
469
- // If test passed or this is the last retry, send the event
470
- const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
471
- if (isFinalAttempt) {
472
- console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
473
- // Send test end event to API and get response
474
- const testEndResponse = await this.sendToApi({
475
- type: 'testEnd',
476
- runId: this.runId,
477
- timestamp: new Date().toISOString(),
478
- test: testData
479
- });
480
- // Handle artifacts (test case is now guaranteed to be in database)
481
- if (this.config.enableArtifacts) {
482
- // Pass test case DB ID if available for faster lookups
483
- await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
484
- }
479
+ // Send testEnd event for all tests, regardless of status
480
+ // This ensures tests that are interrupted or have unexpected statuses are properly recorded
481
+ console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
482
+ // Send test end event to API and get response
483
+ const testEndResponse = await this.sendToApi({
484
+ type: 'testEnd',
485
+ runId: this.runId,
486
+ timestamp: new Date().toISOString(),
487
+ test: testData
488
+ });
489
+ // Handle artifacts (test case is now guaranteed to be in database)
490
+ if (this.config.enableArtifacts) {
491
+ // Pass test case DB ID if available for faster lookups
492
+ await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
485
493
  }
486
494
  }
487
495
  // Update spec status
@@ -497,12 +505,33 @@ class TestLensReporter {
497
505
  specData.status = 'skipped';
498
506
  }
499
507
  // Check if all tests in spec are complete
508
+ // Only consider tests that were actually executed (have testData)
500
509
  const remainingTests = test.parent.tests.filter((t) => {
501
510
  const tId = this.getTestId(t);
502
511
  const tData = this.testMap.get(tId);
503
- return !tData || !tData.endTime;
512
+ // If testData exists but no endTime, it's still running
513
+ return tData && !tData.endTime;
504
514
  });
505
515
  if (remainingTests.length === 0) {
516
+ // Determine final spec status based on all executed tests
517
+ const executedTests = test.parent.tests
518
+ .map((t) => {
519
+ const tId = this.getTestId(t);
520
+ return this.testMap.get(tId);
521
+ })
522
+ .filter((tData) => !!tData);
523
+ if (executedTests.length > 0) {
524
+ const allTestStatuses = executedTests.map(tData => tData.status);
525
+ if (allTestStatuses.every(status => status === 'passed')) {
526
+ specData.status = 'passed';
527
+ }
528
+ else if (allTestStatuses.some(status => status === 'failed')) {
529
+ specData.status = 'failed';
530
+ }
531
+ else if (allTestStatuses.every(status => status === 'skipped')) {
532
+ specData.status = 'skipped';
533
+ }
534
+ }
506
535
  // Aggregate tags from all tests in this spec
507
536
  const allTags = new Set();
508
537
  test.parent.tests.forEach((t) => {
@@ -530,6 +559,17 @@ class TestLensReporter {
530
559
  async onEnd(result) {
531
560
  this.runMetadata.endTime = new Date().toISOString();
532
561
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
562
+ // Wait for all pending artifact uploads to complete before sending runEnd
563
+ if (this.pendingUploads.size > 0) {
564
+ console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
565
+ try {
566
+ await Promise.all(Array.from(this.pendingUploads));
567
+ console.log(`✅ All artifact uploads completed`);
568
+ }
569
+ catch (error) {
570
+ console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
571
+ }
572
+ }
533
573
  // Calculate final stats
534
574
  const totalTests = Array.from(this.testMap.values()).length;
535
575
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -715,34 +755,53 @@ class TestLensReporter {
715
755
  }
716
756
  }
717
757
  // Upload to S3 first (pass DB ID if available for faster lookup)
718
- const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
719
- // Skip if upload failed or file was too large
720
- if (!s3Data) {
721
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
722
- continue;
723
- }
724
- const artifactData = {
725
- testId,
726
- type: this.getArtifactType(attachment.name),
727
- path: attachment.path,
728
- name: fileName,
729
- contentType: attachment.contentType,
730
- fileSize: this.getFileSize(attachment.path),
731
- storageType: 's3',
732
- s3Key: s3Data.key,
733
- s3Url: s3Data.url
734
- };
735
- // Send artifact data to API
736
- await this.sendToApi({
737
- type: 'artifact',
738
- runId: this.runId,
739
- timestamp: new Date().toISOString(),
740
- artifact: artifactData
758
+ // Create upload promise that we can track
759
+ const uploadPromise = Promise.resolve().then(async () => {
760
+ try {
761
+ if (!attachment.path) {
762
+ console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
763
+ return;
764
+ }
765
+ const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
766
+ // Skip if upload failed or file was too large
767
+ if (!s3Data) {
768
+ console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
769
+ return;
770
+ }
771
+ const artifactData = {
772
+ testId,
773
+ type: this.getArtifactType(attachment.name),
774
+ path: attachment.path,
775
+ name: fileName,
776
+ contentType: attachment.contentType,
777
+ fileSize: this.getFileSize(attachment.path),
778
+ storageType: 's3',
779
+ s3Key: s3Data.key,
780
+ s3Url: s3Data.url
781
+ };
782
+ // Send artifact data to API
783
+ await this.sendToApi({
784
+ type: 'artifact',
785
+ runId: this.runId,
786
+ timestamp: new Date().toISOString(),
787
+ artifact: artifactData
788
+ });
789
+ console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
790
+ }
791
+ catch (error) {
792
+ console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, error.message);
793
+ }
794
+ });
795
+ // Track this upload and ensure cleanup on completion
796
+ this.pendingUploads.add(uploadPromise);
797
+ uploadPromise.finally(() => {
798
+ this.pendingUploads.delete(uploadPromise);
741
799
  });
742
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
800
+ // Don't await here - let uploads happen in parallel
801
+ // They will be awaited in onEnd
743
802
  }
744
803
  catch (error) {
745
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, error.message);
804
+ console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, error.message);
746
805
  }
747
806
  }
748
807
  }
@@ -760,8 +819,13 @@ class TestLensReporter {
760
819
  describe: block.describe // parent describe block name
761
820
  }));
762
821
  // Send to dedicated spec code blocks API endpoint
763
- const baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
764
- const specEndpoint = `${baseUrl}/webhook/playwright/spec-code-blocks`;
822
+ // Extract base URL - handle both full and partial endpoint patterns
823
+ let baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
824
+ if (baseUrl === this.config.apiEndpoint) {
825
+ // Fallback: try alternative pattern if main pattern didn't match
826
+ baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
827
+ }
828
+ const specEndpoint = `${baseUrl}/api/v1/webhook/playwright/spec-code-blocks`;
765
829
  await this.axiosInstance.post(specEndpoint, {
766
830
  filePath: path.relative(process.cwd(), specPath),
767
831
  codeBlocks,
@@ -1014,7 +1078,7 @@ class TestLensReporter {
1014
1078
  const errorData = error?.response?.data;
1015
1079
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1016
1080
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1017
- console.error('\\n' + '='.repeat(80));
1081
+ console.error('\n' + '='.repeat(80));
1018
1082
  if (errorData?.error === 'test_cases_limit_reached') {
1019
1083
  console.error('❌ TESTLENS ERROR: Test Cases Limit Reached');
1020
1084
  }
@@ -1093,24 +1157,8 @@ class TestLensReporter {
1093
1157
  };
1094
1158
  return contentTypes[ext] || 'application/octet-stream';
1095
1159
  }
1096
- generateS3Key(runId, testId, fileName) {
1097
- const date = new Date().toISOString().slice(0, 10);
1098
- const safeTestId = this.sanitizeForS3(testId);
1099
- const safeFileName = this.sanitizeForS3(fileName);
1100
- const ext = path.extname(fileName);
1101
- const baseName = path.basename(fileName, ext);
1102
- return `test-artifacts/${date}/${runId}/${safeTestId}/${safeFileName}${ext}`;
1103
- }
1104
- sanitizeForS3(value) {
1105
- return value
1106
- .replace(/[\/:*?"<>|]/g, '-')
1107
- .replace(/[-\u001f\u007f]/g, '-')
1108
- .replace(/[^-~]/g, '-')
1109
- .replace(/\s+/g, '-')
1110
- .replace(/[_]/g, '-')
1111
- .replace(/-+/g, '-')
1112
- .replace(/^-|-$/g, '');
1113
- }
1160
+ // Note: S3 key generation and sanitization are handled server-side
1161
+ // generateS3Key() and sanitizeForS3() methods removed as they were not used
1114
1162
  getFileSize(filePath) {
1115
1163
  try {
1116
1164
  const stats = fs.statSync(filePath);
package/index.ts CHANGED
@@ -113,6 +113,7 @@ export interface RunMetadata {
113
113
  os: string;
114
114
  playwrightVersion: string;
115
115
  nodeVersion: string;
116
+ testlensVersion?: string;
116
117
  gitInfo?: GitInfo | null;
117
118
  shardInfo?: {
118
119
  current: number;
@@ -184,6 +185,7 @@ export class TestLensReporter implements Reporter {
184
185
  private testMap: Map<string, TestData>;
185
186
  private runCreationFailed: boolean = false; // Track if run creation failed due to limits
186
187
  private cliArgs: Record<string, any> = {}; // Store CLI args separately
188
+ private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
187
189
 
188
190
  /**
189
191
  * Parse custom metadata from environment variables
@@ -347,7 +349,8 @@ export class TestLensReporter implements Reporter {
347
349
  browser: 'multiple',
348
350
  os: `${os.type()} ${os.release()}`,
349
351
  playwrightVersion: this.getPlaywrightVersion(),
350
- nodeVersion: process.version
352
+ nodeVersion: process.version,
353
+ testlensVersion: this.getTestLensVersion()
351
354
  };
352
355
 
353
356
  // Add custom metadata if provided
@@ -374,6 +377,15 @@ export class TestLensReporter implements Reporter {
374
377
  }
375
378
  }
376
379
 
380
+ private getTestLensVersion(): string {
381
+ try {
382
+ const testlensPackage = require('./package.json');
383
+ return testlensPackage.version;
384
+ } catch (error) {
385
+ return 'unknown';
386
+ }
387
+ }
388
+
377
389
  private normalizeTestStatus(status: string): string {
378
390
  // Treat timeout as failed for consistency with analytics
379
391
  if (status === 'timedOut') {
@@ -683,25 +695,21 @@ export class TestLensReporter implements Reporter {
683
695
  return testError;
684
696
  });
685
697
 
686
- // Only send testEnd event after final retry attempt
687
- // If test passed or this is the last retry, send the event
688
- const isFinalAttempt = result.status === 'passed' || result.status === 'skipped' || result.retry >= test.retries;
689
-
690
- if (isFinalAttempt) {
691
- console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
692
- // Send test end event to API and get response
693
- const testEndResponse = await this.sendToApi({
694
- type: 'testEnd',
695
- runId: this.runId,
696
- timestamp: new Date().toISOString(),
697
- test: testData
698
- });
698
+ // Send testEnd event for all tests, regardless of status
699
+ // This ensures tests that are interrupted or have unexpected statuses are properly recorded
700
+ console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
701
+ // Send test end event to API and get response
702
+ const testEndResponse = await this.sendToApi({
703
+ type: 'testEnd',
704
+ runId: this.runId,
705
+ timestamp: new Date().toISOString(),
706
+ test: testData
707
+ });
699
708
 
700
- // Handle artifacts (test case is now guaranteed to be in database)
701
- if (this.config.enableArtifacts) {
702
- // Pass test case DB ID if available for faster lookups
703
- await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
704
- }
709
+ // Handle artifacts (test case is now guaranteed to be in database)
710
+ if (this.config.enableArtifacts) {
711
+ // Pass test case DB ID if available for faster lookups
712
+ await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
705
713
  }
706
714
  }
707
715
 
@@ -718,13 +726,33 @@ export class TestLensReporter implements Reporter {
718
726
  }
719
727
 
720
728
  // Check if all tests in spec are complete
729
+ // Only consider tests that were actually executed (have testData)
721
730
  const remainingTests = test.parent.tests.filter((t: any) => {
722
731
  const tId = this.getTestId(t);
723
732
  const tData = this.testMap.get(tId);
724
- return !tData || !tData.endTime;
733
+ // If testData exists but no endTime, it's still running
734
+ return tData && !tData.endTime;
725
735
  });
726
736
 
727
737
  if (remainingTests.length === 0) {
738
+ // Determine final spec status based on all executed tests
739
+ const executedTests = test.parent.tests
740
+ .map((t: any) => {
741
+ const tId = this.getTestId(t);
742
+ return this.testMap.get(tId);
743
+ })
744
+ .filter((tData: TestData | undefined): tData is TestData => !!tData);
745
+
746
+ if (executedTests.length > 0) {
747
+ const allTestStatuses = executedTests.map(tData => tData.status);
748
+ if (allTestStatuses.every(status => status === 'passed')) {
749
+ specData.status = 'passed';
750
+ } else if (allTestStatuses.some(status => status === 'failed')) {
751
+ specData.status = 'failed';
752
+ } else if (allTestStatuses.every(status => status === 'skipped')) {
753
+ specData.status = 'skipped';
754
+ }
755
+ }
728
756
  // Aggregate tags from all tests in this spec
729
757
  const allTags = new Set<string>();
730
758
  test.parent.tests.forEach((t: any) => {
@@ -757,6 +785,17 @@ export class TestLensReporter implements Reporter {
757
785
  this.runMetadata.endTime = new Date().toISOString();
758
786
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
759
787
 
788
+ // Wait for all pending artifact uploads to complete before sending runEnd
789
+ if (this.pendingUploads.size > 0) {
790
+ console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
791
+ try {
792
+ await Promise.all(Array.from(this.pendingUploads));
793
+ console.log(`✅ All artifact uploads completed`);
794
+ } catch (error) {
795
+ console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
796
+ }
797
+ }
798
+
760
799
  // Calculate final stats
761
800
  const totalTests = Array.from(this.testMap.values()).length;
762
801
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -952,37 +991,58 @@ export class TestLensReporter implements Reporter {
952
991
  }
953
992
 
954
993
  // Upload to S3 first (pass DB ID if available for faster lookup)
955
- const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
956
-
957
- // Skip if upload failed or file was too large
958
- if (!s3Data) {
959
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
960
- continue;
961
- }
962
-
963
- const artifactData = {
964
- testId,
965
- type: this.getArtifactType(attachment.name),
966
- path: attachment.path,
967
- name: fileName,
968
- contentType: attachment.contentType,
969
- fileSize: this.getFileSize(attachment.path),
970
- storageType: 's3',
971
- s3Key: s3Data.key,
972
- s3Url: s3Data.url
973
- };
994
+ // Create upload promise that we can track
995
+ const uploadPromise = Promise.resolve().then(async () => {
996
+ try {
997
+ if (!attachment.path) {
998
+ console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
999
+ return;
1000
+ }
1001
+
1002
+ const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
1003
+
1004
+ // Skip if upload failed or file was too large
1005
+ if (!s3Data) {
1006
+ console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1007
+ return;
1008
+ }
1009
+
1010
+ const artifactData = {
1011
+ testId,
1012
+ type: this.getArtifactType(attachment.name),
1013
+ path: attachment.path,
1014
+ name: fileName,
1015
+ contentType: attachment.contentType,
1016
+ fileSize: this.getFileSize(attachment.path),
1017
+ storageType: 's3',
1018
+ s3Key: s3Data.key,
1019
+ s3Url: s3Data.url
1020
+ };
974
1021
 
975
- // Send artifact data to API
976
- await this.sendToApi({
977
- type: 'artifact',
978
- runId: this.runId,
979
- timestamp: new Date().toISOString(),
980
- artifact: artifactData
1022
+ // Send artifact data to API
1023
+ await this.sendToApi({
1024
+ type: 'artifact',
1025
+ runId: this.runId,
1026
+ timestamp: new Date().toISOString(),
1027
+ artifact: artifactData
1028
+ });
1029
+
1030
+ console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1031
+ } catch (error) {
1032
+ console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
1033
+ }
1034
+ });
1035
+
1036
+ // Track this upload and ensure cleanup on completion
1037
+ this.pendingUploads.add(uploadPromise);
1038
+ uploadPromise.finally(() => {
1039
+ this.pendingUploads.delete(uploadPromise);
981
1040
  });
982
1041
 
983
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1042
+ // Don't await here - let uploads happen in parallel
1043
+ // They will be awaited in onEnd
984
1044
  } catch (error) {
985
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
1045
+ console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, (error as Error).message);
986
1046
  }
987
1047
  }
988
1048
  }
@@ -1003,8 +1063,13 @@ export class TestLensReporter implements Reporter {
1003
1063
  }));
1004
1064
 
1005
1065
  // Send to dedicated spec code blocks API endpoint
1006
- const baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
1007
- const specEndpoint = `${baseUrl}/webhook/playwright/spec-code-blocks`;
1066
+ // Extract base URL - handle both full and partial endpoint patterns
1067
+ let baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1068
+ if (baseUrl === this.config.apiEndpoint) {
1069
+ // Fallback: try alternative pattern if main pattern didn't match
1070
+ baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
1071
+ }
1072
+ const specEndpoint = `${baseUrl}/api/v1/webhook/playwright/spec-code-blocks`;
1008
1073
 
1009
1074
  await this.axiosInstance.post(specEndpoint, {
1010
1075
  filePath: path.relative(process.cwd(), specPath),
@@ -1291,7 +1356,7 @@ export class TestLensReporter implements Reporter {
1291
1356
 
1292
1357
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1293
1358
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1294
- console.error('\\n' + '='.repeat(80));
1359
+ console.error('\n' + '='.repeat(80));
1295
1360
 
1296
1361
  if (errorData?.error === 'test_cases_limit_reached') {
1297
1362
  console.error('❌ TESTLENS ERROR: Test Cases Limit Reached');
@@ -1372,26 +1437,8 @@ export class TestLensReporter implements Reporter {
1372
1437
  return contentTypes[ext] || 'application/octet-stream';
1373
1438
  }
1374
1439
 
1375
- private generateS3Key(runId: string, testId: string, fileName: string): string {
1376
- const date = new Date().toISOString().slice(0, 10);
1377
- const safeTestId = this.sanitizeForS3(testId);
1378
- const safeFileName = this.sanitizeForS3(fileName);
1379
- const ext = path.extname(fileName);
1380
- const baseName = path.basename(fileName, ext);
1381
-
1382
- return `test-artifacts/${date}/${runId}/${safeTestId}/${safeFileName}${ext}`;
1383
- }
1384
-
1385
- private sanitizeForS3(value: string): string {
1386
- return value
1387
- .replace(/[\/:*?"<>|]/g, '-')
1388
- .replace(/[-\u001f\u007f]/g, '-')
1389
- .replace(/[^-~]/g, '-')
1390
- .replace(/\s+/g, '-')
1391
- .replace(/[_]/g, '-')
1392
- .replace(/-+/g, '-')
1393
- .replace(/^-|-$/g, '');
1394
- }
1440
+ // Note: S3 key generation and sanitization are handled server-side
1441
+ // generateS3Key() and sanitizeForS3() methods removed as they were not used
1395
1442
 
1396
1443
  private getFileSize(filePath: string): number {
1397
1444
  try {
@@ -1405,3 +1452,4 @@ export class TestLensReporter implements Reporter {
1405
1452
  }
1406
1453
 
1407
1454
  export default TestLensReporter;
1455
+
package/package.json CHANGED
@@ -1,82 +1,73 @@
1
- {
2
- "name": "@alternative-path/testlens-playwright-reporter",
3
- "version": "0.4.3",
4
- "description": "Universal Playwright reporter for TestLens - works with both TypeScript and JavaScript projects",
5
- "main": "index.js",
6
- "types": "index.d.ts",
7
- "bin": {
8
- "testlens-cross-env": "cross-env.js"
9
- },
10
- "files": [
11
- "index.js",
12
- "index.d.ts",
13
- "index.ts",
14
- "lib/",
15
- "postinstall.js",
16
- "cross-env.js",
17
- "README.md",
18
- "CHANGELOG.md"
19
- ],
20
- "scripts": {
21
- "postinstall": "node postinstall.js",
22
- "prepack": "node build-embed-env.js",
23
- "prepublishOnly": "npm run lint && npm run test",
24
- "test": "echo 'Tests will be added in future versions'",
25
- "lint": "echo 'Linting passed - no linter configured yet'",
26
- "build": "tsc",
27
- "dev": "node index.js"
28
- },
29
- "keywords": [
30
- "playwright",
31
- "reporter",
32
- "testing",
33
- "testlens",
34
- "typescript",
35
- "javascript",
36
- "e2e",
37
- "automation",
38
- "ci-cd",
39
- "dashboard",
40
- "test-reporting"
41
- ],
42
- "author": {
43
- "name": "TestLens Team",
44
- "email": "support@alternative-path.com",
45
- "url": "https://testlens.qa-path.com"
46
- },
47
- "license": "MIT",
48
- "peerDependencies": {
49
- "@playwright/test": ">=1.40.0"
50
- },
51
- "dependencies": {
52
- "@aws-sdk/client-s3": "^3.624.0",
53
- "@aws-sdk/s3-request-presigner": "^3.624.0",
54
- "axios": "^1.11.0",
55
- "cross-env": "^7.0.3",
56
- "dotenv": "^16.4.5",
57
- "form-data": "^4.0.1",
58
- "mime": "^4.0.4",
59
- "tslib": "^2.8.1"
60
- },
61
- "engines": {
62
- "node": ">=16.0.0",
63
- "npm": ">=8.0.0"
64
- },
65
- "repository": {
66
- "type": "git",
67
- "url": "git+https://github.com/alternative-path/testlens-reporter.git",
68
- "directory": "."
69
- },
70
- "bugs": {
71
- "url": "https://github.com/alternative-path/testlens-reporter/issues"
72
- },
73
- "homepage": "https://github.com/alternative-path/testlens-reporter#readme",
74
- "publishConfig": {
75
- "access": "public",
76
- "registry": "https://registry.npmjs.org/"
77
- },
78
- "devDependencies": {
79
- "@types/node": "^24.3.1",
80
- "typescript": "^5.9.2"
81
- }
82
- }
1
+ {
2
+ "name": "@alternative-path/testlens-playwright-reporter",
3
+ "version": "0.4.4",
4
+ "description": "Universal Playwright reporter for TestLens - works with both TypeScript and JavaScript projects",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "bin": {
8
+ "testlens-cross-env": "cross-env.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "index.d.ts",
13
+ "index.ts",
14
+ "lib/",
15
+ "postinstall.js",
16
+ "cross-env.js",
17
+ "README.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "scripts": {
21
+ "postinstall": "node postinstall.js",
22
+ "prepack": "node build-embed-env.js",
23
+ "prepublishOnly": "npm run lint && npm run test",
24
+ "test": "echo 'Tests will be added in future versions'",
25
+ "lint": "echo 'Linting passed - no linter configured yet'",
26
+ "build": "tsc",
27
+ "dev": "node index.js"
28
+ },
29
+ "keywords": [
30
+ "playwright",
31
+ "reporter",
32
+ "testing",
33
+ "testlens",
34
+ "typescript",
35
+ "javascript",
36
+ "e2e",
37
+ "automation",
38
+ "ci-cd",
39
+ "dashboard",
40
+ "test-reporting"
41
+ ],
42
+ "author": {
43
+ "name": "TestLens Team",
44
+ "email": "support@alternative-path.com",
45
+ "url": "https://testlens.qa-path.com"
46
+ },
47
+ "license": "MIT",
48
+ "peerDependencies": {
49
+ "@playwright/test": ">=1.40.0"
50
+ },
51
+ "dependencies": {
52
+ "@aws-sdk/client-s3": "^3.624.0",
53
+ "@aws-sdk/s3-request-presigner": "^3.624.0",
54
+ "axios": "^1.11.0",
55
+ "cross-env": "^7.0.3",
56
+ "dotenv": "^16.4.5",
57
+ "form-data": "^4.0.1",
58
+ "mime": "^4.0.4",
59
+ "tslib": "^2.8.1"
60
+ },
61
+ "engines": {
62
+ "node": ">=16.0.0",
63
+ "npm": ">=8.0.0"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public",
67
+ "registry": "https://registry.npmjs.org/"
68
+ },
69
+ "devDependencies": {
70
+ "@types/node": "^24.3.1",
71
+ "typescript": "^5.9.2"
72
+ }
73
+ }