@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.js
CHANGED
|
@@ -9,6 +9,66 @@ const fs = tslib_1.__importStar(require("fs"));
|
|
|
9
9
|
const https = tslib_1.__importStar(require("https"));
|
|
10
10
|
const axios_1 = tslib_1.__importDefault(require("axios"));
|
|
11
11
|
const child_process_1 = require("child_process");
|
|
12
|
+
const pino_1 = tslib_1.__importDefault(require("pino"));
|
|
13
|
+
const isVerboseLogs = process.env.TESTLENS_LOG_VERBOSE === '1' ||
|
|
14
|
+
process.env.TESTLENS_LOG_VERBOSE === 'true';
|
|
15
|
+
const noisyInfoPrefixes = [
|
|
16
|
+
'[OK]',
|
|
17
|
+
'[UPLOAD]',
|
|
18
|
+
'[ARTIFACT]',
|
|
19
|
+
'[SKIP]',
|
|
20
|
+
'[TestLens] onTestEnd called',
|
|
21
|
+
'[TestLens] Sending testEnd'
|
|
22
|
+
];
|
|
23
|
+
const noisyInfoSubstrings = [
|
|
24
|
+
'Sent runStart event to TestLens',
|
|
25
|
+
'Sent specStart event to TestLens',
|
|
26
|
+
'Sent testStart event to TestLens',
|
|
27
|
+
'Sent testEnd event to TestLens',
|
|
28
|
+
'Sent runEnd event to TestLens',
|
|
29
|
+
'Sent artifact event to TestLens',
|
|
30
|
+
'S3 upload completed',
|
|
31
|
+
'Upload confirmed',
|
|
32
|
+
'Uploading ',
|
|
33
|
+
'Processed artifact',
|
|
34
|
+
'Spec code blocks',
|
|
35
|
+
'\n[METADATA]',
|
|
36
|
+
'Custom Metadata Detected',
|
|
37
|
+
'Using API key from environment variable'
|
|
38
|
+
];
|
|
39
|
+
const shouldSuppressInfo = (msg) => {
|
|
40
|
+
if (isVerboseLogs)
|
|
41
|
+
return false;
|
|
42
|
+
if (!msg)
|
|
43
|
+
return false;
|
|
44
|
+
return (noisyInfoPrefixes.some(prefix => msg.startsWith(prefix)) ||
|
|
45
|
+
noisyInfoSubstrings.some(sub => msg.includes(sub)));
|
|
46
|
+
};
|
|
47
|
+
// Create pino logger instance
|
|
48
|
+
const logger = (0, pino_1.default)({
|
|
49
|
+
level: process.env.TESTLENS_LOG_LEVEL || process.env.LOG_LEVEL || 'info',
|
|
50
|
+
base: null, // remove pid/hostname
|
|
51
|
+
timestamp: false, // remove time
|
|
52
|
+
formatters: {
|
|
53
|
+
level: () => ({}) // remove level
|
|
54
|
+
},
|
|
55
|
+
hooks: {
|
|
56
|
+
logMethod(args, method, level) {
|
|
57
|
+
// pino level is numeric (info = 30)
|
|
58
|
+
if (level === 30) {
|
|
59
|
+
const msg = typeof args[0] === 'string'
|
|
60
|
+
? args[0]
|
|
61
|
+
: typeof args[1] === 'string'
|
|
62
|
+
? args[1]
|
|
63
|
+
: '';
|
|
64
|
+
if (shouldSuppressInfo(msg)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return method.apply(this, args);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
12
72
|
// Lazy-load mime module to support ESM
|
|
13
73
|
let mimeModule = null;
|
|
14
74
|
async function getMime() {
|
|
@@ -45,11 +105,11 @@ class TestLensReporter {
|
|
|
45
105
|
// For testlensBuildTag, support comma-separated values
|
|
46
106
|
if (key === 'testlensBuildTag' && value.includes(',')) {
|
|
47
107
|
customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
|
48
|
-
|
|
108
|
+
logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
|
|
49
109
|
}
|
|
50
110
|
else {
|
|
51
111
|
customArgs[key] = value;
|
|
52
|
-
|
|
112
|
+
logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
|
|
53
113
|
}
|
|
54
114
|
break; // Use first match
|
|
55
115
|
}
|
|
@@ -61,6 +121,12 @@ class TestLensReporter {
|
|
|
61
121
|
this.runCreationFailed = false; // Track if run creation failed due to limits
|
|
62
122
|
this.cliArgs = {}; // Store CLI args separately
|
|
63
123
|
this.pendingUploads = new Set(); // Track pending artifact uploads
|
|
124
|
+
this.artifactStats = {
|
|
125
|
+
uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
126
|
+
skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
127
|
+
failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
|
|
128
|
+
};
|
|
129
|
+
this.artifactsSeen = 0;
|
|
64
130
|
// Parse custom CLI arguments
|
|
65
131
|
const customArgs = TestLensReporter.parseCustomArgs();
|
|
66
132
|
this.cliArgs = customArgs; // Store CLI args separately for later use
|
|
@@ -88,32 +154,43 @@ class TestLensReporter {
|
|
|
88
154
|
flushInterval: options.flushInterval || 5000,
|
|
89
155
|
retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
|
|
90
156
|
timeout: options.timeout || 60000,
|
|
157
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
158
|
+
ignoreSslErrors: options.ignoreSslErrors,
|
|
91
159
|
customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
|
|
92
160
|
};
|
|
93
161
|
if (!this.config.apiKey) {
|
|
94
162
|
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.');
|
|
95
163
|
}
|
|
96
164
|
if (apiKey !== options.apiKey) {
|
|
97
|
-
|
|
165
|
+
logger.info('✓ Using API key from environment variable');
|
|
166
|
+
}
|
|
167
|
+
// Default environment to allow self-signed certs unless explicitly set
|
|
168
|
+
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
|
|
169
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
98
170
|
}
|
|
99
171
|
// Determine SSL validation behavior
|
|
100
|
-
let rejectUnauthorized =
|
|
101
|
-
// Check various ways SSL validation can be disabled (in order of precedence)
|
|
102
|
-
if (this.config.ignoreSslErrors) {
|
|
103
|
-
// Explicit configuration option
|
|
172
|
+
let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
|
|
173
|
+
// Check various ways SSL validation can be disabled or enforced (in order of precedence)
|
|
174
|
+
if (this.config.ignoreSslErrors === true) {
|
|
104
175
|
rejectUnauthorized = false;
|
|
105
|
-
|
|
176
|
+
logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
|
|
106
177
|
}
|
|
107
178
|
else if (this.config.rejectUnauthorized === false) {
|
|
108
|
-
// Explicit configuration option
|
|
109
179
|
rejectUnauthorized = false;
|
|
110
|
-
|
|
180
|
+
logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
|
|
181
|
+
}
|
|
182
|
+
else if (this.config.rejectUnauthorized === true) {
|
|
183
|
+
rejectUnauthorized = true;
|
|
111
184
|
}
|
|
112
185
|
else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
|
|
113
|
-
// Environment variable override
|
|
114
186
|
rejectUnauthorized = false;
|
|
115
|
-
|
|
187
|
+
logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
188
|
+
}
|
|
189
|
+
else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
|
|
190
|
+
rejectUnauthorized = true;
|
|
116
191
|
}
|
|
192
|
+
// Mirror the resolved value so all HTTPS requests in this process follow it
|
|
193
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
|
|
117
194
|
// Set up axios instance with retry logic and enhanced SSL handling
|
|
118
195
|
this.axiosInstance = axios_1.default.create({
|
|
119
196
|
baseURL: this.config.apiEndpoint,
|
|
@@ -152,11 +229,11 @@ class TestLensReporter {
|
|
|
152
229
|
this.runCreationFailed = false;
|
|
153
230
|
// Log custom metadata if any
|
|
154
231
|
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
155
|
-
|
|
232
|
+
logger.info('\n[METADATA] Custom Metadata Detected:');
|
|
156
233
|
Object.entries(this.config.customMetadata).forEach(([key, value]) => {
|
|
157
|
-
|
|
234
|
+
logger.info(` ${key}: ${value}`);
|
|
158
235
|
});
|
|
159
|
-
|
|
236
|
+
logger.info('');
|
|
160
237
|
}
|
|
161
238
|
}
|
|
162
239
|
initializeRunMetadata() {
|
|
@@ -219,26 +296,27 @@ class TestLensReporter {
|
|
|
219
296
|
return status;
|
|
220
297
|
}
|
|
221
298
|
async onBegin(config, suite) {
|
|
299
|
+
logger.info(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
|
|
222
300
|
// Show Build Name if provided, otherwise show Run ID
|
|
223
301
|
if (this.runMetadata.testlensBuildName) {
|
|
224
|
-
|
|
225
|
-
|
|
302
|
+
logger.info(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
303
|
+
logger.info(` Run ID: ${this.runId}`);
|
|
226
304
|
}
|
|
227
305
|
else {
|
|
228
|
-
|
|
306
|
+
logger.info(`TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
229
307
|
}
|
|
230
308
|
// Collect Git information if enabled
|
|
231
309
|
if (this.config.enableGitInfo) {
|
|
232
310
|
this.runMetadata.gitInfo = await this.collectGitInfo();
|
|
233
311
|
if (this.runMetadata.gitInfo) {
|
|
234
|
-
|
|
312
|
+
logger.info(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
235
313
|
}
|
|
236
314
|
else {
|
|
237
|
-
|
|
315
|
+
logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
238
316
|
}
|
|
239
317
|
}
|
|
240
318
|
else {
|
|
241
|
-
|
|
319
|
+
logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
|
|
242
320
|
}
|
|
243
321
|
// Add shard information if available
|
|
244
322
|
if (config.shard) {
|
|
@@ -257,7 +335,7 @@ class TestLensReporter {
|
|
|
257
335
|
}
|
|
258
336
|
async onTestBegin(test, result) {
|
|
259
337
|
// Log which test is starting
|
|
260
|
-
|
|
338
|
+
logger.info(`[TEST] Running test: ${test.title}`);
|
|
261
339
|
const specPath = test.location.file;
|
|
262
340
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
263
341
|
// Create or update spec data
|
|
@@ -330,10 +408,23 @@ class TestLensReporter {
|
|
|
330
408
|
async onTestEnd(test, result) {
|
|
331
409
|
const testId = this.getTestId(test);
|
|
332
410
|
let testData = this.testMap.get(testId);
|
|
333
|
-
|
|
411
|
+
logger.info(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
412
|
+
if (result.attachments && result.attachments.length > 0) {
|
|
413
|
+
for (const attachment of result.attachments) {
|
|
414
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
415
|
+
this.artifactsSeen += 1;
|
|
416
|
+
if (attachment.path) {
|
|
417
|
+
this.bumpArtifactStat('uploaded', artifactType);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
|
|
334
425
|
// For skipped tests, onTestBegin might not be called, so we need to create the test data here
|
|
335
426
|
if (!testData) {
|
|
336
|
-
|
|
427
|
+
logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
|
|
337
428
|
// Create spec data if not exists (skipped tests might not have spec data either)
|
|
338
429
|
const specPath = test.location.file;
|
|
339
430
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -478,7 +569,7 @@ class TestLensReporter {
|
|
|
478
569
|
});
|
|
479
570
|
// Send testEnd event for all tests, regardless of status
|
|
480
571
|
// This ensures tests that are interrupted or have unexpected statuses are properly recorded
|
|
481
|
-
|
|
572
|
+
logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
482
573
|
// Send test end event to API and get response
|
|
483
574
|
const testEndResponse = await this.sendToApi({
|
|
484
575
|
type: 'testEnd',
|
|
@@ -491,6 +582,13 @@ class TestLensReporter {
|
|
|
491
582
|
// Pass test case DB ID if available for faster lookups
|
|
492
583
|
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
493
584
|
}
|
|
585
|
+
else if (result.attachments && result.attachments.length > 0) {
|
|
586
|
+
for (const attachment of result.attachments) {
|
|
587
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
588
|
+
this.artifactsSeen += 1;
|
|
589
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
494
592
|
}
|
|
495
593
|
// Update spec status
|
|
496
594
|
const specPath = test.location.file;
|
|
@@ -561,15 +659,29 @@ class TestLensReporter {
|
|
|
561
659
|
this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
|
|
562
660
|
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
563
661
|
if (this.pendingUploads.size > 0) {
|
|
564
|
-
|
|
662
|
+
logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
565
663
|
try {
|
|
566
664
|
await Promise.all(Array.from(this.pendingUploads));
|
|
567
|
-
|
|
665
|
+
logger.debug(`[OK] All artifact uploads completed`);
|
|
568
666
|
}
|
|
569
667
|
catch (error) {
|
|
570
|
-
|
|
668
|
+
logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
571
669
|
}
|
|
572
670
|
}
|
|
671
|
+
const uploaded = this.artifactStats.uploaded;
|
|
672
|
+
const skipped = this.artifactStats.skipped;
|
|
673
|
+
const failed = this.artifactStats.failed;
|
|
674
|
+
// Always show summary so users can confirm artifact settings
|
|
675
|
+
const summary = `[ARTIFACTS] seen=${this.artifactsSeen} | ` +
|
|
676
|
+
`uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
|
|
677
|
+
`skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
|
|
678
|
+
`failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
|
|
679
|
+
if (logger.levelVal > 30) {
|
|
680
|
+
console.log(summary);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
logger.info(summary);
|
|
684
|
+
}
|
|
573
685
|
// Calculate final stats
|
|
574
686
|
const totalTests = Array.from(this.testMap.values()).length;
|
|
575
687
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
@@ -598,13 +710,13 @@ class TestLensReporter {
|
|
|
598
710
|
});
|
|
599
711
|
// Show Build Name if provided, otherwise show Run ID
|
|
600
712
|
if (this.runMetadata.testlensBuildName) {
|
|
601
|
-
|
|
602
|
-
|
|
713
|
+
logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
|
|
714
|
+
logger.info(` Run ID: ${this.runId}`);
|
|
603
715
|
}
|
|
604
716
|
else {
|
|
605
|
-
|
|
717
|
+
logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
|
|
606
718
|
}
|
|
607
|
-
|
|
719
|
+
logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
|
|
608
720
|
}
|
|
609
721
|
async sendToApi(payload) {
|
|
610
722
|
// Skip sending if run creation already failed
|
|
@@ -618,7 +730,7 @@ class TestLensReporter {
|
|
|
618
730
|
}
|
|
619
731
|
});
|
|
620
732
|
if (this.config.enableRealTimeStream) {
|
|
621
|
-
|
|
733
|
+
logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
622
734
|
}
|
|
623
735
|
// Return response data for caller to use
|
|
624
736
|
return response.data;
|
|
@@ -632,65 +744,66 @@ class TestLensReporter {
|
|
|
632
744
|
if (payload.type === 'runStart' && errorData?.limit_type === 'test_runs') {
|
|
633
745
|
this.runCreationFailed = true;
|
|
634
746
|
}
|
|
635
|
-
|
|
747
|
+
logger.error('\n' + '='.repeat(80));
|
|
636
748
|
if (errorData?.limit_type === 'test_cases') {
|
|
637
|
-
|
|
749
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
638
750
|
}
|
|
639
751
|
else if (errorData?.limit_type === 'test_runs') {
|
|
640
|
-
|
|
752
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
641
753
|
}
|
|
642
754
|
else {
|
|
643
|
-
|
|
755
|
+
logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
|
|
644
756
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
757
|
+
logger.error('='.repeat(80));
|
|
758
|
+
logger.error('');
|
|
759
|
+
logger.error(errorData?.message || 'You have reached your plan limit.');
|
|
760
|
+
logger.error('');
|
|
761
|
+
logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
762
|
+
logger.error('');
|
|
763
|
+
logger.error('To continue, please upgrade your plan.');
|
|
764
|
+
logger.error('Contact: support@alternative-path.com');
|
|
765
|
+
logger.error('');
|
|
766
|
+
logger.error('='.repeat(80));
|
|
767
|
+
logger.error('');
|
|
656
768
|
return; // Don't log the full error object for limit errors
|
|
657
769
|
}
|
|
658
770
|
// Check for trial expiration, subscription errors, or limit errors (401)
|
|
659
771
|
if (status === 401) {
|
|
660
772
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
661
773
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
662
|
-
|
|
774
|
+
logger.error('\n' + '='.repeat(80));
|
|
663
775
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
664
|
-
|
|
776
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
665
777
|
}
|
|
666
778
|
else if (errorData?.error === 'test_runs_limit_reached') {
|
|
667
|
-
|
|
779
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
668
780
|
}
|
|
669
781
|
else {
|
|
670
|
-
|
|
782
|
+
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
671
783
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
784
|
+
logger.error('='.repeat(80));
|
|
785
|
+
logger.error('');
|
|
786
|
+
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
787
|
+
logger.error('');
|
|
788
|
+
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
789
|
+
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
790
|
+
logger.error('');
|
|
679
791
|
if (errorData?.trial_end_date) {
|
|
680
|
-
|
|
681
|
-
|
|
792
|
+
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
793
|
+
logger.error('');
|
|
682
794
|
}
|
|
683
|
-
|
|
684
|
-
|
|
795
|
+
logger.error('='.repeat(80));
|
|
796
|
+
logger.error('');
|
|
685
797
|
}
|
|
686
798
|
else {
|
|
687
|
-
|
|
799
|
+
logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
688
800
|
}
|
|
689
801
|
}
|
|
690
802
|
else if (status !== 403) {
|
|
691
803
|
// Log other errors (but not 403 which we handled above)
|
|
692
|
-
|
|
693
|
-
message:
|
|
804
|
+
logger.error({
|
|
805
|
+
message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
|
|
806
|
+
error: error?.message || 'Unknown error',
|
|
694
807
|
status: status,
|
|
695
808
|
statusText: error?.response?.statusText,
|
|
696
809
|
data: errorData,
|
|
@@ -709,19 +822,21 @@ class TestLensReporter {
|
|
|
709
822
|
}
|
|
710
823
|
const attachments = result.attachments;
|
|
711
824
|
for (const attachment of attachments) {
|
|
825
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
712
826
|
if (attachment.path) {
|
|
713
827
|
// Check if attachment should be processed based on config
|
|
714
|
-
const artifactType = this.getArtifactType(attachment.name);
|
|
715
828
|
const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
|
|
716
829
|
const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
|
|
717
830
|
// Skip video if disabled in config
|
|
718
831
|
if (isVideo && !this.config.enableVideo) {
|
|
719
|
-
|
|
832
|
+
logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
833
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
720
834
|
continue;
|
|
721
835
|
}
|
|
722
836
|
// Skip screenshot if disabled in config
|
|
723
837
|
if (isScreenshot && !this.config.enableScreenshot) {
|
|
724
|
-
|
|
838
|
+
logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
839
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
725
840
|
continue;
|
|
726
841
|
}
|
|
727
842
|
try {
|
|
@@ -759,13 +874,15 @@ class TestLensReporter {
|
|
|
759
874
|
const uploadPromise = Promise.resolve().then(async () => {
|
|
760
875
|
try {
|
|
761
876
|
if (!attachment.path) {
|
|
762
|
-
|
|
877
|
+
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
878
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
763
879
|
return;
|
|
764
880
|
}
|
|
765
881
|
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
|
|
766
882
|
// Skip if upload failed or file was too large
|
|
767
883
|
if (!s3Data) {
|
|
768
|
-
|
|
884
|
+
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
885
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
769
886
|
return;
|
|
770
887
|
}
|
|
771
888
|
const artifactData = {
|
|
@@ -786,10 +903,11 @@ class TestLensReporter {
|
|
|
786
903
|
timestamp: new Date().toISOString(),
|
|
787
904
|
artifact: artifactData
|
|
788
905
|
});
|
|
789
|
-
|
|
906
|
+
logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
790
907
|
}
|
|
791
908
|
catch (error) {
|
|
792
|
-
|
|
909
|
+
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
|
|
910
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
793
911
|
}
|
|
794
912
|
});
|
|
795
913
|
// Track this upload and ensure cleanup on completion
|
|
@@ -801,9 +919,13 @@ class TestLensReporter {
|
|
|
801
919
|
// They will be awaited in onEnd
|
|
802
920
|
}
|
|
803
921
|
catch (error) {
|
|
804
|
-
|
|
922
|
+
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
|
|
923
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
805
924
|
}
|
|
806
925
|
}
|
|
926
|
+
else {
|
|
927
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
928
|
+
}
|
|
807
929
|
}
|
|
808
930
|
}
|
|
809
931
|
async sendSpecCodeBlocks(specPath) {
|
|
@@ -831,16 +953,16 @@ class TestLensReporter {
|
|
|
831
953
|
codeBlocks,
|
|
832
954
|
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
|
|
833
955
|
});
|
|
834
|
-
|
|
956
|
+
logger.info(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
835
957
|
}
|
|
836
958
|
catch (error) {
|
|
837
959
|
const errorData = error?.response?.data;
|
|
838
960
|
// Handle duplicate spec code blocks gracefully (when re-running tests)
|
|
839
961
|
if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
|
|
840
|
-
|
|
962
|
+
logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
841
963
|
return;
|
|
842
964
|
}
|
|
843
|
-
|
|
965
|
+
logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
|
|
844
966
|
}
|
|
845
967
|
}
|
|
846
968
|
extractTestBlocks(filePath) {
|
|
@@ -899,7 +1021,7 @@ class TestLensReporter {
|
|
|
899
1021
|
return blocks;
|
|
900
1022
|
}
|
|
901
1023
|
catch (error) {
|
|
902
|
-
|
|
1024
|
+
logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
|
|
903
1025
|
return [];
|
|
904
1026
|
}
|
|
905
1027
|
}
|
|
@@ -922,7 +1044,7 @@ class TestLensReporter {
|
|
|
922
1044
|
}
|
|
923
1045
|
catch (e) {
|
|
924
1046
|
// Remote info is optional - handle gracefully
|
|
925
|
-
|
|
1047
|
+
logger.info('[INFO] No git remote configured, skipping remote info');
|
|
926
1048
|
}
|
|
927
1049
|
const isDirty = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
|
|
928
1050
|
return {
|
|
@@ -951,6 +1073,12 @@ class TestLensReporter {
|
|
|
951
1073
|
return 'trace';
|
|
952
1074
|
return 'attachment';
|
|
953
1075
|
}
|
|
1076
|
+
bumpArtifactStat(stat, type) {
|
|
1077
|
+
const bucket = this.artifactStats[stat];
|
|
1078
|
+
if (bucket[type] !== undefined) {
|
|
1079
|
+
bucket[type] += 1;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
954
1082
|
extractTags(test) {
|
|
955
1083
|
const tags = [];
|
|
956
1084
|
// Playwright stores tags in the _tags property
|
|
@@ -994,7 +1122,7 @@ class TestLensReporter {
|
|
|
994
1122
|
// Check file size first
|
|
995
1123
|
const fileSize = this.getFileSize(filePath);
|
|
996
1124
|
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
997
|
-
|
|
1125
|
+
logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
998
1126
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
999
1127
|
// Step 1: Request pre-signed URL from server
|
|
1000
1128
|
const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
|
|
@@ -1019,7 +1147,7 @@ class TestLensReporter {
|
|
|
1019
1147
|
}
|
|
1020
1148
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1021
1149
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1022
|
-
|
|
1150
|
+
logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1023
1151
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1024
1152
|
// IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
|
|
1025
1153
|
// The backend signs with ServerSideEncryption:'AES256', so we must send that header
|
|
@@ -1037,7 +1165,7 @@ class TestLensReporter {
|
|
|
1037
1165
|
if (uploadResponse.status !== 200) {
|
|
1038
1166
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1039
1167
|
}
|
|
1040
|
-
|
|
1168
|
+
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1041
1169
|
// Step 3: Confirm upload with server to save metadata
|
|
1042
1170
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
1043
1171
|
const confirmBody = {
|
|
@@ -1059,7 +1187,7 @@ class TestLensReporter {
|
|
|
1059
1187
|
});
|
|
1060
1188
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1061
1189
|
const artifact = confirmResponse.data.artifact;
|
|
1062
|
-
|
|
1190
|
+
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1063
1191
|
return {
|
|
1064
1192
|
key: s3Key,
|
|
1065
1193
|
url: artifact.s3Url,
|
|
@@ -1078,29 +1206,29 @@ class TestLensReporter {
|
|
|
1078
1206
|
const errorData = error?.response?.data;
|
|
1079
1207
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1080
1208
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1081
|
-
|
|
1209
|
+
logger.error('\n' + '='.repeat(80));
|
|
1082
1210
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1083
|
-
|
|
1211
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1084
1212
|
}
|
|
1085
1213
|
else if (errorData?.error === 'test_runs_limit_reached') {
|
|
1086
|
-
|
|
1214
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1087
1215
|
}
|
|
1088
1216
|
else {
|
|
1089
|
-
|
|
1217
|
+
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1090
1218
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1219
|
+
logger.error('='.repeat(80));
|
|
1220
|
+
logger.error('');
|
|
1221
|
+
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1222
|
+
logger.error('');
|
|
1223
|
+
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1224
|
+
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1225
|
+
logger.error('');
|
|
1098
1226
|
if (errorData?.trial_end_date) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1227
|
+
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1228
|
+
logger.error('');
|
|
1101
1229
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1230
|
+
logger.error('='.repeat(80));
|
|
1231
|
+
logger.error('');
|
|
1104
1232
|
return null;
|
|
1105
1233
|
}
|
|
1106
1234
|
}
|
|
@@ -1118,9 +1246,9 @@ class TestLensReporter {
|
|
|
1118
1246
|
else if (error.response?.status === 403) {
|
|
1119
1247
|
errorMsg = `Access denied (403) - presigned URL may have expired`;
|
|
1120
1248
|
}
|
|
1121
|
-
|
|
1249
|
+
logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
|
|
1122
1250
|
if (error.response?.data) {
|
|
1123
|
-
|
|
1251
|
+
logger.error({ errorDetails: error.response.data }, 'Error details');
|
|
1124
1252
|
}
|
|
1125
1253
|
// Don't throw, just return null to continue with other artifacts
|
|
1126
1254
|
return null;
|
|
@@ -1138,7 +1266,7 @@ class TestLensReporter {
|
|
|
1138
1266
|
}
|
|
1139
1267
|
}
|
|
1140
1268
|
catch (error) {
|
|
1141
|
-
|
|
1269
|
+
logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
|
|
1142
1270
|
}
|
|
1143
1271
|
// Fallback to basic content type mapping
|
|
1144
1272
|
const contentTypes = {
|
|
@@ -1165,7 +1293,7 @@ class TestLensReporter {
|
|
|
1165
1293
|
return stats.size;
|
|
1166
1294
|
}
|
|
1167
1295
|
catch (error) {
|
|
1168
|
-
|
|
1296
|
+
logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
|
|
1169
1297
|
return 0;
|
|
1170
1298
|
}
|
|
1171
1299
|
}
|