@alternative-path/testlens-playwright-reporter 0.4.4 → 0.4.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/index.d.ts +3 -0
- package/index.js +230 -102
- package/index.ts +236 -104
- package/package.json +2 -1
- package/postinstall.js +49 -39
package/index.ts
CHANGED
|
@@ -6,7 +6,71 @@ import * as https from 'https';
|
|
|
6
6
|
import axios, { AxiosInstance } from 'axios';
|
|
7
7
|
import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
|
-
import
|
|
9
|
+
import pino from 'pino';
|
|
10
|
+
|
|
11
|
+
const isVerboseLogs =
|
|
12
|
+
process.env.TESTLENS_LOG_VERBOSE === '1' ||
|
|
13
|
+
process.env.TESTLENS_LOG_VERBOSE === 'true';
|
|
14
|
+
const noisyInfoPrefixes = [
|
|
15
|
+
'[OK]',
|
|
16
|
+
'[UPLOAD]',
|
|
17
|
+
'[ARTIFACT]',
|
|
18
|
+
'[SKIP]',
|
|
19
|
+
'[TestLens] onTestEnd called',
|
|
20
|
+
'[TestLens] Sending testEnd'
|
|
21
|
+
];
|
|
22
|
+
const noisyInfoSubstrings = [
|
|
23
|
+
'Sent runStart event to TestLens',
|
|
24
|
+
'Sent specStart event to TestLens',
|
|
25
|
+
'Sent testStart event to TestLens',
|
|
26
|
+
'Sent testEnd event to TestLens',
|
|
27
|
+
'Sent runEnd event to TestLens',
|
|
28
|
+
'Sent artifact event to TestLens',
|
|
29
|
+
'S3 upload completed',
|
|
30
|
+
'Upload confirmed',
|
|
31
|
+
'Uploading ',
|
|
32
|
+
'Processed artifact',
|
|
33
|
+
'Spec code blocks',
|
|
34
|
+
'\n[METADATA]',
|
|
35
|
+
'Custom Metadata Detected',
|
|
36
|
+
'Using API key from environment variable'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const shouldSuppressInfo = (msg: string): boolean => {
|
|
40
|
+
if (isVerboseLogs) return false;
|
|
41
|
+
if (!msg) return false;
|
|
42
|
+
return (
|
|
43
|
+
noisyInfoPrefixes.some(prefix => msg.startsWith(prefix)) ||
|
|
44
|
+
noisyInfoSubstrings.some(sub => msg.includes(sub))
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Create pino logger instance
|
|
49
|
+
const logger = pino({
|
|
50
|
+
level: process.env.TESTLENS_LOG_LEVEL || process.env.LOG_LEVEL || 'info',
|
|
51
|
+
base: null, // remove pid/hostname
|
|
52
|
+
timestamp: false, // remove time
|
|
53
|
+
formatters: {
|
|
54
|
+
level: () => ({}) // remove level
|
|
55
|
+
},
|
|
56
|
+
hooks: {
|
|
57
|
+
logMethod(args, method, level) {
|
|
58
|
+
// pino level is numeric (info = 30)
|
|
59
|
+
if (level === 30) {
|
|
60
|
+
const msg =
|
|
61
|
+
typeof args[0] === 'string'
|
|
62
|
+
? args[0]
|
|
63
|
+
: typeof args[1] === 'string'
|
|
64
|
+
? args[1]
|
|
65
|
+
: '';
|
|
66
|
+
if (shouldSuppressInfo(msg)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return method.apply(this, args as any);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
10
74
|
|
|
11
75
|
// Lazy-load mime module to support ESM
|
|
12
76
|
let mimeModule: any = null;
|
|
@@ -186,6 +250,12 @@ export class TestLensReporter implements Reporter {
|
|
|
186
250
|
private runCreationFailed: boolean = false; // Track if run creation failed due to limits
|
|
187
251
|
private cliArgs: Record<string, any> = {}; // Store CLI args separately
|
|
188
252
|
private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
|
|
253
|
+
private artifactStats = {
|
|
254
|
+
uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
255
|
+
skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
256
|
+
failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
|
|
257
|
+
};
|
|
258
|
+
private artifactsSeen: number = 0;
|
|
189
259
|
|
|
190
260
|
/**
|
|
191
261
|
* Parse custom metadata from environment variables
|
|
@@ -214,10 +284,10 @@ export class TestLensReporter implements Reporter {
|
|
|
214
284
|
// For testlensBuildTag, support comma-separated values
|
|
215
285
|
if (key === 'testlensBuildTag' && value.includes(',')) {
|
|
216
286
|
customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
|
217
|
-
|
|
287
|
+
logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
|
|
218
288
|
} else {
|
|
219
289
|
customArgs[key] = value;
|
|
220
|
-
|
|
290
|
+
logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
|
|
221
291
|
}
|
|
222
292
|
break; // Use first match
|
|
223
293
|
}
|
|
@@ -257,6 +327,8 @@ export class TestLensReporter implements Reporter {
|
|
|
257
327
|
flushInterval: options.flushInterval || 5000,
|
|
258
328
|
retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
|
|
259
329
|
timeout: options.timeout || 60000,
|
|
330
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
331
|
+
ignoreSslErrors: options.ignoreSslErrors,
|
|
260
332
|
customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
|
|
261
333
|
} as Required<TestLensReporterConfig>;
|
|
262
334
|
|
|
@@ -265,27 +337,36 @@ export class TestLensReporter implements Reporter {
|
|
|
265
337
|
}
|
|
266
338
|
|
|
267
339
|
if (apiKey !== options.apiKey) {
|
|
268
|
-
|
|
340
|
+
logger.info('✓ Using API key from environment variable');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Default environment to allow self-signed certs unless explicitly set
|
|
344
|
+
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
|
|
345
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
269
346
|
}
|
|
270
347
|
|
|
271
348
|
// Determine SSL validation behavior
|
|
272
|
-
let rejectUnauthorized =
|
|
349
|
+
let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
|
|
273
350
|
|
|
274
|
-
// Check various ways SSL validation can be disabled (in order of precedence)
|
|
275
|
-
if (this.config.ignoreSslErrors) {
|
|
276
|
-
// Explicit configuration option
|
|
351
|
+
// Check various ways SSL validation can be disabled or enforced (in order of precedence)
|
|
352
|
+
if (this.config.ignoreSslErrors === true) {
|
|
277
353
|
rejectUnauthorized = false;
|
|
278
|
-
|
|
354
|
+
logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
|
|
279
355
|
} else if (this.config.rejectUnauthorized === false) {
|
|
280
|
-
// Explicit configuration option
|
|
281
356
|
rejectUnauthorized = false;
|
|
282
|
-
|
|
357
|
+
logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
|
|
358
|
+
} else if (this.config.rejectUnauthorized === true) {
|
|
359
|
+
rejectUnauthorized = true;
|
|
283
360
|
} else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
|
|
284
|
-
// Environment variable override
|
|
285
361
|
rejectUnauthorized = false;
|
|
286
|
-
|
|
362
|
+
logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
363
|
+
} else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
|
|
364
|
+
rejectUnauthorized = true;
|
|
287
365
|
}
|
|
288
366
|
|
|
367
|
+
// Mirror the resolved value so all HTTPS requests in this process follow it
|
|
368
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
|
|
369
|
+
|
|
289
370
|
// Set up axios instance with retry logic and enhanced SSL handling
|
|
290
371
|
this.axiosInstance = axios.create({
|
|
291
372
|
baseURL: this.config.apiEndpoint,
|
|
@@ -333,11 +414,11 @@ export class TestLensReporter implements Reporter {
|
|
|
333
414
|
|
|
334
415
|
// Log custom metadata if any
|
|
335
416
|
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
336
|
-
|
|
417
|
+
logger.info('\n[METADATA] Custom Metadata Detected:');
|
|
337
418
|
Object.entries(this.config.customMetadata).forEach(([key, value]) => {
|
|
338
|
-
|
|
419
|
+
logger.info(` ${key}: ${value}`);
|
|
339
420
|
});
|
|
340
|
-
|
|
421
|
+
logger.info('');
|
|
341
422
|
}
|
|
342
423
|
}
|
|
343
424
|
|
|
@@ -407,24 +488,25 @@ export class TestLensReporter implements Reporter {
|
|
|
407
488
|
}
|
|
408
489
|
|
|
409
490
|
async onBegin(config: FullConfig, suite: Suite): Promise<void> {
|
|
491
|
+
logger.info(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
|
|
410
492
|
// Show Build Name if provided, otherwise show Run ID
|
|
411
493
|
if (this.runMetadata.testlensBuildName) {
|
|
412
|
-
|
|
413
|
-
|
|
494
|
+
logger.info(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
495
|
+
logger.info(` Run ID: ${this.runId}`);
|
|
414
496
|
} else {
|
|
415
|
-
|
|
497
|
+
logger.info(`TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
416
498
|
}
|
|
417
499
|
|
|
418
500
|
// Collect Git information if enabled
|
|
419
501
|
if (this.config.enableGitInfo) {
|
|
420
502
|
this.runMetadata.gitInfo = await this.collectGitInfo();
|
|
421
503
|
if (this.runMetadata.gitInfo) {
|
|
422
|
-
|
|
504
|
+
logger.info(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
423
505
|
} else {
|
|
424
|
-
|
|
506
|
+
logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
425
507
|
}
|
|
426
508
|
} else {
|
|
427
|
-
|
|
509
|
+
logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
|
|
428
510
|
}
|
|
429
511
|
|
|
430
512
|
// Add shard information if available
|
|
@@ -446,7 +528,7 @@ export class TestLensReporter implements Reporter {
|
|
|
446
528
|
|
|
447
529
|
async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
|
|
448
530
|
// Log which test is starting
|
|
449
|
-
|
|
531
|
+
logger.info(`[TEST] Running test: ${test.title}`);
|
|
450
532
|
|
|
451
533
|
const specPath = test.location.file;
|
|
452
534
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -527,11 +609,25 @@ export class TestLensReporter implements Reporter {
|
|
|
527
609
|
const testId = this.getTestId(test);
|
|
528
610
|
let testData = this.testMap.get(testId);
|
|
529
611
|
|
|
530
|
-
|
|
612
|
+
logger.info(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
613
|
+
|
|
614
|
+
if (result.attachments && result.attachments.length > 0) {
|
|
615
|
+
for (const attachment of result.attachments) {
|
|
616
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
617
|
+
this.artifactsSeen += 1;
|
|
618
|
+
if (attachment.path) {
|
|
619
|
+
this.bumpArtifactStat('uploaded', artifactType);
|
|
620
|
+
} else {
|
|
621
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
|
|
531
627
|
|
|
532
628
|
// For skipped tests, onTestBegin might not be called, so we need to create the test data here
|
|
533
629
|
if (!testData) {
|
|
534
|
-
|
|
630
|
+
logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
|
|
535
631
|
// Create spec data if not exists (skipped tests might not have spec data either)
|
|
536
632
|
const specPath = test.location.file;
|
|
537
633
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -697,7 +793,7 @@ export class TestLensReporter implements Reporter {
|
|
|
697
793
|
|
|
698
794
|
// Send testEnd event for all tests, regardless of status
|
|
699
795
|
// This ensures tests that are interrupted or have unexpected statuses are properly recorded
|
|
700
|
-
|
|
796
|
+
logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
701
797
|
// Send test end event to API and get response
|
|
702
798
|
const testEndResponse = await this.sendToApi({
|
|
703
799
|
type: 'testEnd',
|
|
@@ -710,6 +806,12 @@ export class TestLensReporter implements Reporter {
|
|
|
710
806
|
if (this.config.enableArtifacts) {
|
|
711
807
|
// Pass test case DB ID if available for faster lookups
|
|
712
808
|
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
809
|
+
} else if (result.attachments && result.attachments.length > 0) {
|
|
810
|
+
for (const attachment of result.attachments) {
|
|
811
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
812
|
+
this.artifactsSeen += 1;
|
|
813
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
814
|
+
}
|
|
713
815
|
}
|
|
714
816
|
}
|
|
715
817
|
|
|
@@ -787,15 +889,30 @@ export class TestLensReporter implements Reporter {
|
|
|
787
889
|
|
|
788
890
|
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
789
891
|
if (this.pendingUploads.size > 0) {
|
|
790
|
-
|
|
892
|
+
logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
791
893
|
try {
|
|
792
894
|
await Promise.all(Array.from(this.pendingUploads));
|
|
793
|
-
|
|
895
|
+
logger.debug(`[OK] All artifact uploads completed`);
|
|
794
896
|
} catch (error) {
|
|
795
|
-
|
|
897
|
+
logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
796
898
|
}
|
|
797
899
|
}
|
|
798
900
|
|
|
901
|
+
const uploaded = this.artifactStats.uploaded;
|
|
902
|
+
const skipped = this.artifactStats.skipped;
|
|
903
|
+
const failed = this.artifactStats.failed;
|
|
904
|
+
// Always show summary so users can confirm artifact settings
|
|
905
|
+
const summary =
|
|
906
|
+
`[ARTIFACTS] seen=${this.artifactsSeen} | ` +
|
|
907
|
+
`uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
|
|
908
|
+
`skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
|
|
909
|
+
`failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
|
|
910
|
+
if (logger.levelVal > 30) {
|
|
911
|
+
console.log(summary);
|
|
912
|
+
} else {
|
|
913
|
+
logger.info(summary);
|
|
914
|
+
}
|
|
915
|
+
|
|
799
916
|
// Calculate final stats
|
|
800
917
|
const totalTests = Array.from(this.testMap.values()).length;
|
|
801
918
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
@@ -827,12 +944,12 @@ export class TestLensReporter implements Reporter {
|
|
|
827
944
|
|
|
828
945
|
// Show Build Name if provided, otherwise show Run ID
|
|
829
946
|
if (this.runMetadata.testlensBuildName) {
|
|
830
|
-
|
|
831
|
-
|
|
947
|
+
logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
|
|
948
|
+
logger.info(` Run ID: ${this.runId}`);
|
|
832
949
|
} else {
|
|
833
|
-
|
|
950
|
+
logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
|
|
834
951
|
}
|
|
835
|
-
|
|
952
|
+
logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
|
|
836
953
|
}
|
|
837
954
|
|
|
838
955
|
private async sendToApi(payload: any): Promise<any> {
|
|
@@ -848,7 +965,7 @@ export class TestLensReporter implements Reporter {
|
|
|
848
965
|
}
|
|
849
966
|
});
|
|
850
967
|
if (this.config.enableRealTimeStream) {
|
|
851
|
-
|
|
968
|
+
logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
852
969
|
}
|
|
853
970
|
// Return response data for caller to use
|
|
854
971
|
return response.data;
|
|
@@ -863,25 +980,25 @@ export class TestLensReporter implements Reporter {
|
|
|
863
980
|
this.runCreationFailed = true;
|
|
864
981
|
}
|
|
865
982
|
|
|
866
|
-
|
|
983
|
+
logger.error('\n' + '='.repeat(80));
|
|
867
984
|
if (errorData?.limit_type === 'test_cases') {
|
|
868
|
-
|
|
985
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
869
986
|
} else if (errorData?.limit_type === 'test_runs') {
|
|
870
|
-
|
|
987
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
871
988
|
} else {
|
|
872
|
-
|
|
989
|
+
logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
|
|
873
990
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
991
|
+
logger.error('='.repeat(80));
|
|
992
|
+
logger.error('');
|
|
993
|
+
logger.error(errorData?.message || 'You have reached your plan limit.');
|
|
994
|
+
logger.error('');
|
|
995
|
+
logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
996
|
+
logger.error('');
|
|
997
|
+
logger.error('To continue, please upgrade your plan.');
|
|
998
|
+
logger.error('Contact: support@alternative-path.com');
|
|
999
|
+
logger.error('');
|
|
1000
|
+
logger.error('='.repeat(80));
|
|
1001
|
+
logger.error('');
|
|
885
1002
|
return; // Don't log the full error object for limit errors
|
|
886
1003
|
}
|
|
887
1004
|
|
|
@@ -889,36 +1006,37 @@ export class TestLensReporter implements Reporter {
|
|
|
889
1006
|
if (status === 401) {
|
|
890
1007
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
891
1008
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
892
|
-
|
|
1009
|
+
logger.error('\n' + '='.repeat(80));
|
|
893
1010
|
|
|
894
1011
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
895
|
-
|
|
1012
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
896
1013
|
} else if (errorData?.error === 'test_runs_limit_reached') {
|
|
897
|
-
|
|
1014
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
898
1015
|
} else {
|
|
899
|
-
|
|
1016
|
+
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
900
1017
|
}
|
|
901
1018
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1019
|
+
logger.error('='.repeat(80));
|
|
1020
|
+
logger.error('');
|
|
1021
|
+
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1022
|
+
logger.error('');
|
|
1023
|
+
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1024
|
+
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1025
|
+
logger.error('');
|
|
909
1026
|
if (errorData?.trial_end_date) {
|
|
910
|
-
|
|
911
|
-
|
|
1027
|
+
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1028
|
+
logger.error('');
|
|
912
1029
|
}
|
|
913
|
-
|
|
914
|
-
|
|
1030
|
+
logger.error('='.repeat(80));
|
|
1031
|
+
logger.error('');
|
|
915
1032
|
} else {
|
|
916
|
-
|
|
1033
|
+
logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
917
1034
|
}
|
|
918
1035
|
} else if (status !== 403) {
|
|
919
1036
|
// Log other errors (but not 403 which we handled above)
|
|
920
|
-
|
|
921
|
-
message:
|
|
1037
|
+
logger.error({
|
|
1038
|
+
message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
|
|
1039
|
+
error: error?.message || 'Unknown error',
|
|
922
1040
|
status: status,
|
|
923
1041
|
statusText: error?.response?.statusText,
|
|
924
1042
|
data: errorData,
|
|
@@ -941,21 +1059,23 @@ export class TestLensReporter implements Reporter {
|
|
|
941
1059
|
const attachments = result.attachments;
|
|
942
1060
|
|
|
943
1061
|
for (const attachment of attachments) {
|
|
1062
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
944
1063
|
if (attachment.path) {
|
|
945
1064
|
// Check if attachment should be processed based on config
|
|
946
|
-
const artifactType = this.getArtifactType(attachment.name);
|
|
947
1065
|
const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
|
|
948
1066
|
const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
|
|
949
1067
|
|
|
950
1068
|
// Skip video if disabled in config
|
|
951
1069
|
if (isVideo && !this.config.enableVideo) {
|
|
952
|
-
|
|
1070
|
+
logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
1071
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
953
1072
|
continue;
|
|
954
1073
|
}
|
|
955
1074
|
|
|
956
1075
|
// Skip screenshot if disabled in config
|
|
957
1076
|
if (isScreenshot && !this.config.enableScreenshot) {
|
|
958
|
-
|
|
1077
|
+
logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
1078
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
959
1079
|
continue;
|
|
960
1080
|
}
|
|
961
1081
|
|
|
@@ -995,7 +1115,8 @@ export class TestLensReporter implements Reporter {
|
|
|
995
1115
|
const uploadPromise = Promise.resolve().then(async () => {
|
|
996
1116
|
try {
|
|
997
1117
|
if (!attachment.path) {
|
|
998
|
-
|
|
1118
|
+
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
1119
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
999
1120
|
return;
|
|
1000
1121
|
}
|
|
1001
1122
|
|
|
@@ -1003,7 +1124,8 @@ export class TestLensReporter implements Reporter {
|
|
|
1003
1124
|
|
|
1004
1125
|
// Skip if upload failed or file was too large
|
|
1005
1126
|
if (!s3Data) {
|
|
1006
|
-
|
|
1127
|
+
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
1128
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
1007
1129
|
return;
|
|
1008
1130
|
}
|
|
1009
1131
|
|
|
@@ -1027,9 +1149,10 @@ export class TestLensReporter implements Reporter {
|
|
|
1027
1149
|
artifact: artifactData
|
|
1028
1150
|
});
|
|
1029
1151
|
|
|
1030
|
-
|
|
1152
|
+
logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
1031
1153
|
} catch (error) {
|
|
1032
|
-
|
|
1154
|
+
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
|
|
1155
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
1033
1156
|
}
|
|
1034
1157
|
});
|
|
1035
1158
|
|
|
@@ -1042,8 +1165,11 @@ export class TestLensReporter implements Reporter {
|
|
|
1042
1165
|
// Don't await here - let uploads happen in parallel
|
|
1043
1166
|
// They will be awaited in onEnd
|
|
1044
1167
|
} catch (error) {
|
|
1045
|
-
|
|
1168
|
+
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
|
|
1169
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
1046
1170
|
}
|
|
1171
|
+
} else {
|
|
1172
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
1047
1173
|
}
|
|
1048
1174
|
}
|
|
1049
1175
|
}
|
|
@@ -1077,17 +1203,17 @@ export class TestLensReporter implements Reporter {
|
|
|
1077
1203
|
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
|
|
1078
1204
|
});
|
|
1079
1205
|
|
|
1080
|
-
|
|
1206
|
+
logger.info(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
1081
1207
|
} catch (error: any) {
|
|
1082
1208
|
const errorData = error?.response?.data;
|
|
1083
1209
|
|
|
1084
1210
|
// Handle duplicate spec code blocks gracefully (when re-running tests)
|
|
1085
1211
|
if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
|
|
1086
|
-
|
|
1212
|
+
logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
1087
1213
|
return;
|
|
1088
1214
|
}
|
|
1089
1215
|
|
|
1090
|
-
|
|
1216
|
+
logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
|
|
1091
1217
|
}
|
|
1092
1218
|
}
|
|
1093
1219
|
|
|
@@ -1153,7 +1279,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1153
1279
|
|
|
1154
1280
|
return blocks;
|
|
1155
1281
|
} catch (error: any) {
|
|
1156
|
-
|
|
1282
|
+
logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
|
|
1157
1283
|
return [];
|
|
1158
1284
|
}
|
|
1159
1285
|
}
|
|
@@ -1177,7 +1303,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1177
1303
|
}
|
|
1178
1304
|
} catch (e) {
|
|
1179
1305
|
// Remote info is optional - handle gracefully
|
|
1180
|
-
|
|
1306
|
+
logger.info('[INFO] No git remote configured, skipping remote info');
|
|
1181
1307
|
}
|
|
1182
1308
|
|
|
1183
1309
|
const isDirty = execSync('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
|
|
@@ -1206,6 +1332,13 @@ export class TestLensReporter implements Reporter {
|
|
|
1206
1332
|
return 'attachment';
|
|
1207
1333
|
}
|
|
1208
1334
|
|
|
1335
|
+
private bumpArtifactStat(stat: 'uploaded' | 'skipped' | 'failed', type: string): void {
|
|
1336
|
+
const bucket = this.artifactStats[stat] as Record<string, number>;
|
|
1337
|
+
if (bucket[type] !== undefined) {
|
|
1338
|
+
bucket[type] += 1;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1209
1342
|
private extractTags(test: TestCase): string[] {
|
|
1210
1343
|
const tags: string[] = [];
|
|
1211
1344
|
|
|
@@ -1258,7 +1391,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1258
1391
|
const fileSize = this.getFileSize(filePath);
|
|
1259
1392
|
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
1260
1393
|
|
|
1261
|
-
|
|
1394
|
+
logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
1262
1395
|
|
|
1263
1396
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
1264
1397
|
|
|
@@ -1290,7 +1423,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1290
1423
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1291
1424
|
|
|
1292
1425
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1293
|
-
|
|
1426
|
+
logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1294
1427
|
|
|
1295
1428
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1296
1429
|
|
|
@@ -1312,7 +1445,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1312
1445
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1313
1446
|
}
|
|
1314
1447
|
|
|
1315
|
-
|
|
1448
|
+
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1316
1449
|
|
|
1317
1450
|
// Step 3: Confirm upload with server to save metadata
|
|
1318
1451
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
@@ -1338,7 +1471,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1338
1471
|
|
|
1339
1472
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1340
1473
|
const artifact = confirmResponse.data.artifact;
|
|
1341
|
-
|
|
1474
|
+
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1342
1475
|
return {
|
|
1343
1476
|
key: s3Key,
|
|
1344
1477
|
url: artifact.s3Url,
|
|
@@ -1356,29 +1489,29 @@ export class TestLensReporter implements Reporter {
|
|
|
1356
1489
|
|
|
1357
1490
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1358
1491
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1359
|
-
|
|
1492
|
+
logger.error('\n' + '='.repeat(80));
|
|
1360
1493
|
|
|
1361
1494
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1362
|
-
|
|
1495
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1363
1496
|
} else if (errorData?.error === 'test_runs_limit_reached') {
|
|
1364
|
-
|
|
1497
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1365
1498
|
} else {
|
|
1366
|
-
|
|
1499
|
+
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1367
1500
|
}
|
|
1368
1501
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1502
|
+
logger.error('='.repeat(80));
|
|
1503
|
+
logger.error('');
|
|
1504
|
+
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1505
|
+
logger.error('');
|
|
1506
|
+
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1507
|
+
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1508
|
+
logger.error('');
|
|
1376
1509
|
if (errorData?.trial_end_date) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1510
|
+
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1511
|
+
logger.error('');
|
|
1379
1512
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1513
|
+
logger.error('='.repeat(80));
|
|
1514
|
+
logger.error('');
|
|
1382
1515
|
return null;
|
|
1383
1516
|
}
|
|
1384
1517
|
}
|
|
@@ -1396,9 +1529,9 @@ export class TestLensReporter implements Reporter {
|
|
|
1396
1529
|
errorMsg = `Access denied (403) - presigned URL may have expired`;
|
|
1397
1530
|
}
|
|
1398
1531
|
|
|
1399
|
-
|
|
1532
|
+
logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
|
|
1400
1533
|
if (error.response?.data) {
|
|
1401
|
-
|
|
1534
|
+
logger.error({ errorDetails: error.response.data }, 'Error details');
|
|
1402
1535
|
}
|
|
1403
1536
|
|
|
1404
1537
|
// Don't throw, just return null to continue with other artifacts
|
|
@@ -1417,7 +1550,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1417
1550
|
return mimeType || 'application/octet-stream';
|
|
1418
1551
|
}
|
|
1419
1552
|
} catch (error: any) {
|
|
1420
|
-
|
|
1553
|
+
logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
|
|
1421
1554
|
}
|
|
1422
1555
|
// Fallback to basic content type mapping
|
|
1423
1556
|
const contentTypes: Record<string, string> = {
|
|
@@ -1445,11 +1578,10 @@ export class TestLensReporter implements Reporter {
|
|
|
1445
1578
|
const stats = fs.statSync(filePath);
|
|
1446
1579
|
return stats.size;
|
|
1447
1580
|
} catch (error) {
|
|
1448
|
-
|
|
1581
|
+
logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
|
|
1449
1582
|
return 0;
|
|
1450
1583
|
}
|
|
1451
1584
|
}
|
|
1452
1585
|
}
|
|
1453
1586
|
|
|
1454
1587
|
export default TestLensReporter;
|
|
1455
|
-
|