@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.ts
CHANGED
|
@@ -3,7 +3,6 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as https from 'https';
|
|
6
|
-
import * as tls from 'tls';
|
|
7
6
|
import axios, { AxiosInstance } from 'axios';
|
|
8
7
|
import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
|
|
9
8
|
import { execSync } from 'child_process';
|
|
@@ -47,8 +46,6 @@ export interface TestLensReporterConfig {
|
|
|
47
46
|
rejectUnauthorized?: boolean;
|
|
48
47
|
/** Alternative SSL option - set to true to ignore SSL certificate errors */
|
|
49
48
|
ignoreSslErrors?: boolean;
|
|
50
|
-
/** Path to a custom CA certificate file (PEM format) to use for SSL verification */
|
|
51
|
-
caCertificate?: string;
|
|
52
49
|
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
53
50
|
customMetadata?: Record<string, string | string[]>;
|
|
54
51
|
}
|
|
@@ -80,8 +77,6 @@ export interface TestLensReporterOptions {
|
|
|
80
77
|
rejectUnauthorized?: boolean;
|
|
81
78
|
/** Alternative SSL option - set to true to ignore SSL certificate errors */
|
|
82
79
|
ignoreSslErrors?: boolean;
|
|
83
|
-
/** Path to a custom CA certificate file (PEM format) to use for SSL verification */
|
|
84
|
-
caCertificate?: string;
|
|
85
80
|
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
86
81
|
customMetadata?: Record<string, string | string[]>;
|
|
87
82
|
}
|
|
@@ -118,6 +113,7 @@ export interface RunMetadata {
|
|
|
118
113
|
os: string;
|
|
119
114
|
playwrightVersion: string;
|
|
120
115
|
nodeVersion: string;
|
|
116
|
+
testlensVersion?: string;
|
|
121
117
|
gitInfo?: GitInfo | null;
|
|
122
118
|
shardInfo?: {
|
|
123
119
|
current: number;
|
|
@@ -183,118 +179,13 @@ export interface SpecData {
|
|
|
183
179
|
export class TestLensReporter implements Reporter {
|
|
184
180
|
private config: Required<TestLensReporterConfig>;
|
|
185
181
|
private axiosInstance: AxiosInstance;
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Get bundled CA certificates for TestLens
|
|
189
|
-
* Combines custom CA bundle with Node.js root certificates
|
|
190
|
-
* This ensures SSL works with both testlens.qa-path.com and other HTTPS endpoints
|
|
191
|
-
*/
|
|
192
|
-
private static getBundledCaCertificates(): Buffer[] | undefined {
|
|
193
|
-
const allCerts: Buffer[] = [];
|
|
194
|
-
|
|
195
|
-
// First, add our bundled TestLens CA certificate chain
|
|
196
|
-
try {
|
|
197
|
-
const certPath = path.join(__dirname, 'testlens-ca-bundle.pem');
|
|
198
|
-
if (fs.existsSync(certPath)) {
|
|
199
|
-
const certData = fs.readFileSync(certPath, 'utf8');
|
|
200
|
-
// Split the bundle into individual certificates
|
|
201
|
-
const certs = certData.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
|
|
202
|
-
if (certs && certs.length > 0) {
|
|
203
|
-
const buffers = certs.map(cert => Buffer.from(cert, 'utf8'));
|
|
204
|
-
allCerts.push(...buffers);
|
|
205
|
-
if (process.env.DEBUG) {
|
|
206
|
-
console.log(`✓ Loaded bundled TestLens CA certificates (${buffers.length} certificate(s))`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
} catch (error) {
|
|
211
|
-
if (process.env.DEBUG) {
|
|
212
|
-
console.log('⚠️ Bundled CA certificate not available');
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Then, add Node.js built-in root certificates for general SSL support
|
|
217
|
-
try {
|
|
218
|
-
if (tls.rootCertificates && Array.isArray(tls.rootCertificates) && tls.rootCertificates.length > 0) {
|
|
219
|
-
const rootCerts = tls.rootCertificates.map(cert => Buffer.from(cert, 'utf8'));
|
|
220
|
-
allCerts.push(...rootCerts);
|
|
221
|
-
if (process.env.DEBUG) {
|
|
222
|
-
console.log(`✓ Added Node.js built-in root certificates (${rootCerts.length} certificates)`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
} catch (error) {
|
|
226
|
-
if (process.env.DEBUG) {
|
|
227
|
-
console.log('⚠️ Node.js built-in root certificates not available');
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return allCerts.length > 0 ? allCerts : undefined;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Automatically detect and load system CA certificates
|
|
236
|
-
* Uses bundled certificates as primary source, falls back to system certificates
|
|
237
|
-
*/
|
|
238
|
-
private static getSystemCaCertificates(): Buffer[] | undefined {
|
|
239
|
-
// First, try to use our bundled certificates (Node.js rootCertificates)
|
|
240
|
-
const bundledCerts = TestLensReporter.getBundledCaCertificates();
|
|
241
|
-
if (bundledCerts && bundledCerts.length > 0) {
|
|
242
|
-
return bundledCerts;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Fallback: Try to load from file system (for older Node.js versions or special cases)
|
|
246
|
-
const platform = os.platform();
|
|
247
|
-
const certificates: Buffer[] = [];
|
|
248
|
-
const certPaths: string[] = [];
|
|
249
|
-
|
|
250
|
-
if (platform === 'darwin') {
|
|
251
|
-
// macOS: Common certificate locations
|
|
252
|
-
certPaths.push(
|
|
253
|
-
'/etc/ssl/cert.pem',
|
|
254
|
-
'/usr/local/etc/openssl/cert.pem',
|
|
255
|
-
'/opt/homebrew/etc/openssl/cert.pem',
|
|
256
|
-
'/System/Library/OpenSSL/certs/cert.pem'
|
|
257
|
-
);
|
|
258
|
-
} else {
|
|
259
|
-
// Linux and other Unix-like systems
|
|
260
|
-
certPaths.push(
|
|
261
|
-
'/etc/ssl/certs/ca-certificates.crt',
|
|
262
|
-
'/etc/ssl/certs/ca-bundle.crt',
|
|
263
|
-
'/etc/pki/tls/certs/ca-bundle.crt',
|
|
264
|
-
'/etc/ssl/ca-bundle.pem',
|
|
265
|
-
'/usr/share/ssl/certs/ca-bundle.crt',
|
|
266
|
-
'/usr/local/share/certs/ca-root-nss.crt',
|
|
267
|
-
'/etc/ca-certificates/extracted/tls-ca-bundle.pem'
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Try to load certificates from common locations
|
|
272
|
-
for (const certPath of certPaths) {
|
|
273
|
-
try {
|
|
274
|
-
if (certPath && fs.existsSync(certPath)) {
|
|
275
|
-
const certData = fs.readFileSync(certPath);
|
|
276
|
-
certificates.push(certData);
|
|
277
|
-
// Only log in debug mode to avoid noise
|
|
278
|
-
if (process.env.DEBUG) {
|
|
279
|
-
console.log(`✓ Loaded CA certificates from: ${certPath}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
} catch (error) {
|
|
283
|
-
// Silently continue if a certificate file can't be read
|
|
284
|
-
// This is expected as not all paths will exist on every system
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Return undefined if no certificates found (let Node.js use defaults)
|
|
289
|
-
// Return certificates array if we found any
|
|
290
|
-
return certificates.length > 0 ? certificates : undefined;
|
|
291
|
-
}
|
|
292
182
|
private runId: string;
|
|
293
183
|
private runMetadata: RunMetadata;
|
|
294
184
|
private specMap: Map<string, SpecData>;
|
|
295
185
|
private testMap: Map<string, TestData>;
|
|
296
186
|
private runCreationFailed: boolean = false; // Track if run creation failed due to limits
|
|
297
187
|
private cliArgs: Record<string, any> = {}; // Store CLI args separately
|
|
188
|
+
private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
|
|
298
189
|
|
|
299
190
|
/**
|
|
300
191
|
* Parse custom metadata from environment variables
|
|
@@ -379,44 +270,6 @@ export class TestLensReporter implements Reporter {
|
|
|
379
270
|
|
|
380
271
|
// Determine SSL validation behavior
|
|
381
272
|
let rejectUnauthorized = true; // Default to secure
|
|
382
|
-
let ca: Buffer | string | Buffer[] | undefined = undefined;
|
|
383
|
-
|
|
384
|
-
// Use bundled CA certificates as primary source (Node.js rootCertificates)
|
|
385
|
-
// This ensures consistent behavior across all platforms
|
|
386
|
-
const bundledCerts = TestLensReporter.getBundledCaCertificates();
|
|
387
|
-
|
|
388
|
-
// Load custom CA certificate if explicitly provided (for advanced users)
|
|
389
|
-
// Custom certificates will be combined with bundled certificates
|
|
390
|
-
if (this.config.caCertificate) {
|
|
391
|
-
try {
|
|
392
|
-
if (fs.existsSync(this.config.caCertificate)) {
|
|
393
|
-
const customCert = fs.readFileSync(this.config.caCertificate);
|
|
394
|
-
// Combine bundled certs with custom cert
|
|
395
|
-
if (bundledCerts && Array.isArray(bundledCerts) && bundledCerts.length > 0) {
|
|
396
|
-
ca = [...bundledCerts, customCert];
|
|
397
|
-
console.log(`✓ Using bundled CA certificates + custom certificate: ${this.config.caCertificate}`);
|
|
398
|
-
} else {
|
|
399
|
-
ca = customCert;
|
|
400
|
-
console.log(`✓ Using custom CA certificate: ${this.config.caCertificate}`);
|
|
401
|
-
}
|
|
402
|
-
} else {
|
|
403
|
-
console.warn(`⚠️ CA certificate file not found: ${this.config.caCertificate}`);
|
|
404
|
-
// Fall back to bundled certs if custom cert not found
|
|
405
|
-
ca = bundledCerts || undefined;
|
|
406
|
-
}
|
|
407
|
-
} catch (error) {
|
|
408
|
-
console.warn(`⚠️ Failed to read CA certificate file: ${this.config.caCertificate}`, (error as Error).message);
|
|
409
|
-
// Fall back to bundled certs if custom cert read fails
|
|
410
|
-
ca = bundledCerts || undefined;
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
// Use bundled certificates as primary source
|
|
414
|
-
// This works reliably across all platforms (Windows, macOS, Linux)
|
|
415
|
-
ca = bundledCerts || undefined;
|
|
416
|
-
if (ca && process.env.DEBUG) {
|
|
417
|
-
console.log(`✓ Using bundled CA certificates (${Array.isArray(ca) ? ca.length : 1} certificates)`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
273
|
|
|
421
274
|
// Check various ways SSL validation can be disabled (in order of precedence)
|
|
422
275
|
if (this.config.ignoreSslErrors) {
|
|
@@ -434,25 +287,6 @@ export class TestLensReporter implements Reporter {
|
|
|
434
287
|
}
|
|
435
288
|
|
|
436
289
|
// Set up axios instance with retry logic and enhanced SSL handling
|
|
437
|
-
const httpsAgentOptions: https.AgentOptions = {
|
|
438
|
-
rejectUnauthorized: rejectUnauthorized,
|
|
439
|
-
// Allow any TLS version for better compatibility
|
|
440
|
-
minVersion: 'TLSv1.2',
|
|
441
|
-
maxVersion: 'TLSv1.3'
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
// Add CA certificates if available
|
|
445
|
-
// On Windows, ca will be undefined to let Node.js use Windows certificate store automatically
|
|
446
|
-
// On Unix systems, ca will contain certificates if found, or undefined to use Node.js defaults
|
|
447
|
-
if (ca !== undefined) {
|
|
448
|
-
if (Array.isArray(ca) && ca.length > 0) {
|
|
449
|
-
httpsAgentOptions.ca = ca;
|
|
450
|
-
} else if (typeof ca === 'string' || Buffer.isBuffer(ca)) {
|
|
451
|
-
httpsAgentOptions.ca = ca;
|
|
452
|
-
}
|
|
453
|
-
// If ca is undefined, we don't set it, allowing Node.js to use its default certificate store
|
|
454
|
-
}
|
|
455
|
-
|
|
456
290
|
this.axiosInstance = axios.create({
|
|
457
291
|
baseURL: this.config.apiEndpoint,
|
|
458
292
|
timeout: this.config.timeout,
|
|
@@ -461,7 +295,12 @@ export class TestLensReporter implements Reporter {
|
|
|
461
295
|
...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
|
|
462
296
|
},
|
|
463
297
|
// Enhanced SSL handling with flexible TLS configuration
|
|
464
|
-
httpsAgent: new https.Agent(
|
|
298
|
+
httpsAgent: new https.Agent({
|
|
299
|
+
rejectUnauthorized: rejectUnauthorized,
|
|
300
|
+
// Allow any TLS version for better compatibility
|
|
301
|
+
minVersion: 'TLSv1.2',
|
|
302
|
+
maxVersion: 'TLSv1.3'
|
|
303
|
+
})
|
|
465
304
|
});
|
|
466
305
|
|
|
467
306
|
// Add retry interceptor
|
|
@@ -510,7 +349,8 @@ export class TestLensReporter implements Reporter {
|
|
|
510
349
|
browser: 'multiple',
|
|
511
350
|
os: `${os.type()} ${os.release()}`,
|
|
512
351
|
playwrightVersion: this.getPlaywrightVersion(),
|
|
513
|
-
nodeVersion: process.version
|
|
352
|
+
nodeVersion: process.version,
|
|
353
|
+
testlensVersion: this.getTestLensVersion()
|
|
514
354
|
};
|
|
515
355
|
|
|
516
356
|
// Add custom metadata if provided
|
|
@@ -537,6 +377,15 @@ export class TestLensReporter implements Reporter {
|
|
|
537
377
|
}
|
|
538
378
|
}
|
|
539
379
|
|
|
380
|
+
private getTestLensVersion(): string {
|
|
381
|
+
try {
|
|
382
|
+
const testlensPackage = require('./package.json');
|
|
383
|
+
return testlensPackage.version;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
return 'unknown';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
540
389
|
private normalizeTestStatus(status: string): string {
|
|
541
390
|
// Treat timeout as failed for consistency with analytics
|
|
542
391
|
if (status === 'timedOut') {
|
|
@@ -846,25 +695,21 @@ export class TestLensReporter implements Reporter {
|
|
|
846
695
|
return testError;
|
|
847
696
|
});
|
|
848
697
|
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
timestamp: new Date().toISOString(),
|
|
860
|
-
test: testData
|
|
861
|
-
});
|
|
698
|
+
// Send testEnd event for all tests, regardless of status
|
|
699
|
+
// This ensures tests that are interrupted or have unexpected statuses are properly recorded
|
|
700
|
+
console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
701
|
+
// Send test end event to API and get response
|
|
702
|
+
const testEndResponse = await this.sendToApi({
|
|
703
|
+
type: 'testEnd',
|
|
704
|
+
runId: this.runId,
|
|
705
|
+
timestamp: new Date().toISOString(),
|
|
706
|
+
test: testData
|
|
707
|
+
});
|
|
862
708
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
709
|
+
// Handle artifacts (test case is now guaranteed to be in database)
|
|
710
|
+
if (this.config.enableArtifacts) {
|
|
711
|
+
// Pass test case DB ID if available for faster lookups
|
|
712
|
+
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
868
713
|
}
|
|
869
714
|
}
|
|
870
715
|
|
|
@@ -881,13 +726,33 @@ export class TestLensReporter implements Reporter {
|
|
|
881
726
|
}
|
|
882
727
|
|
|
883
728
|
// Check if all tests in spec are complete
|
|
729
|
+
// Only consider tests that were actually executed (have testData)
|
|
884
730
|
const remainingTests = test.parent.tests.filter((t: any) => {
|
|
885
731
|
const tId = this.getTestId(t);
|
|
886
732
|
const tData = this.testMap.get(tId);
|
|
887
|
-
|
|
733
|
+
// If testData exists but no endTime, it's still running
|
|
734
|
+
return tData && !tData.endTime;
|
|
888
735
|
});
|
|
889
736
|
|
|
890
737
|
if (remainingTests.length === 0) {
|
|
738
|
+
// Determine final spec status based on all executed tests
|
|
739
|
+
const executedTests = test.parent.tests
|
|
740
|
+
.map((t: any) => {
|
|
741
|
+
const tId = this.getTestId(t);
|
|
742
|
+
return this.testMap.get(tId);
|
|
743
|
+
})
|
|
744
|
+
.filter((tData: TestData | undefined): tData is TestData => !!tData);
|
|
745
|
+
|
|
746
|
+
if (executedTests.length > 0) {
|
|
747
|
+
const allTestStatuses = executedTests.map(tData => tData.status);
|
|
748
|
+
if (allTestStatuses.every(status => status === 'passed')) {
|
|
749
|
+
specData.status = 'passed';
|
|
750
|
+
} else if (allTestStatuses.some(status => status === 'failed')) {
|
|
751
|
+
specData.status = 'failed';
|
|
752
|
+
} else if (allTestStatuses.every(status => status === 'skipped')) {
|
|
753
|
+
specData.status = 'skipped';
|
|
754
|
+
}
|
|
755
|
+
}
|
|
891
756
|
// Aggregate tags from all tests in this spec
|
|
892
757
|
const allTags = new Set<string>();
|
|
893
758
|
test.parent.tests.forEach((t: any) => {
|
|
@@ -920,6 +785,17 @@ export class TestLensReporter implements Reporter {
|
|
|
920
785
|
this.runMetadata.endTime = new Date().toISOString();
|
|
921
786
|
this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
|
|
922
787
|
|
|
788
|
+
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
789
|
+
if (this.pendingUploads.size > 0) {
|
|
790
|
+
console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
791
|
+
try {
|
|
792
|
+
await Promise.all(Array.from(this.pendingUploads));
|
|
793
|
+
console.log(`✅ All artifact uploads completed`);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
923
799
|
// Calculate final stats
|
|
924
800
|
const totalTests = Array.from(this.testMap.values()).length;
|
|
925
801
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
@@ -1039,68 +915,17 @@ export class TestLensReporter implements Reporter {
|
|
|
1039
915
|
} else {
|
|
1040
916
|
console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
1041
917
|
}
|
|
1042
|
-
} else {
|
|
1043
|
-
//
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
console.error('\n' + '='.repeat(80));
|
|
1054
|
-
console.error('❌ SSL Certificate Error');
|
|
1055
|
-
console.error('='.repeat(80));
|
|
1056
|
-
console.error('');
|
|
1057
|
-
console.error(`Failed to send ${payload.type} event to TestLens due to SSL certificate issue.`);
|
|
1058
|
-
console.error('');
|
|
1059
|
-
console.error('The reporter automatically attempts to detect and use system CA certificates.');
|
|
1060
|
-
console.error('If this error persists, it may indicate:');
|
|
1061
|
-
console.error(' - Missing or outdated system CA certificates');
|
|
1062
|
-
console.error(' - Corporate proxy with custom CA certificate');
|
|
1063
|
-
console.error(' - Incomplete certificate chain from the server');
|
|
1064
|
-
console.error('');
|
|
1065
|
-
console.error('Error details:');
|
|
1066
|
-
console.error(` Code: ${error?.code || 'Unknown'}`);
|
|
1067
|
-
console.error(` Message: ${error?.message || 'Unknown error'}`);
|
|
1068
|
-
console.error('');
|
|
1069
|
-
console.error('Possible solutions:');
|
|
1070
|
-
console.error('');
|
|
1071
|
-
console.error('1. Update your system\'s CA certificate store:');
|
|
1072
|
-
console.error(' - Windows: Update Windows root certificates via Windows Update');
|
|
1073
|
-
console.error(' - macOS: Run: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <certificate.pem>');
|
|
1074
|
-
console.error(' - Linux: Update ca-certificates package: sudo apt-get update && sudo apt-get install ca-certificates');
|
|
1075
|
-
console.error('');
|
|
1076
|
-
console.error('2. If you have a custom CA certificate file (e.g., from corporate proxy),');
|
|
1077
|
-
console.error(' you can specify it in your config (optional):');
|
|
1078
|
-
console.error(' reporter: [');
|
|
1079
|
-
console.error(' [\'@testlens/playwright-reporter\', {');
|
|
1080
|
-
console.error(' caCertificate: \'/path/to/your/ca-certificate.pem\'');
|
|
1081
|
-
console.error(' }]');
|
|
1082
|
-
console.error(' ]');
|
|
1083
|
-
console.error('');
|
|
1084
|
-
console.error('3. Contact your network administrator if you\'re behind a corporate proxy');
|
|
1085
|
-
console.error(' that uses a custom CA certificate.');
|
|
1086
|
-
console.error('');
|
|
1087
|
-
console.error('⚠️ WARNING: Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables SSL verification');
|
|
1088
|
-
console.error(' and is insecure. Only use this as a last resort in development.');
|
|
1089
|
-
console.error('');
|
|
1090
|
-
console.error('='.repeat(80));
|
|
1091
|
-
console.error('');
|
|
1092
|
-
} else if (status !== 403) {
|
|
1093
|
-
// Log other errors (but not 403 which we handled above)
|
|
1094
|
-
console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
|
|
1095
|
-
message: error?.message || 'Unknown error',
|
|
1096
|
-
status: status,
|
|
1097
|
-
statusText: error?.response?.statusText,
|
|
1098
|
-
data: errorData,
|
|
1099
|
-
code: error?.code,
|
|
1100
|
-
url: error?.config?.url,
|
|
1101
|
-
method: error?.config?.method
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
918
|
+
} else if (status !== 403) {
|
|
919
|
+
// Log other errors (but not 403 which we handled above)
|
|
920
|
+
console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
|
|
921
|
+
message: error?.message || 'Unknown error',
|
|
922
|
+
status: status,
|
|
923
|
+
statusText: error?.response?.statusText,
|
|
924
|
+
data: errorData,
|
|
925
|
+
code: error?.code,
|
|
926
|
+
url: error?.config?.url,
|
|
927
|
+
method: error?.config?.method
|
|
928
|
+
});
|
|
1104
929
|
}
|
|
1105
930
|
|
|
1106
931
|
// Don't throw error to avoid breaking test execution
|
|
@@ -1166,37 +991,58 @@ export class TestLensReporter implements Reporter {
|
|
|
1166
991
|
}
|
|
1167
992
|
|
|
1168
993
|
// Upload to S3 first (pass DB ID if available for faster lookup)
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
994
|
+
// Create upload promise that we can track
|
|
995
|
+
const uploadPromise = Promise.resolve().then(async () => {
|
|
996
|
+
try {
|
|
997
|
+
if (!attachment.path) {
|
|
998
|
+
console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
|
|
1003
|
+
|
|
1004
|
+
// Skip if upload failed or file was too large
|
|
1005
|
+
if (!s3Data) {
|
|
1006
|
+
console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const artifactData = {
|
|
1011
|
+
testId,
|
|
1012
|
+
type: this.getArtifactType(attachment.name),
|
|
1013
|
+
path: attachment.path,
|
|
1014
|
+
name: fileName,
|
|
1015
|
+
contentType: attachment.contentType,
|
|
1016
|
+
fileSize: this.getFileSize(attachment.path),
|
|
1017
|
+
storageType: 's3',
|
|
1018
|
+
s3Key: s3Data.key,
|
|
1019
|
+
s3Url: s3Data.url
|
|
1020
|
+
};
|
|
1188
1021
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1022
|
+
// Send artifact data to API
|
|
1023
|
+
await this.sendToApi({
|
|
1024
|
+
type: 'artifact',
|
|
1025
|
+
runId: this.runId,
|
|
1026
|
+
timestamp: new Date().toISOString(),
|
|
1027
|
+
artifact: artifactData
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
|
|
1033
|
+
}
|
|
1195
1034
|
});
|
|
1196
1035
|
|
|
1197
|
-
|
|
1036
|
+
// Track this upload and ensure cleanup on completion
|
|
1037
|
+
this.pendingUploads.add(uploadPromise);
|
|
1038
|
+
uploadPromise.finally(() => {
|
|
1039
|
+
this.pendingUploads.delete(uploadPromise);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// Don't await here - let uploads happen in parallel
|
|
1043
|
+
// They will be awaited in onEnd
|
|
1198
1044
|
} catch (error) {
|
|
1199
|
-
console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to
|
|
1045
|
+
console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, (error as Error).message);
|
|
1200
1046
|
}
|
|
1201
1047
|
}
|
|
1202
1048
|
}
|
|
@@ -1217,8 +1063,13 @@ export class TestLensReporter implements Reporter {
|
|
|
1217
1063
|
}));
|
|
1218
1064
|
|
|
1219
1065
|
// Send to dedicated spec code blocks API endpoint
|
|
1220
|
-
|
|
1221
|
-
|
|
1066
|
+
// Extract base URL - handle both full and partial endpoint patterns
|
|
1067
|
+
let baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
1068
|
+
if (baseUrl === this.config.apiEndpoint) {
|
|
1069
|
+
// Fallback: try alternative pattern if main pattern didn't match
|
|
1070
|
+
baseUrl = this.config.apiEndpoint.replace('/webhook/playwright', '');
|
|
1071
|
+
}
|
|
1072
|
+
const specEndpoint = `${baseUrl}/api/v1/webhook/playwright/spec-code-blocks`;
|
|
1222
1073
|
|
|
1223
1074
|
await this.axiosInstance.post(specEndpoint, {
|
|
1224
1075
|
filePath: path.relative(process.cwd(), specPath),
|
|
@@ -1236,58 +1087,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1236
1087
|
return;
|
|
1237
1088
|
}
|
|
1238
1089
|
|
|
1239
|
-
|
|
1240
|
-
const isSslError = error?.code === 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY' ||
|
|
1241
|
-
error?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
|
|
1242
|
-
error?.code === 'CERT_HAS_EXPIRED' ||
|
|
1243
|
-
error?.code === 'SELF_SIGNED_CERT_IN_CHAIN' ||
|
|
1244
|
-
error?.message?.includes('certificate') ||
|
|
1245
|
-
error?.message?.includes('SSL') ||
|
|
1246
|
-
error?.message?.includes('TLS');
|
|
1247
|
-
|
|
1248
|
-
if (isSslError) {
|
|
1249
|
-
console.error('\n' + '='.repeat(80));
|
|
1250
|
-
console.error('❌ SSL Certificate Error');
|
|
1251
|
-
console.error('='.repeat(80));
|
|
1252
|
-
console.error('');
|
|
1253
|
-
console.error('Failed to send spec code blocks due to SSL certificate issue.');
|
|
1254
|
-
console.error('');
|
|
1255
|
-
console.error('The reporter automatically attempts to detect and use system CA certificates.');
|
|
1256
|
-
console.error('If this error persists, it may indicate:');
|
|
1257
|
-
console.error(' - Missing or outdated system CA certificates');
|
|
1258
|
-
console.error(' - Corporate proxy with custom CA certificate');
|
|
1259
|
-
console.error(' - Incomplete certificate chain from the server');
|
|
1260
|
-
console.error('');
|
|
1261
|
-
console.error('Error details:');
|
|
1262
|
-
console.error(` Code: ${error?.code || 'Unknown'}`);
|
|
1263
|
-
console.error(` Message: ${error?.message || 'Unknown error'}`);
|
|
1264
|
-
console.error('');
|
|
1265
|
-
console.error('Possible solutions:');
|
|
1266
|
-
console.error('');
|
|
1267
|
-
console.error('1. Update your system\'s CA certificate store:');
|
|
1268
|
-
console.error(' - Windows: Update Windows root certificates via Windows Update');
|
|
1269
|
-
console.error(' - macOS: Run: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <certificate.pem>');
|
|
1270
|
-
console.error(' - Linux: Update ca-certificates package: sudo apt-get update && sudo apt-get install ca-certificates');
|
|
1271
|
-
console.error('');
|
|
1272
|
-
console.error('2. If you have a custom CA certificate file (e.g., from corporate proxy),');
|
|
1273
|
-
console.error(' you can specify it in your config (optional):');
|
|
1274
|
-
console.error(' reporter: [');
|
|
1275
|
-
console.error(' [\'@testlens/playwright-reporter\', {');
|
|
1276
|
-
console.error(' caCertificate: \'/path/to/your/ca-certificate.pem\'');
|
|
1277
|
-
console.error(' }]');
|
|
1278
|
-
console.error(' ]');
|
|
1279
|
-
console.error('');
|
|
1280
|
-
console.error('3. Contact your network administrator if you\'re behind a corporate proxy');
|
|
1281
|
-
console.error(' that uses a custom CA certificate.');
|
|
1282
|
-
console.error('');
|
|
1283
|
-
console.error('⚠️ WARNING: Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables SSL verification');
|
|
1284
|
-
console.error(' and is insecure. Only use this as a last resort in development.');
|
|
1285
|
-
console.error('');
|
|
1286
|
-
console.error('='.repeat(80));
|
|
1287
|
-
console.error('');
|
|
1288
|
-
} else {
|
|
1289
|
-
console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
|
|
1290
|
-
}
|
|
1090
|
+
console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
|
|
1291
1091
|
}
|
|
1292
1092
|
}
|
|
1293
1093
|
|
|
@@ -1504,8 +1304,8 @@ export class TestLensReporter implements Reporter {
|
|
|
1504
1304
|
maxContentLength: Infinity,
|
|
1505
1305
|
maxBodyLength: Infinity,
|
|
1506
1306
|
timeout: Math.max(600000, Math.ceil(fileSize / (1024 * 1024)) * 10000), // 10s per MB, min 10 minutes - increased for large trace files
|
|
1507
|
-
//
|
|
1508
|
-
httpsAgent:
|
|
1307
|
+
// Don't use custom HTTPS agent for S3 uploads
|
|
1308
|
+
httpsAgent: undefined
|
|
1509
1309
|
});
|
|
1510
1310
|
|
|
1511
1311
|
if (uploadResponse.status !== 200) {
|
|
@@ -1556,7 +1356,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1556
1356
|
|
|
1557
1357
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1558
1358
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1559
|
-
console.error('
|
|
1359
|
+
console.error('\n' + '='.repeat(80));
|
|
1560
1360
|
|
|
1561
1361
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1562
1362
|
console.error('❌ TESTLENS ERROR: Test Cases Limit Reached');
|
|
@@ -1637,26 +1437,8 @@ export class TestLensReporter implements Reporter {
|
|
|
1637
1437
|
return contentTypes[ext] || 'application/octet-stream';
|
|
1638
1438
|
}
|
|
1639
1439
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const safeTestId = this.sanitizeForS3(testId);
|
|
1643
|
-
const safeFileName = this.sanitizeForS3(fileName);
|
|
1644
|
-
const ext = path.extname(fileName);
|
|
1645
|
-
const baseName = path.basename(fileName, ext);
|
|
1646
|
-
|
|
1647
|
-
return `test-artifacts/${date}/${runId}/${safeTestId}/${safeFileName}${ext}`;
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
private sanitizeForS3(value: string): string {
|
|
1651
|
-
return value
|
|
1652
|
-
.replace(/[\/:*?"<>|]/g, '-')
|
|
1653
|
-
.replace(/[-\u001f\u007f]/g, '-')
|
|
1654
|
-
.replace(/[^-~]/g, '-')
|
|
1655
|
-
.replace(/\s+/g, '-')
|
|
1656
|
-
.replace(/[_]/g, '-')
|
|
1657
|
-
.replace(/-+/g, '-')
|
|
1658
|
-
.replace(/^-|-$/g, '');
|
|
1659
|
-
}
|
|
1440
|
+
// Note: S3 key generation and sanitization are handled server-side
|
|
1441
|
+
// generateS3Key() and sanitizeForS3() methods removed as they were not used
|
|
1660
1442
|
|
|
1661
1443
|
private getFileSize(filePath: string): number {
|
|
1662
1444
|
try {
|
|
@@ -1670,3 +1452,4 @@ export class TestLensReporter implements Reporter {
|
|
|
1670
1452
|
}
|
|
1671
1453
|
|
|
1672
1454
|
export default TestLensReporter;
|
|
1455
|
+
|