testlens-playwright-reporter 0.3.2 → 0.3.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.
- package/cross-env.js +24 -0
- package/index.d.ts +191 -120
- package/index.js +1097 -919
- package/index.ts +278 -30
- package/package.json +11 -3
- package/postinstall.js +39 -0
- package/lib/index.js +0 -908
package/index.ts
CHANGED
|
@@ -22,8 +22,8 @@ async function getMime() {
|
|
|
22
22
|
export interface TestLensReporterConfig {
|
|
23
23
|
/** TestLens API endpoint URL */
|
|
24
24
|
apiEndpoint?: string;
|
|
25
|
-
/** API key for authentication -
|
|
26
|
-
apiKey
|
|
25
|
+
/** API key for authentication - can be provided in config or via TESTLENS_API_KEY environment variable */
|
|
26
|
+
apiKey?: string;
|
|
27
27
|
/** Enable real-time streaming of test events */
|
|
28
28
|
enableRealTimeStream?: boolean;
|
|
29
29
|
/** Enable Git information collection */
|
|
@@ -46,13 +46,15 @@ export interface TestLensReporterConfig {
|
|
|
46
46
|
rejectUnauthorized?: boolean;
|
|
47
47
|
/** Alternative SSL option - set to true to ignore SSL certificate errors */
|
|
48
48
|
ignoreSslErrors?: boolean;
|
|
49
|
+
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
50
|
+
customMetadata?: Record<string, string>;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface TestLensReporterOptions {
|
|
52
54
|
/** TestLens API endpoint URL */
|
|
53
55
|
apiEndpoint?: string;
|
|
54
|
-
/** API key for authentication -
|
|
55
|
-
apiKey
|
|
56
|
+
/** API key for authentication - can be provided in config or via TESTLENS_API_KEY environment variable */
|
|
57
|
+
apiKey?: string;
|
|
56
58
|
/** Enable real-time streaming of test events */
|
|
57
59
|
enableRealTimeStream?: boolean;
|
|
58
60
|
/** Enable Git information collection */
|
|
@@ -75,6 +77,8 @@ export interface TestLensReporterOptions {
|
|
|
75
77
|
rejectUnauthorized?: boolean;
|
|
76
78
|
/** Alternative SSL option - set to true to ignore SSL certificate errors */
|
|
77
79
|
ignoreSslErrors?: boolean;
|
|
80
|
+
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
81
|
+
customMetadata?: Record<string, string>;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
export interface GitInfo {
|
|
@@ -119,6 +123,8 @@ export interface RunMetadata {
|
|
|
119
123
|
failedTests?: number;
|
|
120
124
|
skippedTests?: number;
|
|
121
125
|
status?: string;
|
|
126
|
+
testlensBuildName?: string;
|
|
127
|
+
customMetadata?: Record<string, string>;
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
export interface TestError {
|
|
@@ -163,7 +169,7 @@ export interface TestData {
|
|
|
163
169
|
export interface SpecData {
|
|
164
170
|
filePath: string;
|
|
165
171
|
testSuiteName: string;
|
|
166
|
-
tags
|
|
172
|
+
tags?: string[];
|
|
167
173
|
startTime: string;
|
|
168
174
|
endTime?: string;
|
|
169
175
|
status: string;
|
|
@@ -176,11 +182,61 @@ export class TestLensReporter implements Reporter {
|
|
|
176
182
|
private runMetadata: RunMetadata;
|
|
177
183
|
private specMap: Map<string, SpecData>;
|
|
178
184
|
private testMap: Map<string, TestData>;
|
|
185
|
+
private runCreationFailed: boolean = false; // Track if run creation failed due to limits
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Parse custom metadata from environment variables
|
|
189
|
+
* Checks for common metadata environment variables
|
|
190
|
+
*/
|
|
191
|
+
private static parseCustomArgs(): Record<string, string> {
|
|
192
|
+
const customArgs: Record<string, string> = {};
|
|
193
|
+
|
|
194
|
+
// Common environment variable names for build metadata
|
|
195
|
+
const envVarMappings: Record<string, string[]> = {
|
|
196
|
+
'testlensBuildTag': ['BUILDTAG', 'BUILD_TAG','TestlensBuildTag'],
|
|
197
|
+
'testlensBuildName': ['BUILDNAME', 'BUILD_NAME', 'TestlensBuildName'],
|
|
198
|
+
'environment': ['ENVIRONMENT', 'ENV', 'NODE_ENV', 'DEPLOYMENT_ENV'],
|
|
199
|
+
'branch': ['BRANCH', 'GIT_BRANCH', 'CI_COMMIT_BRANCH', 'GITHUB_REF_NAME'],
|
|
200
|
+
'team': ['TEAM', 'TEAM_NAME'],
|
|
201
|
+
'project': ['PROJECT', 'PROJECT_NAME'],
|
|
202
|
+
'customvalue': ['CUSTOMVALUE', 'CUSTOM_VALUE']
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Check for each metadata key
|
|
206
|
+
Object.entries(envVarMappings).forEach(([key, envVars]) => {
|
|
207
|
+
for (const envVar of envVars) {
|
|
208
|
+
const value = process.env[envVar];
|
|
209
|
+
if (value) {
|
|
210
|
+
customArgs[key] = value;
|
|
211
|
+
console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
|
|
212
|
+
break; // Use first match
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return customArgs;
|
|
218
|
+
}
|
|
179
219
|
|
|
180
220
|
constructor(options: TestLensReporterOptions) {
|
|
221
|
+
// Parse custom CLI arguments
|
|
222
|
+
const customArgs = TestLensReporter.parseCustomArgs();
|
|
223
|
+
|
|
224
|
+
// Allow API key from environment variable if not provided in config
|
|
225
|
+
// Check multiple environment variable names in priority order (uppercase and lowercase)
|
|
226
|
+
const apiKey = options.apiKey
|
|
227
|
+
|| process.env.TESTLENS_API_KEY
|
|
228
|
+
|| process.env.testlens_api_key
|
|
229
|
+
|| process.env.TESTLENS_KEY
|
|
230
|
+
|| process.env.testlens_key
|
|
231
|
+
|| process.env.testlensApiKey
|
|
232
|
+
|| process.env.PLAYWRIGHT_API_KEY
|
|
233
|
+
|| process.env.playwright_api_key
|
|
234
|
+
|| process.env.PW_API_KEY
|
|
235
|
+
|| process.env.pw_api_key;
|
|
236
|
+
|
|
181
237
|
this.config = {
|
|
182
238
|
apiEndpoint: options.apiEndpoint || 'https://testlens.qa-path.com/api/v1/webhook/playwright',
|
|
183
|
-
apiKey:
|
|
239
|
+
apiKey: apiKey, // API key from config or environment variable
|
|
184
240
|
enableRealTimeStream: options.enableRealTimeStream !== undefined ? options.enableRealTimeStream : true,
|
|
185
241
|
enableGitInfo: options.enableGitInfo !== undefined ? options.enableGitInfo : true,
|
|
186
242
|
enableArtifacts: options.enableArtifacts !== undefined ? options.enableArtifacts : true,
|
|
@@ -189,11 +245,16 @@ export class TestLensReporter implements Reporter {
|
|
|
189
245
|
batchSize: options.batchSize || 10,
|
|
190
246
|
flushInterval: options.flushInterval || 5000,
|
|
191
247
|
retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
|
|
192
|
-
timeout: options.timeout || 60000
|
|
248
|
+
timeout: options.timeout || 60000,
|
|
249
|
+
customMetadata: { ...customArgs, ...options.customMetadata } // CLI args + config metadata
|
|
193
250
|
} as Required<TestLensReporterConfig>;
|
|
194
251
|
|
|
195
252
|
if (!this.config.apiKey) {
|
|
196
|
-
throw new Error('API_KEY is required for TestLensReporter. Pass it as apiKey option in your playwright config.');
|
|
253
|
+
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.');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (apiKey !== options.apiKey) {
|
|
257
|
+
console.log('✓ Using API key from environment variable');
|
|
197
258
|
}
|
|
198
259
|
|
|
199
260
|
// Determine SSL validation behavior
|
|
@@ -257,10 +318,20 @@ export class TestLensReporter implements Reporter {
|
|
|
257
318
|
this.runMetadata = this.initializeRunMetadata();
|
|
258
319
|
this.specMap = new Map<string, SpecData>();
|
|
259
320
|
this.testMap = new Map<string, TestData>();
|
|
321
|
+
this.runCreationFailed = false;
|
|
322
|
+
|
|
323
|
+
// Log custom metadata if any
|
|
324
|
+
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
325
|
+
console.log('\n📋 Custom Metadata Detected:');
|
|
326
|
+
Object.entries(this.config.customMetadata).forEach(([key, value]) => {
|
|
327
|
+
console.log(` ${key}: ${value}`);
|
|
328
|
+
});
|
|
329
|
+
console.log('');
|
|
330
|
+
}
|
|
260
331
|
}
|
|
261
332
|
|
|
262
333
|
private initializeRunMetadata(): RunMetadata {
|
|
263
|
-
|
|
334
|
+
const metadata: RunMetadata = {
|
|
264
335
|
id: this.runId,
|
|
265
336
|
startTime: new Date().toISOString(),
|
|
266
337
|
environment: 'production',
|
|
@@ -269,6 +340,18 @@ export class TestLensReporter implements Reporter {
|
|
|
269
340
|
playwrightVersion: this.getPlaywrightVersion(),
|
|
270
341
|
nodeVersion: process.version
|
|
271
342
|
};
|
|
343
|
+
|
|
344
|
+
// Add custom metadata if provided
|
|
345
|
+
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
346
|
+
metadata.customMetadata = this.config.customMetadata;
|
|
347
|
+
|
|
348
|
+
// Extract testlensBuildName as a dedicated field for dashboard display
|
|
349
|
+
if (this.config.customMetadata.testlensBuildName) {
|
|
350
|
+
metadata.testlensBuildName = this.config.customMetadata.testlensBuildName;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return metadata;
|
|
272
355
|
}
|
|
273
356
|
|
|
274
357
|
private getPlaywrightVersion(): string {
|
|
@@ -301,7 +384,13 @@ export class TestLensReporter implements Reporter {
|
|
|
301
384
|
}
|
|
302
385
|
|
|
303
386
|
async onBegin(config: FullConfig, suite: Suite): Promise<void> {
|
|
304
|
-
|
|
387
|
+
// Show Build Name if provided, otherwise show Run ID
|
|
388
|
+
if (this.runMetadata.testlensBuildName) {
|
|
389
|
+
console.log(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
390
|
+
console.log(` Run ID: ${this.runId}`);
|
|
391
|
+
} else {
|
|
392
|
+
console.log(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
393
|
+
}
|
|
305
394
|
|
|
306
395
|
// Collect Git information if enabled
|
|
307
396
|
if (this.config.enableGitInfo) {
|
|
@@ -333,18 +422,24 @@ export class TestLensReporter implements Reporter {
|
|
|
333
422
|
}
|
|
334
423
|
|
|
335
424
|
async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
|
|
425
|
+
// Log which test is starting
|
|
426
|
+
console.log(`\n▶️ Running test: ${test.title}`);
|
|
427
|
+
|
|
336
428
|
const specPath = test.location.file;
|
|
337
429
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
338
430
|
|
|
339
431
|
// Create or update spec data
|
|
340
432
|
if (!this.specMap.has(specKey)) {
|
|
433
|
+
const extractedTags = this.extractTags(test);
|
|
341
434
|
const specData: SpecData = {
|
|
342
435
|
filePath: path.relative(process.cwd(), specPath),
|
|
343
436
|
testSuiteName: test.parent.title,
|
|
344
|
-
tags: this.extractTags(test),
|
|
345
437
|
startTime: new Date().toISOString(),
|
|
346
438
|
status: 'running'
|
|
347
439
|
};
|
|
440
|
+
if (extractedTags.length > 0) {
|
|
441
|
+
specData.tags = extractedTags;
|
|
442
|
+
}
|
|
348
443
|
this.specMap.set(specKey, specData);
|
|
349
444
|
|
|
350
445
|
// Send spec start event to API
|
|
@@ -419,13 +514,16 @@ export class TestLensReporter implements Reporter {
|
|
|
419
514
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
420
515
|
|
|
421
516
|
if (!this.specMap.has(specKey)) {
|
|
517
|
+
const extractedTags = this.extractTags(test);
|
|
422
518
|
const specData: SpecData = {
|
|
423
519
|
filePath: path.relative(process.cwd(), specPath),
|
|
424
520
|
testSuiteName: test.parent.title,
|
|
425
|
-
tags: this.extractTags(test),
|
|
426
521
|
startTime: new Date().toISOString(),
|
|
427
522
|
status: 'skipped'
|
|
428
523
|
};
|
|
524
|
+
if (extractedTags.length > 0) {
|
|
525
|
+
specData.tags = extractedTags;
|
|
526
|
+
}
|
|
429
527
|
this.specMap.set(specKey, specData);
|
|
430
528
|
|
|
431
529
|
// Send spec start event to API
|
|
@@ -615,6 +713,18 @@ export class TestLensReporter implements Reporter {
|
|
|
615
713
|
});
|
|
616
714
|
|
|
617
715
|
if (remainingTests.length === 0) {
|
|
716
|
+
// Aggregate tags from all tests in this spec
|
|
717
|
+
const allTags = new Set<string>();
|
|
718
|
+
test.parent.tests.forEach((t: any) => {
|
|
719
|
+
const tags = this.extractTags(t);
|
|
720
|
+
tags.forEach(tag => allTags.add(tag));
|
|
721
|
+
});
|
|
722
|
+
const aggregatedTags = Array.from(allTags);
|
|
723
|
+
// Only update tags if we have any
|
|
724
|
+
if (aggregatedTags.length > 0) {
|
|
725
|
+
specData.tags = aggregatedTags;
|
|
726
|
+
}
|
|
727
|
+
|
|
618
728
|
specData.endTime = new Date().toISOString();
|
|
619
729
|
|
|
620
730
|
// Send spec end event to API
|
|
@@ -664,13 +774,23 @@ export class TestLensReporter implements Reporter {
|
|
|
664
774
|
}
|
|
665
775
|
});
|
|
666
776
|
|
|
667
|
-
|
|
777
|
+
// Show Build Name if provided, otherwise show Run ID
|
|
778
|
+
if (this.runMetadata.testlensBuildName) {
|
|
779
|
+
console.log(`📊 TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
|
|
780
|
+
console.log(` Run ID: ${this.runId}`);
|
|
781
|
+
} else {
|
|
782
|
+
console.log(`📊 TestLens Report completed - Run ID: ${this.runId}`);
|
|
783
|
+
}
|
|
668
784
|
console.log(`🎯 Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
|
|
669
785
|
}
|
|
670
786
|
|
|
671
787
|
private async sendToApi(payload: any): Promise<void> {
|
|
788
|
+
// Skip sending if run creation already failed
|
|
789
|
+
if (this.runCreationFailed && payload.type !== 'runStart') {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
672
793
|
try {
|
|
673
|
-
console.log(`📤 Sending ${payload.type} event to ${this.config.apiEndpoint}`);
|
|
674
794
|
const response = await this.axiosInstance.post('', payload, {
|
|
675
795
|
headers: {
|
|
676
796
|
'X-API-Key': this.config.apiKey
|
|
@@ -680,21 +800,91 @@ export class TestLensReporter implements Reporter {
|
|
|
680
800
|
console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
681
801
|
}
|
|
682
802
|
} catch (error: any) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
803
|
+
const errorData = error?.response?.data;
|
|
804
|
+
const status = error?.response?.status;
|
|
805
|
+
|
|
806
|
+
// Check for limit exceeded (403)
|
|
807
|
+
if (status === 403 && errorData?.error === 'limit_exceeded') {
|
|
808
|
+
// Set flag to skip subsequent events
|
|
809
|
+
if (payload.type === 'runStart' && errorData?.limit_type === 'test_runs') {
|
|
810
|
+
this.runCreationFailed = true;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
console.error('\n' + '='.repeat(80));
|
|
814
|
+
if (errorData?.limit_type === 'test_cases') {
|
|
815
|
+
console.error('❌ TESTLENS ERROR: Test Cases Limit Reached');
|
|
816
|
+
} else if (errorData?.limit_type === 'test_runs') {
|
|
817
|
+
console.error('❌ TESTLENS ERROR: Test Runs Limit Reached');
|
|
818
|
+
} else {
|
|
819
|
+
console.error('❌ TESTLENS ERROR: Plan Limit Reached');
|
|
820
|
+
}
|
|
821
|
+
console.error('='.repeat(80));
|
|
822
|
+
console.error('');
|
|
823
|
+
console.error(errorData?.message || 'You have reached your plan limit.');
|
|
824
|
+
console.error('');
|
|
825
|
+
console.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
826
|
+
console.error('');
|
|
827
|
+
console.error('To continue, please upgrade your plan.');
|
|
828
|
+
console.error('Contact: support@alternative-path.com');
|
|
829
|
+
console.error('');
|
|
830
|
+
console.error('='.repeat(80));
|
|
831
|
+
console.error('');
|
|
832
|
+
return; // Don't log the full error object for limit errors
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Check for trial expiration, subscription errors, or limit errors (401)
|
|
836
|
+
if (status === 401) {
|
|
837
|
+
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
838
|
+
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
839
|
+
console.error('\n' + '='.repeat(80));
|
|
840
|
+
|
|
841
|
+
if (errorData?.error === 'test_cases_limit_reached') {
|
|
842
|
+
console.error('❌ TESTLENS ERROR: Test Cases Limit Reached');
|
|
843
|
+
} else if (errorData?.error === 'test_runs_limit_reached') {
|
|
844
|
+
console.error('❌ TESTLENS ERROR: Test Runs Limit Reached');
|
|
845
|
+
} else {
|
|
846
|
+
console.error('❌ TESTLENS ERROR: Your trial plan has ended');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
console.error('='.repeat(80));
|
|
850
|
+
console.error('');
|
|
851
|
+
console.error(errorData?.message || 'Your trial period has expired.');
|
|
852
|
+
console.error('');
|
|
853
|
+
console.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
854
|
+
console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
855
|
+
console.error('');
|
|
856
|
+
if (errorData?.trial_end_date) {
|
|
857
|
+
console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
858
|
+
console.error('');
|
|
859
|
+
}
|
|
860
|
+
console.error('='.repeat(80));
|
|
861
|
+
console.error('');
|
|
862
|
+
} else {
|
|
863
|
+
console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
864
|
+
}
|
|
865
|
+
} else if (status !== 403) {
|
|
866
|
+
// Log other errors (but not 403 which we handled above)
|
|
867
|
+
console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
|
|
868
|
+
message: error?.message || 'Unknown error',
|
|
869
|
+
status: status,
|
|
870
|
+
statusText: error?.response?.statusText,
|
|
871
|
+
data: errorData,
|
|
872
|
+
code: error?.code,
|
|
873
|
+
url: error?.config?.url,
|
|
874
|
+
method: error?.config?.method
|
|
875
|
+
});
|
|
876
|
+
}
|
|
692
877
|
|
|
693
878
|
// Don't throw error to avoid breaking test execution
|
|
694
879
|
}
|
|
695
880
|
}
|
|
696
881
|
|
|
697
882
|
private async processArtifacts(testId: string, result: TestResult): Promise<void> {
|
|
883
|
+
// Skip artifact processing if run creation failed
|
|
884
|
+
if (this.runCreationFailed) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
698
888
|
const attachments = result.attachments;
|
|
699
889
|
|
|
700
890
|
for (const attachment of attachments) {
|
|
@@ -810,7 +1000,15 @@ export class TestLensReporter implements Reporter {
|
|
|
810
1000
|
|
|
811
1001
|
console.log(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
812
1002
|
} catch (error: any) {
|
|
813
|
-
|
|
1003
|
+
const errorData = error?.response?.data;
|
|
1004
|
+
|
|
1005
|
+
// Handle duplicate spec code blocks gracefully (when re-running tests)
|
|
1006
|
+
if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
|
|
1007
|
+
console.log(`ℹ️ Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
|
|
814
1012
|
}
|
|
815
1013
|
}
|
|
816
1014
|
|
|
@@ -917,7 +1115,7 @@ export class TestLensReporter implements Reporter {
|
|
|
917
1115
|
remoteUrl
|
|
918
1116
|
};
|
|
919
1117
|
} catch (error: any) {
|
|
920
|
-
|
|
1118
|
+
// Silently skip git information if not in a git repository
|
|
921
1119
|
return null;
|
|
922
1120
|
}
|
|
923
1121
|
}
|
|
@@ -932,18 +1130,35 @@ export class TestLensReporter implements Reporter {
|
|
|
932
1130
|
private extractTags(test: TestCase): string[] {
|
|
933
1131
|
const tags: string[] = [];
|
|
934
1132
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1133
|
+
// Playwright stores tags in the _tags property
|
|
1134
|
+
const testTags = (test as any)._tags;
|
|
1135
|
+
if (testTags && Array.isArray(testTags)) {
|
|
1136
|
+
tags.push(...testTags);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Also get tags from parent suites by walking up the tree
|
|
1140
|
+
let currentSuite: Suite | undefined = test.parent;
|
|
1141
|
+
while (currentSuite) {
|
|
1142
|
+
const suiteTags = (currentSuite as any)._tags;
|
|
1143
|
+
if (suiteTags && Array.isArray(suiteTags)) {
|
|
1144
|
+
tags.push(...suiteTags);
|
|
938
1145
|
}
|
|
939
|
-
|
|
1146
|
+
currentSuite = currentSuite.parent;
|
|
1147
|
+
}
|
|
940
1148
|
|
|
1149
|
+
// Also extract @tags from test title for backward compatibility
|
|
941
1150
|
const tagMatches = test.title.match(/@[\w-]+/g);
|
|
942
1151
|
if (tagMatches) {
|
|
943
1152
|
tags.push(...tagMatches);
|
|
944
1153
|
}
|
|
945
1154
|
|
|
946
|
-
|
|
1155
|
+
// Add testlensBuildTag from custom metadata if present
|
|
1156
|
+
if (this.config.customMetadata?.testlensBuildTag) {
|
|
1157
|
+
tags.push(`@${this.config.customMetadata.testlensBuildTag}`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Remove duplicates and return
|
|
1161
|
+
return [...new Set(tags)];
|
|
947
1162
|
}
|
|
948
1163
|
|
|
949
1164
|
private getTestId(test: TestCase): string {
|
|
@@ -1038,6 +1253,39 @@ export class TestLensReporter implements Reporter {
|
|
|
1038
1253
|
throw new Error(`Upload confirmation failed: ${confirmResponse.data.error || 'Unknown error'}`);
|
|
1039
1254
|
}
|
|
1040
1255
|
} catch (error: any) {
|
|
1256
|
+
// Check for trial expiration, subscription errors, or limit errors
|
|
1257
|
+
if (error?.response?.status === 401) {
|
|
1258
|
+
const errorData = error?.response?.data;
|
|
1259
|
+
|
|
1260
|
+
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1261
|
+
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1262
|
+
console.error('\\n' + '='.repeat(80));
|
|
1263
|
+
|
|
1264
|
+
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1265
|
+
console.error('❌ TESTLENS ERROR: Test Cases Limit Reached');
|
|
1266
|
+
} else if (errorData?.error === 'test_runs_limit_reached') {
|
|
1267
|
+
console.error('❌ TESTLENS ERROR: Test Runs Limit Reached');
|
|
1268
|
+
} else {
|
|
1269
|
+
console.error('❌ TESTLENS ERROR: Your trial plan has ended');
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
console.error('='.repeat(80));
|
|
1273
|
+
console.error('');
|
|
1274
|
+
console.error(errorData?.message || 'Your trial period has expired.');
|
|
1275
|
+
console.error('');
|
|
1276
|
+
console.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1277
|
+
console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1278
|
+
console.error('');
|
|
1279
|
+
if (errorData?.trial_end_date) {
|
|
1280
|
+
console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1281
|
+
console.error('');
|
|
1282
|
+
}
|
|
1283
|
+
console.error('='.repeat(80));
|
|
1284
|
+
console.error('');
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1041
1289
|
// Better error messages for common issues
|
|
1042
1290
|
let errorMsg = error.message;
|
|
1043
1291
|
|
package/package.json
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testlens-playwright-reporter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|
|
7
|
+
"bin": {
|
|
8
|
+
"testlens-cross-env": "cross-env.js"
|
|
9
|
+
},
|
|
7
10
|
"files": [
|
|
8
11
|
"index.js",
|
|
9
12
|
"index.d.ts",
|
|
10
13
|
"index.ts",
|
|
11
14
|
"lib/",
|
|
15
|
+
"postinstall.js",
|
|
16
|
+
"cross-env.js",
|
|
12
17
|
"README.md",
|
|
13
18
|
"CHANGELOG.md"
|
|
14
19
|
],
|
|
15
20
|
"scripts": {
|
|
21
|
+
"postinstall": "node postinstall.js",
|
|
16
22
|
"prepack": "node build-embed-env.js",
|
|
17
23
|
"prepublishOnly": "npm run lint && npm run test",
|
|
18
24
|
"test": "echo 'Tests will be added in future versions'",
|
|
@@ -35,7 +41,7 @@
|
|
|
35
41
|
],
|
|
36
42
|
"author": {
|
|
37
43
|
"name": "TestLens Team",
|
|
38
|
-
"email": "support@
|
|
44
|
+
"email": "support@alternative-path.com",
|
|
39
45
|
"url": "https://testlens.qa-path.com"
|
|
40
46
|
},
|
|
41
47
|
"license": "MIT",
|
|
@@ -46,9 +52,11 @@
|
|
|
46
52
|
"@aws-sdk/client-s3": "^3.624.0",
|
|
47
53
|
"@aws-sdk/s3-request-presigner": "^3.624.0",
|
|
48
54
|
"axios": "^1.11.0",
|
|
55
|
+
"cross-env": "^7.0.3",
|
|
49
56
|
"dotenv": "^16.4.5",
|
|
50
57
|
"form-data": "^4.0.1",
|
|
51
|
-
"mime": "^4.0.4"
|
|
58
|
+
"mime": "^4.0.4",
|
|
59
|
+
"tslib": "^2.8.1"
|
|
52
60
|
},
|
|
53
61
|
"engines": {
|
|
54
62
|
"node": ">=16.0.0",
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
// Get the parent project's node_modules/.bin directory
|
|
6
|
+
// When installed, we are in: project/node_modules/testlens-playwright-reporter/
|
|
7
|
+
// We need to go to: project/node_modules/.bin/
|
|
8
|
+
const parentBinDir = path.resolve(__dirname, '..', '.bin');
|
|
9
|
+
const localBinDir = path.resolve(__dirname, 'node_modules', '.bin');
|
|
10
|
+
|
|
11
|
+
// Check if parent bin directory exists
|
|
12
|
+
if (!fs.existsSync(parentBinDir)) {
|
|
13
|
+
console.log('Parent .bin directory not found, skipping cross-env setup');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check if cross-env exists in our node_modules
|
|
18
|
+
const crossEnvCmd = process.platform === 'win32' ? 'cross-env.cmd' : 'cross-env';
|
|
19
|
+
const crossEnvPs1 = 'cross-env.ps1';
|
|
20
|
+
const sourceCmdPath = path.join(localBinDir, crossEnvCmd);
|
|
21
|
+
const sourcePs1Path = path.join(localBinDir, crossEnvPs1);
|
|
22
|
+
|
|
23
|
+
if (fs.existsSync(sourceCmdPath)) {
|
|
24
|
+
const targetCmdPath = path.join(parentBinDir, crossEnvCmd);
|
|
25
|
+
|
|
26
|
+
// Copy the file
|
|
27
|
+
fs.copyFileSync(sourceCmdPath, targetCmdPath);
|
|
28
|
+
console.log('✓ cross-env binary installed');
|
|
29
|
+
|
|
30
|
+
// Also copy PowerShell script on Windows
|
|
31
|
+
if (process.platform === 'win32' && fs.existsSync(sourcePs1Path)) {
|
|
32
|
+
const targetPs1Path = path.join(parentBinDir, crossEnvPs1);
|
|
33
|
+
fs.copyFileSync(sourcePs1Path, targetPs1Path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// Don't fail installation if this doesn't work
|
|
38
|
+
console.log('Note: Could not setup cross-env automatically:', error.message);
|
|
39
|
+
}
|