@alternative-path/testlens-playwright-reporter 0.4.4 → 0.4.5
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.js +118 -101
- package/index.ts +119 -102
- package/package.json +2 -1
- package/postinstall.js +8 -3
package/index.js
CHANGED
|
@@ -9,6 +9,11 @@ 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
|
+
// Create pino logger instance
|
|
14
|
+
const logger = (0, pino_1.default)({
|
|
15
|
+
level: process.env.LOG_LEVEL || 'info'
|
|
16
|
+
});
|
|
12
17
|
// Lazy-load mime module to support ESM
|
|
13
18
|
let mimeModule = null;
|
|
14
19
|
async function getMime() {
|
|
@@ -45,11 +50,11 @@ class TestLensReporter {
|
|
|
45
50
|
// For testlensBuildTag, support comma-separated values
|
|
46
51
|
if (key === 'testlensBuildTag' && value.includes(',')) {
|
|
47
52
|
customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
|
48
|
-
|
|
53
|
+
logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
|
|
49
54
|
}
|
|
50
55
|
else {
|
|
51
56
|
customArgs[key] = value;
|
|
52
|
-
|
|
57
|
+
logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
|
|
53
58
|
}
|
|
54
59
|
break; // Use first match
|
|
55
60
|
}
|
|
@@ -88,32 +93,43 @@ class TestLensReporter {
|
|
|
88
93
|
flushInterval: options.flushInterval || 5000,
|
|
89
94
|
retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
|
|
90
95
|
timeout: options.timeout || 60000,
|
|
96
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
97
|
+
ignoreSslErrors: options.ignoreSslErrors,
|
|
91
98
|
customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
|
|
92
99
|
};
|
|
93
100
|
if (!this.config.apiKey) {
|
|
94
101
|
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
102
|
}
|
|
96
103
|
if (apiKey !== options.apiKey) {
|
|
97
|
-
|
|
104
|
+
logger.info('✓ Using API key from environment variable');
|
|
105
|
+
}
|
|
106
|
+
// Default environment to allow self-signed certs unless explicitly set
|
|
107
|
+
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
|
|
108
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
98
109
|
}
|
|
99
110
|
// 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
|
|
111
|
+
let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
|
|
112
|
+
// Check various ways SSL validation can be disabled or enforced (in order of precedence)
|
|
113
|
+
if (this.config.ignoreSslErrors === true) {
|
|
104
114
|
rejectUnauthorized = false;
|
|
105
|
-
|
|
115
|
+
logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
|
|
106
116
|
}
|
|
107
117
|
else if (this.config.rejectUnauthorized === false) {
|
|
108
|
-
// Explicit configuration option
|
|
109
118
|
rejectUnauthorized = false;
|
|
110
|
-
|
|
119
|
+
logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
|
|
120
|
+
}
|
|
121
|
+
else if (this.config.rejectUnauthorized === true) {
|
|
122
|
+
rejectUnauthorized = true;
|
|
111
123
|
}
|
|
112
124
|
else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
|
|
113
|
-
// Environment variable override
|
|
114
125
|
rejectUnauthorized = false;
|
|
115
|
-
|
|
126
|
+
logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
127
|
+
}
|
|
128
|
+
else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
|
|
129
|
+
rejectUnauthorized = true;
|
|
116
130
|
}
|
|
131
|
+
// Mirror the resolved value so all HTTPS requests in this process follow it
|
|
132
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
|
|
117
133
|
// Set up axios instance with retry logic and enhanced SSL handling
|
|
118
134
|
this.axiosInstance = axios_1.default.create({
|
|
119
135
|
baseURL: this.config.apiEndpoint,
|
|
@@ -152,11 +168,11 @@ class TestLensReporter {
|
|
|
152
168
|
this.runCreationFailed = false;
|
|
153
169
|
// Log custom metadata if any
|
|
154
170
|
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
155
|
-
|
|
171
|
+
logger.info('\n[METADATA] Custom Metadata Detected:');
|
|
156
172
|
Object.entries(this.config.customMetadata).forEach(([key, value]) => {
|
|
157
|
-
|
|
173
|
+
logger.info(` ${key}: ${value}`);
|
|
158
174
|
});
|
|
159
|
-
|
|
175
|
+
logger.info('');
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
178
|
initializeRunMetadata() {
|
|
@@ -221,24 +237,24 @@ class TestLensReporter {
|
|
|
221
237
|
async onBegin(config, suite) {
|
|
222
238
|
// Show Build Name if provided, otherwise show Run ID
|
|
223
239
|
if (this.runMetadata.testlensBuildName) {
|
|
224
|
-
|
|
225
|
-
|
|
240
|
+
logger.info(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
241
|
+
logger.info(` Run ID: ${this.runId}`);
|
|
226
242
|
}
|
|
227
243
|
else {
|
|
228
|
-
|
|
244
|
+
logger.info(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
229
245
|
}
|
|
230
246
|
// Collect Git information if enabled
|
|
231
247
|
if (this.config.enableGitInfo) {
|
|
232
248
|
this.runMetadata.gitInfo = await this.collectGitInfo();
|
|
233
249
|
if (this.runMetadata.gitInfo) {
|
|
234
|
-
|
|
250
|
+
logger.info(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
235
251
|
}
|
|
236
252
|
else {
|
|
237
|
-
|
|
253
|
+
logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
238
254
|
}
|
|
239
255
|
}
|
|
240
256
|
else {
|
|
241
|
-
|
|
257
|
+
logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
|
|
242
258
|
}
|
|
243
259
|
// Add shard information if available
|
|
244
260
|
if (config.shard) {
|
|
@@ -257,7 +273,7 @@ class TestLensReporter {
|
|
|
257
273
|
}
|
|
258
274
|
async onTestBegin(test, result) {
|
|
259
275
|
// Log which test is starting
|
|
260
|
-
|
|
276
|
+
logger.info(`\n[TEST] Running test: ${test.title}`);
|
|
261
277
|
const specPath = test.location.file;
|
|
262
278
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
263
279
|
// Create or update spec data
|
|
@@ -330,10 +346,10 @@ class TestLensReporter {
|
|
|
330
346
|
async onTestEnd(test, result) {
|
|
331
347
|
const testId = this.getTestId(test);
|
|
332
348
|
let testData = this.testMap.get(testId);
|
|
333
|
-
|
|
349
|
+
logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
|
|
334
350
|
// For skipped tests, onTestBegin might not be called, so we need to create the test data here
|
|
335
351
|
if (!testData) {
|
|
336
|
-
|
|
352
|
+
logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
|
|
337
353
|
// Create spec data if not exists (skipped tests might not have spec data either)
|
|
338
354
|
const specPath = test.location.file;
|
|
339
355
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -478,7 +494,7 @@ class TestLensReporter {
|
|
|
478
494
|
});
|
|
479
495
|
// Send testEnd event for all tests, regardless of status
|
|
480
496
|
// This ensures tests that are interrupted or have unexpected statuses are properly recorded
|
|
481
|
-
|
|
497
|
+
logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
482
498
|
// Send test end event to API and get response
|
|
483
499
|
const testEndResponse = await this.sendToApi({
|
|
484
500
|
type: 'testEnd',
|
|
@@ -561,13 +577,13 @@ class TestLensReporter {
|
|
|
561
577
|
this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
|
|
562
578
|
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
563
579
|
if (this.pendingUploads.size > 0) {
|
|
564
|
-
|
|
580
|
+
logger.info(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
565
581
|
try {
|
|
566
582
|
await Promise.all(Array.from(this.pendingUploads));
|
|
567
|
-
|
|
583
|
+
logger.info(`[OK] All artifact uploads completed`);
|
|
568
584
|
}
|
|
569
585
|
catch (error) {
|
|
570
|
-
|
|
586
|
+
logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
571
587
|
}
|
|
572
588
|
}
|
|
573
589
|
// Calculate final stats
|
|
@@ -598,13 +614,13 @@ class TestLensReporter {
|
|
|
598
614
|
});
|
|
599
615
|
// Show Build Name if provided, otherwise show Run ID
|
|
600
616
|
if (this.runMetadata.testlensBuildName) {
|
|
601
|
-
|
|
602
|
-
|
|
617
|
+
logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
|
|
618
|
+
logger.info(` Run ID: ${this.runId}`);
|
|
603
619
|
}
|
|
604
620
|
else {
|
|
605
|
-
|
|
621
|
+
logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
|
|
606
622
|
}
|
|
607
|
-
|
|
623
|
+
logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
|
|
608
624
|
}
|
|
609
625
|
async sendToApi(payload) {
|
|
610
626
|
// Skip sending if run creation already failed
|
|
@@ -618,7 +634,7 @@ class TestLensReporter {
|
|
|
618
634
|
}
|
|
619
635
|
});
|
|
620
636
|
if (this.config.enableRealTimeStream) {
|
|
621
|
-
|
|
637
|
+
logger.info(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
622
638
|
}
|
|
623
639
|
// Return response data for caller to use
|
|
624
640
|
return response.data;
|
|
@@ -632,65 +648,66 @@ class TestLensReporter {
|
|
|
632
648
|
if (payload.type === 'runStart' && errorData?.limit_type === 'test_runs') {
|
|
633
649
|
this.runCreationFailed = true;
|
|
634
650
|
}
|
|
635
|
-
|
|
651
|
+
logger.error('\n' + '='.repeat(80));
|
|
636
652
|
if (errorData?.limit_type === 'test_cases') {
|
|
637
|
-
|
|
653
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
638
654
|
}
|
|
639
655
|
else if (errorData?.limit_type === 'test_runs') {
|
|
640
|
-
|
|
656
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
641
657
|
}
|
|
642
658
|
else {
|
|
643
|
-
|
|
659
|
+
logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
|
|
644
660
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
661
|
+
logger.error('='.repeat(80));
|
|
662
|
+
logger.error('');
|
|
663
|
+
logger.error(errorData?.message || 'You have reached your plan limit.');
|
|
664
|
+
logger.error('');
|
|
665
|
+
logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
666
|
+
logger.error('');
|
|
667
|
+
logger.error('To continue, please upgrade your plan.');
|
|
668
|
+
logger.error('Contact: support@alternative-path.com');
|
|
669
|
+
logger.error('');
|
|
670
|
+
logger.error('='.repeat(80));
|
|
671
|
+
logger.error('');
|
|
656
672
|
return; // Don't log the full error object for limit errors
|
|
657
673
|
}
|
|
658
674
|
// Check for trial expiration, subscription errors, or limit errors (401)
|
|
659
675
|
if (status === 401) {
|
|
660
676
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
661
677
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
662
|
-
|
|
678
|
+
logger.error('\n' + '='.repeat(80));
|
|
663
679
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
664
|
-
|
|
680
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
665
681
|
}
|
|
666
682
|
else if (errorData?.error === 'test_runs_limit_reached') {
|
|
667
|
-
|
|
683
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
668
684
|
}
|
|
669
685
|
else {
|
|
670
|
-
|
|
686
|
+
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
671
687
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
688
|
+
logger.error('='.repeat(80));
|
|
689
|
+
logger.error('');
|
|
690
|
+
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
691
|
+
logger.error('');
|
|
692
|
+
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
693
|
+
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
694
|
+
logger.error('');
|
|
679
695
|
if (errorData?.trial_end_date) {
|
|
680
|
-
|
|
681
|
-
|
|
696
|
+
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
697
|
+
logger.error('');
|
|
682
698
|
}
|
|
683
|
-
|
|
684
|
-
|
|
699
|
+
logger.error('='.repeat(80));
|
|
700
|
+
logger.error('');
|
|
685
701
|
}
|
|
686
702
|
else {
|
|
687
|
-
|
|
703
|
+
logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
688
704
|
}
|
|
689
705
|
}
|
|
690
706
|
else if (status !== 403) {
|
|
691
707
|
// Log other errors (but not 403 which we handled above)
|
|
692
|
-
|
|
693
|
-
message:
|
|
708
|
+
logger.error({
|
|
709
|
+
message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
|
|
710
|
+
error: error?.message || 'Unknown error',
|
|
694
711
|
status: status,
|
|
695
712
|
statusText: error?.response?.statusText,
|
|
696
713
|
data: errorData,
|
|
@@ -716,12 +733,12 @@ class TestLensReporter {
|
|
|
716
733
|
const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
|
|
717
734
|
// Skip video if disabled in config
|
|
718
735
|
if (isVideo && !this.config.enableVideo) {
|
|
719
|
-
|
|
736
|
+
logger.info(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
720
737
|
continue;
|
|
721
738
|
}
|
|
722
739
|
// Skip screenshot if disabled in config
|
|
723
740
|
if (isScreenshot && !this.config.enableScreenshot) {
|
|
724
|
-
|
|
741
|
+
logger.info(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
725
742
|
continue;
|
|
726
743
|
}
|
|
727
744
|
try {
|
|
@@ -759,13 +776,13 @@ class TestLensReporter {
|
|
|
759
776
|
const uploadPromise = Promise.resolve().then(async () => {
|
|
760
777
|
try {
|
|
761
778
|
if (!attachment.path) {
|
|
762
|
-
|
|
779
|
+
logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
763
780
|
return;
|
|
764
781
|
}
|
|
765
782
|
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
|
|
766
783
|
// Skip if upload failed or file was too large
|
|
767
784
|
if (!s3Data) {
|
|
768
|
-
|
|
785
|
+
logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
769
786
|
return;
|
|
770
787
|
}
|
|
771
788
|
const artifactData = {
|
|
@@ -786,10 +803,10 @@ class TestLensReporter {
|
|
|
786
803
|
timestamp: new Date().toISOString(),
|
|
787
804
|
artifact: artifactData
|
|
788
805
|
});
|
|
789
|
-
|
|
806
|
+
logger.info(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
790
807
|
}
|
|
791
808
|
catch (error) {
|
|
792
|
-
|
|
809
|
+
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
|
|
793
810
|
}
|
|
794
811
|
});
|
|
795
812
|
// Track this upload and ensure cleanup on completion
|
|
@@ -801,7 +818,7 @@ class TestLensReporter {
|
|
|
801
818
|
// They will be awaited in onEnd
|
|
802
819
|
}
|
|
803
820
|
catch (error) {
|
|
804
|
-
|
|
821
|
+
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
|
|
805
822
|
}
|
|
806
823
|
}
|
|
807
824
|
}
|
|
@@ -831,16 +848,16 @@ class TestLensReporter {
|
|
|
831
848
|
codeBlocks,
|
|
832
849
|
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
|
|
833
850
|
});
|
|
834
|
-
|
|
851
|
+
logger.info(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
835
852
|
}
|
|
836
853
|
catch (error) {
|
|
837
854
|
const errorData = error?.response?.data;
|
|
838
855
|
// Handle duplicate spec code blocks gracefully (when re-running tests)
|
|
839
856
|
if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
|
|
840
|
-
|
|
857
|
+
logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
841
858
|
return;
|
|
842
859
|
}
|
|
843
|
-
|
|
860
|
+
logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
|
|
844
861
|
}
|
|
845
862
|
}
|
|
846
863
|
extractTestBlocks(filePath) {
|
|
@@ -899,7 +916,7 @@ class TestLensReporter {
|
|
|
899
916
|
return blocks;
|
|
900
917
|
}
|
|
901
918
|
catch (error) {
|
|
902
|
-
|
|
919
|
+
logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
|
|
903
920
|
return [];
|
|
904
921
|
}
|
|
905
922
|
}
|
|
@@ -922,7 +939,7 @@ class TestLensReporter {
|
|
|
922
939
|
}
|
|
923
940
|
catch (e) {
|
|
924
941
|
// Remote info is optional - handle gracefully
|
|
925
|
-
|
|
942
|
+
logger.info('[INFO] No git remote configured, skipping remote info');
|
|
926
943
|
}
|
|
927
944
|
const isDirty = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
|
|
928
945
|
return {
|
|
@@ -994,7 +1011,7 @@ class TestLensReporter {
|
|
|
994
1011
|
// Check file size first
|
|
995
1012
|
const fileSize = this.getFileSize(filePath);
|
|
996
1013
|
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
997
|
-
|
|
1014
|
+
logger.info(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
998
1015
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
999
1016
|
// Step 1: Request pre-signed URL from server
|
|
1000
1017
|
const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
|
|
@@ -1019,7 +1036,7 @@ class TestLensReporter {
|
|
|
1019
1036
|
}
|
|
1020
1037
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1021
1038
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1022
|
-
|
|
1039
|
+
logger.info(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1023
1040
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1024
1041
|
// IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
|
|
1025
1042
|
// The backend signs with ServerSideEncryption:'AES256', so we must send that header
|
|
@@ -1037,7 +1054,7 @@ class TestLensReporter {
|
|
|
1037
1054
|
if (uploadResponse.status !== 200) {
|
|
1038
1055
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1039
1056
|
}
|
|
1040
|
-
|
|
1057
|
+
logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1041
1058
|
// Step 3: Confirm upload with server to save metadata
|
|
1042
1059
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
1043
1060
|
const confirmBody = {
|
|
@@ -1059,7 +1076,7 @@ class TestLensReporter {
|
|
|
1059
1076
|
});
|
|
1060
1077
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1061
1078
|
const artifact = confirmResponse.data.artifact;
|
|
1062
|
-
|
|
1079
|
+
logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1063
1080
|
return {
|
|
1064
1081
|
key: s3Key,
|
|
1065
1082
|
url: artifact.s3Url,
|
|
@@ -1078,29 +1095,29 @@ class TestLensReporter {
|
|
|
1078
1095
|
const errorData = error?.response?.data;
|
|
1079
1096
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1080
1097
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1081
|
-
|
|
1098
|
+
logger.error('\n' + '='.repeat(80));
|
|
1082
1099
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1083
|
-
|
|
1100
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1084
1101
|
}
|
|
1085
1102
|
else if (errorData?.error === 'test_runs_limit_reached') {
|
|
1086
|
-
|
|
1103
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1087
1104
|
}
|
|
1088
1105
|
else {
|
|
1089
|
-
|
|
1106
|
+
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1090
1107
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1108
|
+
logger.error('='.repeat(80));
|
|
1109
|
+
logger.error('');
|
|
1110
|
+
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1111
|
+
logger.error('');
|
|
1112
|
+
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1113
|
+
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1114
|
+
logger.error('');
|
|
1098
1115
|
if (errorData?.trial_end_date) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1116
|
+
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1117
|
+
logger.error('');
|
|
1101
1118
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1119
|
+
logger.error('='.repeat(80));
|
|
1120
|
+
logger.error('');
|
|
1104
1121
|
return null;
|
|
1105
1122
|
}
|
|
1106
1123
|
}
|
|
@@ -1118,9 +1135,9 @@ class TestLensReporter {
|
|
|
1118
1135
|
else if (error.response?.status === 403) {
|
|
1119
1136
|
errorMsg = `Access denied (403) - presigned URL may have expired`;
|
|
1120
1137
|
}
|
|
1121
|
-
|
|
1138
|
+
logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
|
|
1122
1139
|
if (error.response?.data) {
|
|
1123
|
-
|
|
1140
|
+
logger.error({ errorDetails: error.response.data }, 'Error details');
|
|
1124
1141
|
}
|
|
1125
1142
|
// Don't throw, just return null to continue with other artifacts
|
|
1126
1143
|
return null;
|
|
@@ -1138,7 +1155,7 @@ class TestLensReporter {
|
|
|
1138
1155
|
}
|
|
1139
1156
|
}
|
|
1140
1157
|
catch (error) {
|
|
1141
|
-
|
|
1158
|
+
logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
|
|
1142
1159
|
}
|
|
1143
1160
|
// Fallback to basic content type mapping
|
|
1144
1161
|
const contentTypes = {
|
|
@@ -1165,7 +1182,7 @@ class TestLensReporter {
|
|
|
1165
1182
|
return stats.size;
|
|
1166
1183
|
}
|
|
1167
1184
|
catch (error) {
|
|
1168
|
-
|
|
1185
|
+
logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
|
|
1169
1186
|
return 0;
|
|
1170
1187
|
}
|
|
1171
1188
|
}
|
package/index.ts
CHANGED
|
@@ -6,7 +6,12 @@ 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
|
+
// Create pino logger instance
|
|
12
|
+
const logger = pino({
|
|
13
|
+
level: process.env.LOG_LEVEL || 'info'
|
|
14
|
+
});
|
|
10
15
|
|
|
11
16
|
// Lazy-load mime module to support ESM
|
|
12
17
|
let mimeModule: any = null;
|
|
@@ -214,10 +219,10 @@ export class TestLensReporter implements Reporter {
|
|
|
214
219
|
// For testlensBuildTag, support comma-separated values
|
|
215
220
|
if (key === 'testlensBuildTag' && value.includes(',')) {
|
|
216
221
|
customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
|
217
|
-
|
|
222
|
+
logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
|
|
218
223
|
} else {
|
|
219
224
|
customArgs[key] = value;
|
|
220
|
-
|
|
225
|
+
logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
|
|
221
226
|
}
|
|
222
227
|
break; // Use first match
|
|
223
228
|
}
|
|
@@ -257,6 +262,8 @@ export class TestLensReporter implements Reporter {
|
|
|
257
262
|
flushInterval: options.flushInterval || 5000,
|
|
258
263
|
retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
|
|
259
264
|
timeout: options.timeout || 60000,
|
|
265
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
266
|
+
ignoreSslErrors: options.ignoreSslErrors,
|
|
260
267
|
customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
|
|
261
268
|
} as Required<TestLensReporterConfig>;
|
|
262
269
|
|
|
@@ -265,27 +272,36 @@ export class TestLensReporter implements Reporter {
|
|
|
265
272
|
}
|
|
266
273
|
|
|
267
274
|
if (apiKey !== options.apiKey) {
|
|
268
|
-
|
|
275
|
+
logger.info('✓ Using API key from environment variable');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Default environment to allow self-signed certs unless explicitly set
|
|
279
|
+
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
|
|
280
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
269
281
|
}
|
|
270
282
|
|
|
271
283
|
// Determine SSL validation behavior
|
|
272
|
-
let rejectUnauthorized =
|
|
284
|
+
let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
|
|
273
285
|
|
|
274
|
-
// Check various ways SSL validation can be disabled (in order of precedence)
|
|
275
|
-
if (this.config.ignoreSslErrors) {
|
|
276
|
-
// Explicit configuration option
|
|
286
|
+
// Check various ways SSL validation can be disabled or enforced (in order of precedence)
|
|
287
|
+
if (this.config.ignoreSslErrors === true) {
|
|
277
288
|
rejectUnauthorized = false;
|
|
278
|
-
|
|
289
|
+
logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
|
|
279
290
|
} else if (this.config.rejectUnauthorized === false) {
|
|
280
|
-
// Explicit configuration option
|
|
281
291
|
rejectUnauthorized = false;
|
|
282
|
-
|
|
292
|
+
logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
|
|
293
|
+
} else if (this.config.rejectUnauthorized === true) {
|
|
294
|
+
rejectUnauthorized = true;
|
|
283
295
|
} else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
|
|
284
|
-
// Environment variable override
|
|
285
296
|
rejectUnauthorized = false;
|
|
286
|
-
|
|
297
|
+
logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
298
|
+
} else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
|
|
299
|
+
rejectUnauthorized = true;
|
|
287
300
|
}
|
|
288
301
|
|
|
302
|
+
// Mirror the resolved value so all HTTPS requests in this process follow it
|
|
303
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
|
|
304
|
+
|
|
289
305
|
// Set up axios instance with retry logic and enhanced SSL handling
|
|
290
306
|
this.axiosInstance = axios.create({
|
|
291
307
|
baseURL: this.config.apiEndpoint,
|
|
@@ -333,11 +349,11 @@ export class TestLensReporter implements Reporter {
|
|
|
333
349
|
|
|
334
350
|
// Log custom metadata if any
|
|
335
351
|
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
336
|
-
|
|
352
|
+
logger.info('\n[METADATA] Custom Metadata Detected:');
|
|
337
353
|
Object.entries(this.config.customMetadata).forEach(([key, value]) => {
|
|
338
|
-
|
|
354
|
+
logger.info(` ${key}: ${value}`);
|
|
339
355
|
});
|
|
340
|
-
|
|
356
|
+
logger.info('');
|
|
341
357
|
}
|
|
342
358
|
}
|
|
343
359
|
|
|
@@ -409,22 +425,22 @@ export class TestLensReporter implements Reporter {
|
|
|
409
425
|
async onBegin(config: FullConfig, suite: Suite): Promise<void> {
|
|
410
426
|
// Show Build Name if provided, otherwise show Run ID
|
|
411
427
|
if (this.runMetadata.testlensBuildName) {
|
|
412
|
-
|
|
413
|
-
|
|
428
|
+
logger.info(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
429
|
+
logger.info(` Run ID: ${this.runId}`);
|
|
414
430
|
} else {
|
|
415
|
-
|
|
431
|
+
logger.info(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
416
432
|
}
|
|
417
433
|
|
|
418
434
|
// Collect Git information if enabled
|
|
419
435
|
if (this.config.enableGitInfo) {
|
|
420
436
|
this.runMetadata.gitInfo = await this.collectGitInfo();
|
|
421
437
|
if (this.runMetadata.gitInfo) {
|
|
422
|
-
|
|
438
|
+
logger.info(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
423
439
|
} else {
|
|
424
|
-
|
|
440
|
+
logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
425
441
|
}
|
|
426
442
|
} else {
|
|
427
|
-
|
|
443
|
+
logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
|
|
428
444
|
}
|
|
429
445
|
|
|
430
446
|
// Add shard information if available
|
|
@@ -446,7 +462,7 @@ export class TestLensReporter implements Reporter {
|
|
|
446
462
|
|
|
447
463
|
async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
|
|
448
464
|
// Log which test is starting
|
|
449
|
-
|
|
465
|
+
logger.info(`\n[TEST] Running test: ${test.title}`);
|
|
450
466
|
|
|
451
467
|
const specPath = test.location.file;
|
|
452
468
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -527,11 +543,11 @@ export class TestLensReporter implements Reporter {
|
|
|
527
543
|
const testId = this.getTestId(test);
|
|
528
544
|
let testData = this.testMap.get(testId);
|
|
529
545
|
|
|
530
|
-
|
|
546
|
+
logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
|
|
531
547
|
|
|
532
548
|
// For skipped tests, onTestBegin might not be called, so we need to create the test data here
|
|
533
549
|
if (!testData) {
|
|
534
|
-
|
|
550
|
+
logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
|
|
535
551
|
// Create spec data if not exists (skipped tests might not have spec data either)
|
|
536
552
|
const specPath = test.location.file;
|
|
537
553
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -697,7 +713,7 @@ export class TestLensReporter implements Reporter {
|
|
|
697
713
|
|
|
698
714
|
// Send testEnd event for all tests, regardless of status
|
|
699
715
|
// This ensures tests that are interrupted or have unexpected statuses are properly recorded
|
|
700
|
-
|
|
716
|
+
logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
701
717
|
// Send test end event to API and get response
|
|
702
718
|
const testEndResponse = await this.sendToApi({
|
|
703
719
|
type: 'testEnd',
|
|
@@ -787,12 +803,12 @@ export class TestLensReporter implements Reporter {
|
|
|
787
803
|
|
|
788
804
|
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
789
805
|
if (this.pendingUploads.size > 0) {
|
|
790
|
-
|
|
806
|
+
logger.info(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
791
807
|
try {
|
|
792
808
|
await Promise.all(Array.from(this.pendingUploads));
|
|
793
|
-
|
|
809
|
+
logger.info(`[OK] All artifact uploads completed`);
|
|
794
810
|
} catch (error) {
|
|
795
|
-
|
|
811
|
+
logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
796
812
|
}
|
|
797
813
|
}
|
|
798
814
|
|
|
@@ -827,12 +843,12 @@ export class TestLensReporter implements Reporter {
|
|
|
827
843
|
|
|
828
844
|
// Show Build Name if provided, otherwise show Run ID
|
|
829
845
|
if (this.runMetadata.testlensBuildName) {
|
|
830
|
-
|
|
831
|
-
|
|
846
|
+
logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
|
|
847
|
+
logger.info(` Run ID: ${this.runId}`);
|
|
832
848
|
} else {
|
|
833
|
-
|
|
849
|
+
logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
|
|
834
850
|
}
|
|
835
|
-
|
|
851
|
+
logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
|
|
836
852
|
}
|
|
837
853
|
|
|
838
854
|
private async sendToApi(payload: any): Promise<any> {
|
|
@@ -848,7 +864,7 @@ export class TestLensReporter implements Reporter {
|
|
|
848
864
|
}
|
|
849
865
|
});
|
|
850
866
|
if (this.config.enableRealTimeStream) {
|
|
851
|
-
|
|
867
|
+
logger.info(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
852
868
|
}
|
|
853
869
|
// Return response data for caller to use
|
|
854
870
|
return response.data;
|
|
@@ -863,25 +879,25 @@ export class TestLensReporter implements Reporter {
|
|
|
863
879
|
this.runCreationFailed = true;
|
|
864
880
|
}
|
|
865
881
|
|
|
866
|
-
|
|
882
|
+
logger.error('\n' + '='.repeat(80));
|
|
867
883
|
if (errorData?.limit_type === 'test_cases') {
|
|
868
|
-
|
|
884
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
869
885
|
} else if (errorData?.limit_type === 'test_runs') {
|
|
870
|
-
|
|
886
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
871
887
|
} else {
|
|
872
|
-
|
|
888
|
+
logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
|
|
873
889
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
890
|
+
logger.error('='.repeat(80));
|
|
891
|
+
logger.error('');
|
|
892
|
+
logger.error(errorData?.message || 'You have reached your plan limit.');
|
|
893
|
+
logger.error('');
|
|
894
|
+
logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
895
|
+
logger.error('');
|
|
896
|
+
logger.error('To continue, please upgrade your plan.');
|
|
897
|
+
logger.error('Contact: support@alternative-path.com');
|
|
898
|
+
logger.error('');
|
|
899
|
+
logger.error('='.repeat(80));
|
|
900
|
+
logger.error('');
|
|
885
901
|
return; // Don't log the full error object for limit errors
|
|
886
902
|
}
|
|
887
903
|
|
|
@@ -889,36 +905,37 @@ export class TestLensReporter implements Reporter {
|
|
|
889
905
|
if (status === 401) {
|
|
890
906
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
891
907
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
892
|
-
|
|
908
|
+
logger.error('\n' + '='.repeat(80));
|
|
893
909
|
|
|
894
910
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
895
|
-
|
|
911
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
896
912
|
} else if (errorData?.error === 'test_runs_limit_reached') {
|
|
897
|
-
|
|
913
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
898
914
|
} else {
|
|
899
|
-
|
|
915
|
+
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
900
916
|
}
|
|
901
917
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
918
|
+
logger.error('='.repeat(80));
|
|
919
|
+
logger.error('');
|
|
920
|
+
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
921
|
+
logger.error('');
|
|
922
|
+
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
923
|
+
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
924
|
+
logger.error('');
|
|
909
925
|
if (errorData?.trial_end_date) {
|
|
910
|
-
|
|
911
|
-
|
|
926
|
+
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
927
|
+
logger.error('');
|
|
912
928
|
}
|
|
913
|
-
|
|
914
|
-
|
|
929
|
+
logger.error('='.repeat(80));
|
|
930
|
+
logger.error('');
|
|
915
931
|
} else {
|
|
916
|
-
|
|
932
|
+
logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
917
933
|
}
|
|
918
934
|
} else if (status !== 403) {
|
|
919
935
|
// Log other errors (but not 403 which we handled above)
|
|
920
|
-
|
|
921
|
-
message:
|
|
936
|
+
logger.error({
|
|
937
|
+
message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
|
|
938
|
+
error: error?.message || 'Unknown error',
|
|
922
939
|
status: status,
|
|
923
940
|
statusText: error?.response?.statusText,
|
|
924
941
|
data: errorData,
|
|
@@ -949,13 +966,13 @@ export class TestLensReporter implements Reporter {
|
|
|
949
966
|
|
|
950
967
|
// Skip video if disabled in config
|
|
951
968
|
if (isVideo && !this.config.enableVideo) {
|
|
952
|
-
|
|
969
|
+
logger.info(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
953
970
|
continue;
|
|
954
971
|
}
|
|
955
972
|
|
|
956
973
|
// Skip screenshot if disabled in config
|
|
957
974
|
if (isScreenshot && !this.config.enableScreenshot) {
|
|
958
|
-
|
|
975
|
+
logger.info(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
959
976
|
continue;
|
|
960
977
|
}
|
|
961
978
|
|
|
@@ -995,7 +1012,7 @@ export class TestLensReporter implements Reporter {
|
|
|
995
1012
|
const uploadPromise = Promise.resolve().then(async () => {
|
|
996
1013
|
try {
|
|
997
1014
|
if (!attachment.path) {
|
|
998
|
-
|
|
1015
|
+
logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
999
1016
|
return;
|
|
1000
1017
|
}
|
|
1001
1018
|
|
|
@@ -1003,7 +1020,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1003
1020
|
|
|
1004
1021
|
// Skip if upload failed or file was too large
|
|
1005
1022
|
if (!s3Data) {
|
|
1006
|
-
|
|
1023
|
+
logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
1007
1024
|
return;
|
|
1008
1025
|
}
|
|
1009
1026
|
|
|
@@ -1027,9 +1044,9 @@ export class TestLensReporter implements Reporter {
|
|
|
1027
1044
|
artifact: artifactData
|
|
1028
1045
|
});
|
|
1029
1046
|
|
|
1030
|
-
|
|
1047
|
+
logger.info(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
1031
1048
|
} catch (error) {
|
|
1032
|
-
|
|
1049
|
+
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
|
|
1033
1050
|
}
|
|
1034
1051
|
});
|
|
1035
1052
|
|
|
@@ -1042,7 +1059,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1042
1059
|
// Don't await here - let uploads happen in parallel
|
|
1043
1060
|
// They will be awaited in onEnd
|
|
1044
1061
|
} catch (error) {
|
|
1045
|
-
|
|
1062
|
+
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
|
|
1046
1063
|
}
|
|
1047
1064
|
}
|
|
1048
1065
|
}
|
|
@@ -1077,17 +1094,17 @@ export class TestLensReporter implements Reporter {
|
|
|
1077
1094
|
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
|
|
1078
1095
|
});
|
|
1079
1096
|
|
|
1080
|
-
|
|
1097
|
+
logger.info(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
1081
1098
|
} catch (error: any) {
|
|
1082
1099
|
const errorData = error?.response?.data;
|
|
1083
1100
|
|
|
1084
1101
|
// Handle duplicate spec code blocks gracefully (when re-running tests)
|
|
1085
1102
|
if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
|
|
1086
|
-
|
|
1103
|
+
logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
1087
1104
|
return;
|
|
1088
1105
|
}
|
|
1089
1106
|
|
|
1090
|
-
|
|
1107
|
+
logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
|
|
1091
1108
|
}
|
|
1092
1109
|
}
|
|
1093
1110
|
|
|
@@ -1153,7 +1170,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1153
1170
|
|
|
1154
1171
|
return blocks;
|
|
1155
1172
|
} catch (error: any) {
|
|
1156
|
-
|
|
1173
|
+
logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
|
|
1157
1174
|
return [];
|
|
1158
1175
|
}
|
|
1159
1176
|
}
|
|
@@ -1177,7 +1194,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1177
1194
|
}
|
|
1178
1195
|
} catch (e) {
|
|
1179
1196
|
// Remote info is optional - handle gracefully
|
|
1180
|
-
|
|
1197
|
+
logger.info('[INFO] No git remote configured, skipping remote info');
|
|
1181
1198
|
}
|
|
1182
1199
|
|
|
1183
1200
|
const isDirty = execSync('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
|
|
@@ -1258,7 +1275,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1258
1275
|
const fileSize = this.getFileSize(filePath);
|
|
1259
1276
|
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
1260
1277
|
|
|
1261
|
-
|
|
1278
|
+
logger.info(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
1262
1279
|
|
|
1263
1280
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
1264
1281
|
|
|
@@ -1290,7 +1307,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1290
1307
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1291
1308
|
|
|
1292
1309
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1293
|
-
|
|
1310
|
+
logger.info(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1294
1311
|
|
|
1295
1312
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1296
1313
|
|
|
@@ -1312,7 +1329,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1312
1329
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1313
1330
|
}
|
|
1314
1331
|
|
|
1315
|
-
|
|
1332
|
+
logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1316
1333
|
|
|
1317
1334
|
// Step 3: Confirm upload with server to save metadata
|
|
1318
1335
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
@@ -1338,7 +1355,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1338
1355
|
|
|
1339
1356
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1340
1357
|
const artifact = confirmResponse.data.artifact;
|
|
1341
|
-
|
|
1358
|
+
logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1342
1359
|
return {
|
|
1343
1360
|
key: s3Key,
|
|
1344
1361
|
url: artifact.s3Url,
|
|
@@ -1356,29 +1373,29 @@ export class TestLensReporter implements Reporter {
|
|
|
1356
1373
|
|
|
1357
1374
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1358
1375
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1359
|
-
|
|
1376
|
+
logger.error('\n' + '='.repeat(80));
|
|
1360
1377
|
|
|
1361
1378
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1362
|
-
|
|
1379
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1363
1380
|
} else if (errorData?.error === 'test_runs_limit_reached') {
|
|
1364
|
-
|
|
1381
|
+
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1365
1382
|
} else {
|
|
1366
|
-
|
|
1383
|
+
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1367
1384
|
}
|
|
1368
1385
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1386
|
+
logger.error('='.repeat(80));
|
|
1387
|
+
logger.error('');
|
|
1388
|
+
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1389
|
+
logger.error('');
|
|
1390
|
+
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1391
|
+
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1392
|
+
logger.error('');
|
|
1376
1393
|
if (errorData?.trial_end_date) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1394
|
+
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1395
|
+
logger.error('');
|
|
1379
1396
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1397
|
+
logger.error('='.repeat(80));
|
|
1398
|
+
logger.error('');
|
|
1382
1399
|
return null;
|
|
1383
1400
|
}
|
|
1384
1401
|
}
|
|
@@ -1396,9 +1413,9 @@ export class TestLensReporter implements Reporter {
|
|
|
1396
1413
|
errorMsg = `Access denied (403) - presigned URL may have expired`;
|
|
1397
1414
|
}
|
|
1398
1415
|
|
|
1399
|
-
|
|
1416
|
+
logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
|
|
1400
1417
|
if (error.response?.data) {
|
|
1401
|
-
|
|
1418
|
+
logger.error({ errorDetails: error.response.data }, 'Error details');
|
|
1402
1419
|
}
|
|
1403
1420
|
|
|
1404
1421
|
// Don't throw, just return null to continue with other artifacts
|
|
@@ -1417,7 +1434,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1417
1434
|
return mimeType || 'application/octet-stream';
|
|
1418
1435
|
}
|
|
1419
1436
|
} catch (error: any) {
|
|
1420
|
-
|
|
1437
|
+
logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
|
|
1421
1438
|
}
|
|
1422
1439
|
// Fallback to basic content type mapping
|
|
1423
1440
|
const contentTypes: Record<string, string> = {
|
|
@@ -1445,7 +1462,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1445
1462
|
const stats = fs.statSync(filePath);
|
|
1446
1463
|
return stats.size;
|
|
1447
1464
|
} catch (error) {
|
|
1448
|
-
|
|
1465
|
+
logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
|
|
1449
1466
|
return 0;
|
|
1450
1467
|
}
|
|
1451
1468
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alternative-path/testlens-playwright-reporter",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"dotenv": "^16.4.5",
|
|
57
57
|
"form-data": "^4.0.1",
|
|
58
58
|
"mime": "^4.0.4",
|
|
59
|
+
"pino": "^9.0.0",
|
|
59
60
|
"tslib": "^2.8.1"
|
|
60
61
|
},
|
|
61
62
|
"engines": {
|
package/postinstall.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const pino = require('pino');
|
|
4
|
+
|
|
5
|
+
const logger = pino({
|
|
6
|
+
level: process.env.LOG_LEVEL || 'info'
|
|
7
|
+
});
|
|
3
8
|
|
|
4
9
|
try {
|
|
5
10
|
// Get the parent project's node_modules/.bin directory
|
|
@@ -10,7 +15,7 @@ try {
|
|
|
10
15
|
|
|
11
16
|
// Check if parent bin directory exists
|
|
12
17
|
if (!fs.existsSync(parentBinDir)) {
|
|
13
|
-
|
|
18
|
+
logger.info('Parent .bin directory not found, skipping cross-env setup');
|
|
14
19
|
process.exit(0);
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -25,7 +30,7 @@ try {
|
|
|
25
30
|
|
|
26
31
|
// Copy the file
|
|
27
32
|
fs.copyFileSync(sourceCmdPath, targetCmdPath);
|
|
28
|
-
|
|
33
|
+
logger.info('✓ cross-env binary installed');
|
|
29
34
|
|
|
30
35
|
// Also copy PowerShell script on Windows
|
|
31
36
|
if (process.platform === 'win32' && fs.existsSync(sourcePs1Path)) {
|
|
@@ -35,5 +40,5 @@ try {
|
|
|
35
40
|
}
|
|
36
41
|
} catch (error) {
|
|
37
42
|
// Don't fail installation if this doesn't work
|
|
38
|
-
|
|
43
|
+
logger.info('Note: Could not setup cross-env automatically:', error.message);
|
|
39
44
|
}
|