@alternative-path/testlens-playwright-reporter 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/index.d.ts +3 -17
- package/index.js +133 -329
- package/index.ts +138 -355
- package/package.json +73 -83
- package/testlens-ca-bundle.pem +0 -88
package/index.js
CHANGED
|
@@ -7,7 +7,6 @@ const os = tslib_1.__importStar(require("os"));
|
|
|
7
7
|
const path = tslib_1.__importStar(require("path"));
|
|
8
8
|
const fs = tslib_1.__importStar(require("fs"));
|
|
9
9
|
const https = tslib_1.__importStar(require("https"));
|
|
10
|
-
const tls = tslib_1.__importStar(require("tls"));
|
|
11
10
|
const axios_1 = tslib_1.__importDefault(require("axios"));
|
|
12
11
|
const child_process_1 = require("child_process");
|
|
13
12
|
// Lazy-load mime module to support ESM
|
|
@@ -21,94 +20,6 @@ async function getMime() {
|
|
|
21
20
|
return mimeModule;
|
|
22
21
|
}
|
|
23
22
|
class TestLensReporter {
|
|
24
|
-
/**
|
|
25
|
-
* Get bundled CA certificates for TestLens
|
|
26
|
-
* Combines custom CA bundle with Node.js root certificates
|
|
27
|
-
* This ensures SSL works with both testlens.qa-path.com and other HTTPS endpoints
|
|
28
|
-
*/
|
|
29
|
-
static getBundledCaCertificates() {
|
|
30
|
-
const allCerts = [];
|
|
31
|
-
// First, add our bundled TestLens CA certificate chain
|
|
32
|
-
try {
|
|
33
|
-
const certPath = path.join(__dirname, 'testlens-ca-bundle.pem');
|
|
34
|
-
if (fs.existsSync(certPath)) {
|
|
35
|
-
const certData = fs.readFileSync(certPath, 'utf8');
|
|
36
|
-
// Split the bundle into individual certificates
|
|
37
|
-
const certs = certData.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
|
|
38
|
-
if (certs && certs.length > 0) {
|
|
39
|
-
const buffers = certs.map(cert => Buffer.from(cert, 'utf8'));
|
|
40
|
-
allCerts.push(...buffers);
|
|
41
|
-
if (process.env.DEBUG) {
|
|
42
|
-
console.log(`✓ Loaded bundled TestLens CA certificates (${buffers.length} certificate(s))`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
if (process.env.DEBUG) {
|
|
49
|
-
console.log('⚠️ Bundled CA certificate not available');
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// Then, add Node.js built-in root certificates for general SSL support
|
|
53
|
-
try {
|
|
54
|
-
if (tls.rootCertificates && Array.isArray(tls.rootCertificates) && tls.rootCertificates.length > 0) {
|
|
55
|
-
const rootCerts = tls.rootCertificates.map(cert => Buffer.from(cert, 'utf8'));
|
|
56
|
-
allCerts.push(...rootCerts);
|
|
57
|
-
if (process.env.DEBUG) {
|
|
58
|
-
console.log(`✓ Added Node.js built-in root certificates (${rootCerts.length} certificates)`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
if (process.env.DEBUG) {
|
|
64
|
-
console.log('⚠️ Node.js built-in root certificates not available');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return allCerts.length > 0 ? allCerts : undefined;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Automatically detect and load system CA certificates
|
|
71
|
-
* Uses bundled certificates as primary source, falls back to system certificates
|
|
72
|
-
*/
|
|
73
|
-
static getSystemCaCertificates() {
|
|
74
|
-
// First, try to use our bundled certificates (Node.js rootCertificates)
|
|
75
|
-
const bundledCerts = TestLensReporter.getBundledCaCertificates();
|
|
76
|
-
if (bundledCerts && bundledCerts.length > 0) {
|
|
77
|
-
return bundledCerts;
|
|
78
|
-
}
|
|
79
|
-
// Fallback: Try to load from file system (for older Node.js versions or special cases)
|
|
80
|
-
const platform = os.platform();
|
|
81
|
-
const certificates = [];
|
|
82
|
-
const certPaths = [];
|
|
83
|
-
if (platform === 'darwin') {
|
|
84
|
-
// macOS: Common certificate locations
|
|
85
|
-
certPaths.push('/etc/ssl/cert.pem', '/usr/local/etc/openssl/cert.pem', '/opt/homebrew/etc/openssl/cert.pem', '/System/Library/OpenSSL/certs/cert.pem');
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
// Linux and other Unix-like systems
|
|
89
|
-
certPaths.push('/etc/ssl/certs/ca-certificates.crt', '/etc/ssl/certs/ca-bundle.crt', '/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/ca-bundle.pem', '/usr/share/ssl/certs/ca-bundle.crt', '/usr/local/share/certs/ca-root-nss.crt', '/etc/ca-certificates/extracted/tls-ca-bundle.pem');
|
|
90
|
-
}
|
|
91
|
-
// Try to load certificates from common locations
|
|
92
|
-
for (const certPath of certPaths) {
|
|
93
|
-
try {
|
|
94
|
-
if (certPath && fs.existsSync(certPath)) {
|
|
95
|
-
const certData = fs.readFileSync(certPath);
|
|
96
|
-
certificates.push(certData);
|
|
97
|
-
// Only log in debug mode to avoid noise
|
|
98
|
-
if (process.env.DEBUG) {
|
|
99
|
-
console.log(`✓ Loaded CA certificates from: ${certPath}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
// Silently continue if a certificate file can't be read
|
|
105
|
-
// This is expected as not all paths will exist on every system
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Return undefined if no certificates found (let Node.js use defaults)
|
|
109
|
-
// Return certificates array if we found any
|
|
110
|
-
return certificates.length > 0 ? certificates : undefined;
|
|
111
|
-
}
|
|
112
23
|
/**
|
|
113
24
|
* Parse custom metadata from environment variables
|
|
114
25
|
* Checks for common metadata environment variables
|
|
@@ -149,6 +60,7 @@ class TestLensReporter {
|
|
|
149
60
|
constructor(options) {
|
|
150
61
|
this.runCreationFailed = false; // Track if run creation failed due to limits
|
|
151
62
|
this.cliArgs = {}; // Store CLI args separately
|
|
63
|
+
this.pendingUploads = new Set(); // Track pending artifact uploads
|
|
152
64
|
// Parse custom CLI arguments
|
|
153
65
|
const customArgs = TestLensReporter.parseCustomArgs();
|
|
154
66
|
this.cliArgs = customArgs; // Store CLI args separately for later use
|
|
@@ -186,46 +98,6 @@ class TestLensReporter {
|
|
|
186
98
|
}
|
|
187
99
|
// Determine SSL validation behavior
|
|
188
100
|
let rejectUnauthorized = true; // Default to secure
|
|
189
|
-
let ca = undefined;
|
|
190
|
-
// Use bundled CA certificates as primary source (Node.js rootCertificates)
|
|
191
|
-
// This ensures consistent behavior across all platforms
|
|
192
|
-
const bundledCerts = TestLensReporter.getBundledCaCertificates();
|
|
193
|
-
// Load custom CA certificate if explicitly provided (for advanced users)
|
|
194
|
-
// Custom certificates will be combined with bundled certificates
|
|
195
|
-
if (this.config.caCertificate) {
|
|
196
|
-
try {
|
|
197
|
-
if (fs.existsSync(this.config.caCertificate)) {
|
|
198
|
-
const customCert = fs.readFileSync(this.config.caCertificate);
|
|
199
|
-
// Combine bundled certs with custom cert
|
|
200
|
-
if (bundledCerts && Array.isArray(bundledCerts) && bundledCerts.length > 0) {
|
|
201
|
-
ca = [...bundledCerts, customCert];
|
|
202
|
-
console.log(`✓ Using bundled CA certificates + custom certificate: ${this.config.caCertificate}`);
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
ca = customCert;
|
|
206
|
-
console.log(`✓ Using custom CA certificate: ${this.config.caCertificate}`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
console.warn(`⚠️ CA certificate file not found: ${this.config.caCertificate}`);
|
|
211
|
-
// Fall back to bundled certs if custom cert not found
|
|
212
|
-
ca = bundledCerts || undefined;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
console.warn(`⚠️ Failed to read CA certificate file: ${this.config.caCertificate}`, error.message);
|
|
217
|
-
// Fall back to bundled certs if custom cert read fails
|
|
218
|
-
ca = bundledCerts || undefined;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
// Use bundled certificates as primary source
|
|
223
|
-
// This works reliably across all platforms (Windows, macOS, Linux)
|
|
224
|
-
ca = bundledCerts || undefined;
|
|
225
|
-
if (ca && process.env.DEBUG) {
|
|
226
|
-
console.log(`✓ Using bundled CA certificates (${Array.isArray(ca) ? ca.length : 1} certificates)`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
101
|
// Check various ways SSL validation can be disabled (in order of precedence)
|
|
230
102
|
if (this.config.ignoreSslErrors) {
|
|
231
103
|
// Explicit configuration option
|
|
@@ -243,24 +115,6 @@ class TestLensReporter {
|
|
|
243
115
|
console.log('⚠️ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
244
116
|
}
|
|
245
117
|
// Set up axios instance with retry logic and enhanced SSL handling
|
|
246
|
-
const httpsAgentOptions = {
|
|
247
|
-
rejectUnauthorized: rejectUnauthorized,
|
|
248
|
-
// Allow any TLS version for better compatibility
|
|
249
|
-
minVersion: 'TLSv1.2',
|
|
250
|
-
maxVersion: 'TLSv1.3'
|
|
251
|
-
};
|
|
252
|
-
// Add CA certificates if available
|
|
253
|
-
// On Windows, ca will be undefined to let Node.js use Windows certificate store automatically
|
|
254
|
-
// On Unix systems, ca will contain certificates if found, or undefined to use Node.js defaults
|
|
255
|
-
if (ca !== undefined) {
|
|
256
|
-
if (Array.isArray(ca) && ca.length > 0) {
|
|
257
|
-
httpsAgentOptions.ca = ca;
|
|
258
|
-
}
|
|
259
|
-
else if (typeof ca === 'string' || Buffer.isBuffer(ca)) {
|
|
260
|
-
httpsAgentOptions.ca = ca;
|
|
261
|
-
}
|
|
262
|
-
// If ca is undefined, we don't set it, allowing Node.js to use its default certificate store
|
|
263
|
-
}
|
|
264
118
|
this.axiosInstance = axios_1.default.create({
|
|
265
119
|
baseURL: this.config.apiEndpoint,
|
|
266
120
|
timeout: this.config.timeout,
|
|
@@ -269,7 +123,12 @@ class TestLensReporter {
|
|
|
269
123
|
...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
|
|
270
124
|
},
|
|
271
125
|
// Enhanced SSL handling with flexible TLS configuration
|
|
272
|
-
httpsAgent: new https.Agent(
|
|
126
|
+
httpsAgent: new https.Agent({
|
|
127
|
+
rejectUnauthorized: rejectUnauthorized,
|
|
128
|
+
// Allow any TLS version for better compatibility
|
|
129
|
+
minVersion: 'TLSv1.2',
|
|
130
|
+
maxVersion: 'TLSv1.3'
|
|
131
|
+
})
|
|
273
132
|
});
|
|
274
133
|
// Add retry interceptor
|
|
275
134
|
this.axiosInstance.interceptors.response.use((response) => response, async (error) => {
|
|
@@ -308,7 +167,8 @@ class TestLensReporter {
|
|
|
308
167
|
browser: 'multiple',
|
|
309
168
|
os: `${os.type()} ${os.release()}`,
|
|
310
169
|
playwrightVersion: this.getPlaywrightVersion(),
|
|
311
|
-
nodeVersion: process.version
|
|
170
|
+
nodeVersion: process.version,
|
|
171
|
+
testlensVersion: this.getTestLensVersion()
|
|
312
172
|
};
|
|
313
173
|
// Add custom metadata if provided
|
|
314
174
|
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
@@ -331,6 +191,15 @@ class TestLensReporter {
|
|
|
331
191
|
return 'unknown';
|
|
332
192
|
}
|
|
333
193
|
}
|
|
194
|
+
getTestLensVersion() {
|
|
195
|
+
try {
|
|
196
|
+
const testlensPackage = require('./package.json');
|
|
197
|
+
return testlensPackage.version;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return 'unknown';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
334
203
|
normalizeTestStatus(status) {
|
|
335
204
|
// Treat timeout as failed for consistency with analytics
|
|
336
205
|
if (status === 'timedOut') {
|
|
@@ -607,23 +476,20 @@ class TestLensReporter {
|
|
|
607
476
|
}
|
|
608
477
|
return testError;
|
|
609
478
|
});
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
// Pass test case DB ID if available for faster lookups
|
|
625
|
-
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
626
|
-
}
|
|
479
|
+
// Send testEnd event for all tests, regardless of status
|
|
480
|
+
// This ensures tests that are interrupted or have unexpected statuses are properly recorded
|
|
481
|
+
console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
482
|
+
// Send test end event to API and get response
|
|
483
|
+
const testEndResponse = await this.sendToApi({
|
|
484
|
+
type: 'testEnd',
|
|
485
|
+
runId: this.runId,
|
|
486
|
+
timestamp: new Date().toISOString(),
|
|
487
|
+
test: testData
|
|
488
|
+
});
|
|
489
|
+
// Handle artifacts (test case is now guaranteed to be in database)
|
|
490
|
+
if (this.config.enableArtifacts) {
|
|
491
|
+
// Pass test case DB ID if available for faster lookups
|
|
492
|
+
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
627
493
|
}
|
|
628
494
|
}
|
|
629
495
|
// Update spec status
|
|
@@ -639,12 +505,33 @@ class TestLensReporter {
|
|
|
639
505
|
specData.status = 'skipped';
|
|
640
506
|
}
|
|
641
507
|
// Check if all tests in spec are complete
|
|
508
|
+
// Only consider tests that were actually executed (have testData)
|
|
642
509
|
const remainingTests = test.parent.tests.filter((t) => {
|
|
643
510
|
const tId = this.getTestId(t);
|
|
644
511
|
const tData = this.testMap.get(tId);
|
|
645
|
-
|
|
512
|
+
// If testData exists but no endTime, it's still running
|
|
513
|
+
return tData && !tData.endTime;
|
|
646
514
|
});
|
|
647
515
|
if (remainingTests.length === 0) {
|
|
516
|
+
// Determine final spec status based on all executed tests
|
|
517
|
+
const executedTests = test.parent.tests
|
|
518
|
+
.map((t) => {
|
|
519
|
+
const tId = this.getTestId(t);
|
|
520
|
+
return this.testMap.get(tId);
|
|
521
|
+
})
|
|
522
|
+
.filter((tData) => !!tData);
|
|
523
|
+
if (executedTests.length > 0) {
|
|
524
|
+
const allTestStatuses = executedTests.map(tData => tData.status);
|
|
525
|
+
if (allTestStatuses.every(status => status === 'passed')) {
|
|
526
|
+
specData.status = 'passed';
|
|
527
|
+
}
|
|
528
|
+
else if (allTestStatuses.some(status => status === 'failed')) {
|
|
529
|
+
specData.status = 'failed';
|
|
530
|
+
}
|
|
531
|
+
else if (allTestStatuses.every(status => status === 'skipped')) {
|
|
532
|
+
specData.status = 'skipped';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
648
535
|
// Aggregate tags from all tests in this spec
|
|
649
536
|
const allTags = new Set();
|
|
650
537
|
test.parent.tests.forEach((t) => {
|
|
@@ -672,6 +559,17 @@ class TestLensReporter {
|
|
|
672
559
|
async onEnd(result) {
|
|
673
560
|
this.runMetadata.endTime = new Date().toISOString();
|
|
674
561
|
this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
|
|
562
|
+
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
563
|
+
if (this.pendingUploads.size > 0) {
|
|
564
|
+
console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
565
|
+
try {
|
|
566
|
+
await Promise.all(Array.from(this.pendingUploads));
|
|
567
|
+
console.log(`✅ All artifact uploads completed`);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
675
573
|
// Calculate final stats
|
|
676
574
|
const totalTests = Array.from(this.testMap.values()).length;
|
|
677
575
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
@@ -789,68 +687,17 @@ class TestLensReporter {
|
|
|
789
687
|
console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
790
688
|
}
|
|
791
689
|
}
|
|
792
|
-
else {
|
|
793
|
-
//
|
|
794
|
-
|
|
795
|
-
error?.
|
|
796
|
-
|
|
797
|
-
error?.
|
|
798
|
-
|
|
799
|
-
error?.
|
|
800
|
-
error?.
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
console.error('❌ SSL Certificate Error');
|
|
804
|
-
console.error('='.repeat(80));
|
|
805
|
-
console.error('');
|
|
806
|
-
console.error(`Failed to send ${payload.type} event to TestLens due to SSL certificate issue.`);
|
|
807
|
-
console.error('');
|
|
808
|
-
console.error('The reporter automatically attempts to detect and use system CA certificates.');
|
|
809
|
-
console.error('If this error persists, it may indicate:');
|
|
810
|
-
console.error(' - Missing or outdated system CA certificates');
|
|
811
|
-
console.error(' - Corporate proxy with custom CA certificate');
|
|
812
|
-
console.error(' - Incomplete certificate chain from the server');
|
|
813
|
-
console.error('');
|
|
814
|
-
console.error('Error details:');
|
|
815
|
-
console.error(` Code: ${error?.code || 'Unknown'}`);
|
|
816
|
-
console.error(` Message: ${error?.message || 'Unknown error'}`);
|
|
817
|
-
console.error('');
|
|
818
|
-
console.error('Possible solutions:');
|
|
819
|
-
console.error('');
|
|
820
|
-
console.error('1. Update your system\'s CA certificate store:');
|
|
821
|
-
console.error(' - Windows: Update Windows root certificates via Windows Update');
|
|
822
|
-
console.error(' - macOS: Run: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <certificate.pem>');
|
|
823
|
-
console.error(' - Linux: Update ca-certificates package: sudo apt-get update && sudo apt-get install ca-certificates');
|
|
824
|
-
console.error('');
|
|
825
|
-
console.error('2. If you have a custom CA certificate file (e.g., from corporate proxy),');
|
|
826
|
-
console.error(' you can specify it in your config (optional):');
|
|
827
|
-
console.error(' reporter: [');
|
|
828
|
-
console.error(' [\'@testlens/playwright-reporter\', {');
|
|
829
|
-
console.error(' caCertificate: \'/path/to/your/ca-certificate.pem\'');
|
|
830
|
-
console.error(' }]');
|
|
831
|
-
console.error(' ]');
|
|
832
|
-
console.error('');
|
|
833
|
-
console.error('3. Contact your network administrator if you\'re behind a corporate proxy');
|
|
834
|
-
console.error(' that uses a custom CA certificate.');
|
|
835
|
-
console.error('');
|
|
836
|
-
console.error('⚠️ WARNING: Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables SSL verification');
|
|
837
|
-
console.error(' and is insecure. Only use this as a last resort in development.');
|
|
838
|
-
console.error('');
|
|
839
|
-
console.error('='.repeat(80));
|
|
840
|
-
console.error('');
|
|
841
|
-
}
|
|
842
|
-
else if (status !== 403) {
|
|
843
|
-
// Log other errors (but not 403 which we handled above)
|
|
844
|
-
console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
|
|
845
|
-
message: error?.message || 'Unknown error',
|
|
846
|
-
status: status,
|
|
847
|
-
statusText: error?.response?.statusText,
|
|
848
|
-
data: errorData,
|
|
849
|
-
code: error?.code,
|
|
850
|
-
url: error?.config?.url,
|
|
851
|
-
method: error?.config?.method
|
|
852
|
-
});
|
|
853
|
-
}
|
|
690
|
+
else if (status !== 403) {
|
|
691
|
+
// Log other errors (but not 403 which we handled above)
|
|
692
|
+
console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
|
|
693
|
+
message: error?.message || 'Unknown error',
|
|
694
|
+
status: status,
|
|
695
|
+
statusText: error?.response?.statusText,
|
|
696
|
+
data: errorData,
|
|
697
|
+
code: error?.code,
|
|
698
|
+
url: error?.config?.url,
|
|
699
|
+
method: error?.config?.method
|
|
700
|
+
});
|
|
854
701
|
}
|
|
855
702
|
// Don't throw error to avoid breaking test execution
|
|
856
703
|
}
|
|
@@ -908,34 +755,53 @@ class TestLensReporter {
|
|
|
908
755
|
}
|
|
909
756
|
}
|
|
910
757
|
// Upload to S3 first (pass DB ID if available for faster lookup)
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
758
|
+
// Create upload promise that we can track
|
|
759
|
+
const uploadPromise = Promise.resolve().then(async () => {
|
|
760
|
+
try {
|
|
761
|
+
if (!attachment.path) {
|
|
762
|
+
console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
|
|
766
|
+
// Skip if upload failed or file was too large
|
|
767
|
+
if (!s3Data) {
|
|
768
|
+
console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const artifactData = {
|
|
772
|
+
testId,
|
|
773
|
+
type: this.getArtifactType(attachment.name),
|
|
774
|
+
path: attachment.path,
|
|
775
|
+
name: fileName,
|
|
776
|
+
contentType: attachment.contentType,
|
|
777
|
+
fileSize: this.getFileSize(attachment.path),
|
|
778
|
+
storageType: 's3',
|
|
779
|
+
s3Key: s3Data.key,
|
|
780
|
+
s3Url: s3Data.url
|
|
781
|
+
};
|
|
782
|
+
// Send artifact data to API
|
|
783
|
+
await this.sendToApi({
|
|
784
|
+
type: 'artifact',
|
|
785
|
+
runId: this.runId,
|
|
786
|
+
timestamp: new Date().toISOString(),
|
|
787
|
+
artifact: artifactData
|
|
788
|
+
});
|
|
789
|
+
console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, error.message);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
// Track this upload and ensure cleanup on completion
|
|
796
|
+
this.pendingUploads.add(uploadPromise);
|
|
797
|
+
uploadPromise.finally(() => {
|
|
798
|
+
this.pendingUploads.delete(uploadPromise);
|
|
934
799
|
});
|
|
935
|
-
|
|
800
|
+
// Don't await here - let uploads happen in parallel
|
|
801
|
+
// They will be awaited in onEnd
|
|
936
802
|
}
|
|
937
803
|
catch (error) {
|
|
938
|
-
console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to
|
|
804
|
+
console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, error.message);
|
|
939
805
|
}
|
|
940
806
|
}
|
|
941
807
|
}
|
|
@@ -953,8 +819,13 @@ class TestLensReporter {
|
|
|
953
819
|
describe: block.describe // parent describe block name
|
|
954
820
|
}));
|
|
955
821
|
// Send to dedicated spec code blocks API endpoint
|
|
956
|
-
|
|
957
|
-
|
|
822
|
+
// Extract base URL - handle both full and partial endpoint patterns
|
|
823
|
+
let baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
824
|
+
if (baseUrl === this.config.apiEndpoint) {
|
|
825
|
+
// Fallback: try alternative pattern if main pattern didn't match
|
|
826
|
+
baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
|
|
827
|
+
}
|
|
828
|
+
const specEndpoint = `${baseUrl}/api/v1/webhook/playwright/spec-code-blocks`;
|
|
958
829
|
await this.axiosInstance.post(specEndpoint, {
|
|
959
830
|
filePath: path.relative(process.cwd(), specPath),
|
|
960
831
|
codeBlocks,
|
|
@@ -969,58 +840,7 @@ class TestLensReporter {
|
|
|
969
840
|
console.log(`ℹ️ Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
970
841
|
return;
|
|
971
842
|
}
|
|
972
|
-
|
|
973
|
-
const isSslError = error?.code === 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY' ||
|
|
974
|
-
error?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
|
|
975
|
-
error?.code === 'CERT_HAS_EXPIRED' ||
|
|
976
|
-
error?.code === 'SELF_SIGNED_CERT_IN_CHAIN' ||
|
|
977
|
-
error?.message?.includes('certificate') ||
|
|
978
|
-
error?.message?.includes('SSL') ||
|
|
979
|
-
error?.message?.includes('TLS');
|
|
980
|
-
if (isSslError) {
|
|
981
|
-
console.error('\n' + '='.repeat(80));
|
|
982
|
-
console.error('❌ SSL Certificate Error');
|
|
983
|
-
console.error('='.repeat(80));
|
|
984
|
-
console.error('');
|
|
985
|
-
console.error('Failed to send spec code blocks due to SSL certificate issue.');
|
|
986
|
-
console.error('');
|
|
987
|
-
console.error('The reporter automatically attempts to detect and use system CA certificates.');
|
|
988
|
-
console.error('If this error persists, it may indicate:');
|
|
989
|
-
console.error(' - Missing or outdated system CA certificates');
|
|
990
|
-
console.error(' - Corporate proxy with custom CA certificate');
|
|
991
|
-
console.error(' - Incomplete certificate chain from the server');
|
|
992
|
-
console.error('');
|
|
993
|
-
console.error('Error details:');
|
|
994
|
-
console.error(` Code: ${error?.code || 'Unknown'}`);
|
|
995
|
-
console.error(` Message: ${error?.message || 'Unknown error'}`);
|
|
996
|
-
console.error('');
|
|
997
|
-
console.error('Possible solutions:');
|
|
998
|
-
console.error('');
|
|
999
|
-
console.error('1. Update your system\'s CA certificate store:');
|
|
1000
|
-
console.error(' - Windows: Update Windows root certificates via Windows Update');
|
|
1001
|
-
console.error(' - macOS: Run: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <certificate.pem>');
|
|
1002
|
-
console.error(' - Linux: Update ca-certificates package: sudo apt-get update && sudo apt-get install ca-certificates');
|
|
1003
|
-
console.error('');
|
|
1004
|
-
console.error('2. If you have a custom CA certificate file (e.g., from corporate proxy),');
|
|
1005
|
-
console.error(' you can specify it in your config (optional):');
|
|
1006
|
-
console.error(' reporter: [');
|
|
1007
|
-
console.error(' [\'@testlens/playwright-reporter\', {');
|
|
1008
|
-
console.error(' caCertificate: \'/path/to/your/ca-certificate.pem\'');
|
|
1009
|
-
console.error(' }]');
|
|
1010
|
-
console.error(' ]');
|
|
1011
|
-
console.error('');
|
|
1012
|
-
console.error('3. Contact your network administrator if you\'re behind a corporate proxy');
|
|
1013
|
-
console.error(' that uses a custom CA certificate.');
|
|
1014
|
-
console.error('');
|
|
1015
|
-
console.error('⚠️ WARNING: Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables SSL verification');
|
|
1016
|
-
console.error(' and is insecure. Only use this as a last resort in development.');
|
|
1017
|
-
console.error('');
|
|
1018
|
-
console.error('='.repeat(80));
|
|
1019
|
-
console.error('');
|
|
1020
|
-
}
|
|
1021
|
-
else {
|
|
1022
|
-
console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
|
|
1023
|
-
}
|
|
843
|
+
console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
|
|
1024
844
|
}
|
|
1025
845
|
}
|
|
1026
846
|
extractTestBlocks(filePath) {
|
|
@@ -1211,8 +1031,8 @@ class TestLensReporter {
|
|
|
1211
1031
|
maxContentLength: Infinity,
|
|
1212
1032
|
maxBodyLength: Infinity,
|
|
1213
1033
|
timeout: Math.max(600000, Math.ceil(fileSize / (1024 * 1024)) * 10000), // 10s per MB, min 10 minutes - increased for large trace files
|
|
1214
|
-
//
|
|
1215
|
-
httpsAgent:
|
|
1034
|
+
// Don't use custom HTTPS agent for S3 uploads
|
|
1035
|
+
httpsAgent: undefined
|
|
1216
1036
|
});
|
|
1217
1037
|
if (uploadResponse.status !== 200) {
|
|
1218
1038
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
@@ -1258,7 +1078,7 @@ class TestLensReporter {
|
|
|
1258
1078
|
const errorData = error?.response?.data;
|
|
1259
1079
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1260
1080
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1261
|
-
console.error('
|
|
1081
|
+
console.error('\n' + '='.repeat(80));
|
|
1262
1082
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1263
1083
|
console.error('❌ TESTLENS ERROR: Test Cases Limit Reached');
|
|
1264
1084
|
}
|
|
@@ -1337,24 +1157,8 @@ class TestLensReporter {
|
|
|
1337
1157
|
};
|
|
1338
1158
|
return contentTypes[ext] || 'application/octet-stream';
|
|
1339
1159
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const safeTestId = this.sanitizeForS3(testId);
|
|
1343
|
-
const safeFileName = this.sanitizeForS3(fileName);
|
|
1344
|
-
const ext = path.extname(fileName);
|
|
1345
|
-
const baseName = path.basename(fileName, ext);
|
|
1346
|
-
return `test-artifacts/${date}/${runId}/${safeTestId}/${safeFileName}${ext}`;
|
|
1347
|
-
}
|
|
1348
|
-
sanitizeForS3(value) {
|
|
1349
|
-
return value
|
|
1350
|
-
.replace(/[\/:*?"<>|]/g, '-')
|
|
1351
|
-
.replace(/[-\u001f\u007f]/g, '-')
|
|
1352
|
-
.replace(/[^-~]/g, '-')
|
|
1353
|
-
.replace(/\s+/g, '-')
|
|
1354
|
-
.replace(/[_]/g, '-')
|
|
1355
|
-
.replace(/-+/g, '-')
|
|
1356
|
-
.replace(/^-|-$/g, '');
|
|
1357
|
-
}
|
|
1160
|
+
// Note: S3 key generation and sanitization are handled server-side
|
|
1161
|
+
// generateS3Key() and sanitizeForS3() methods removed as they were not used
|
|
1358
1162
|
getFileSize(filePath) {
|
|
1359
1163
|
try {
|
|
1360
1164
|
const stats = fs.statSync(filePath);
|